Initial commit
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..6e87a00
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,13 @@
+# Editor configuration, see http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+max_line_length = off
+trim_trailing_whitespace = false
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4bb065d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,52 @@
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+
+# compiled output
+/dist
+/dist-server
+/tmp
+/out-tsc
+/unittests
+
+# dependencies
+/node_modules
+
+# IDEs and editors
+/.idea
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# IDE - VSCode
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# misc
+/.sass-cache
+/connect.lock
+/coverage
+/libpeerconnection.log
+npm-debug.log
+yarn-error.log
+testem.log
+/typings
+
+# e2e
+/e2e/*.js
+/e2e/*.map
+
+# System Files
+.DS_Store
+Thumbs.db
+package-lock.json
+/tsLint-Result.xml
+/unittests/Chrome_*
+/unittests/Chrome_*
+.vscode/launch.json
+.vscode/tasks.json
+.vscode
diff --git a/plannedGridMeasures.frontend_Test.txt b/.tgitconfig
similarity index 100%
rename from plannedGridMeasures.frontend_Test.txt
rename to .tgitconfig
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..34312ef
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,56 @@
+FROM myserver:withproxy
+#linux 16.04
+MAINTAINER Dimitris
+
+WORKDIR /
+### PORTAL ####
+WORKDIR portal
+RUN git clone http://172.18.22.160:8880/gitblit-1.8.0/r/oK/Portal/Backend.git && cd Backend && git checkout DEVELOP_BE
+WORKDIR Backend
+RUN rm -f -r ./target/portal && rm -f -r ./target/portal.war
+RUN mvn install -DskipTests
+RUN mv ./target/portal.war /opt/tomcat/webapps/portal.war
+
+WORKDIR /
+### PORTAL FE ####
+WORKDIR portal
+RUN git clone http://172.18.22.160:8880/gitblit-1.8.0/r/oK/Portal/Frontend.git && cd Frontend && git checkout DEVELOP_FE
+WORKDIR Frontend
+RUN npm install
+RUN ng build --prod
+RUN mv ./dist/ /opt/tomcat/webapps/Frontend/
+
+WORKDIR /
+### DIAGNOSIS APP ####
+WORKDIR microservices
+RUN git clone http://172.18.22.160:8880/gitblit-1.8.0/r/Dropwizard/Microservices/mics-diagnosis-app.git && cd mics-diagnosis-app && git checkout DEVELOP_FE
+WORKDIR mics-diagnosis-app
+RUN npm install
+RUN ng build --prod
+RUN mv ./dist/ /opt/tomcat/webapps/mics-diagnosis-app/
+
+WORKDIR /
+### HOME SERVICE ####
+WORKDIR microservices
+RUN git clone http://172.18.22.160:8880/gitblit-1.8.0/r/Dropwizard/Microservices/mics-home-service.git && cd mics-home-service && git checkout DEVELOP_BE
+WORKDIR mics-home-service
+RUN rm -f -r ./target/mics-home-service && rm -f -r ./target/mics-home-service.war
+RUN mvn install -DskipTests
+RUN mv ./target/mics-home-service.war /opt/tomcat/webapps/mics-home-service.war
+
+WORKDIR /
+### PLGM FRONTEND ####
+WORKDIR gridmeasures
+RUN git clone http://172.18.22.160:8880/gitblit-1.8.0/r/oK/PlannedGridMeasures/Frontend.git && cd Frontend && git checkout DEVELOP_FE
+WORKDIR Frontend
+RUN npm install
+RUN ng build --prod
+RUN mv ./dist/ /opt/tomcat/webapps/GridMeasuresFrontend/
+
+WORKDIR /
+
+CMD ["/opt/tomcat/bin/catalina.sh", "run"]
+
+EXPOSE 8080
+
+
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000..e68728c
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,18 @@
+node {
+
+ stage('Clone repository') {
+ /* Let's make sure we have the repository cloned to our workspace */
+
+ checkout scm
+ }
+
+ stage('Build image') {
+ /* This builds the actual image; synonymous to
+ * docker build on the command line */
+ sh 'docker build --no-cache --rm -t myserver_home:jenkinsversion .'
+ }
+
+ stage('Refresh containers') {
+ bat 'start cmd.exe /c C:\\Jenkinshelper\\refresh_docker.bat'
+ }
+}
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..37b0b81
--- /dev/null
+++ b/README.md
@@ -0,0 +1,27 @@
+# GridmeasureFE
+
+This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.7.2.
+
+## Development server
+
+Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app 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. Use the `-prod` flag for a production build.
+
+## 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 [Protractor](http://www.protractortest.org/).
+
+## Further help
+
+To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
diff --git a/angular.json b/angular.json
new file mode 100644
index 0000000..fe0a7e2
--- /dev/null
+++ b/angular.json
@@ -0,0 +1,148 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "newProjectRoot": "projects",
+ "projects": {
+ "gridmeasure-fe": {
+ "root": "",
+ "sourceRoot": "src",
+ "projectType": "application",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:browser",
+ "options": {
+ "outputPath": "dist",
+ "index": "src/index.html",
+ "main": "src/main.ts",
+ "tsConfig": "src/tsconfig.app.json",
+ "polyfills": "src/polyfills.ts",
+ "assets": [
+ "src/assets",
+ "src/favicon.ico"
+ ],
+ "styles": [
+ "src/app/custom_modules/calendar/angular-calendar-openk.css",
+ "node_modules/bootstrap/dist/css/bootstrap.min.css",
+ "node_modules/font-awesome/css/font-awesome.css",
+ "src/styles.css",
+ "node_modules/bootstrap-toggle/css/bootstrap-toggle.min.css"
+ ],
+ "scripts": [
+ "node_modules/jquery/dist/jquery.min.js",
+ "node_modules/bootstrap/dist/js/bootstrap.js",
+ "node_modules/bootstrap-toggle/js/bootstrap-toggle.min.js"
+ ]
+ },
+ "configurations": {
+ "production": {
+ "optimization": true,
+ "outputHashing": "all",
+ "sourceMap": false,
+ "extractCss": true,
+ "namedChunks": false,
+ "aot": true,
+ "extractLicenses": true,
+ "vendorChunk": false,
+ "buildOptimizer": true,
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.prod.ts"
+ }
+ ]
+ }
+ }
+ },
+ "serve": {
+ "builder": "@angular-devkit/build-angular:dev-server",
+ "options": {
+ "browserTarget": "gridmeasure-fe:build"
+ },
+ "configurations": {
+ "production": {
+ "browserTarget": "gridmeasure-fe:build:production"
+ }
+ }
+ },
+ "extract-i18n": {
+ "builder": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "browserTarget": "gridmeasure-fe:build"
+ }
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "main": "src/test.ts",
+ "karmaConfig": "./karma.conf.js",
+ "polyfills": "src/polyfills.ts",
+ "tsConfig": "src/tsconfig.spec.json",
+ "scripts": [
+ "node_modules/jquery/dist/jquery.min.js",
+ "node_modules/bootstrap/dist/js/bootstrap.js"
+ ],
+ "styles": [
+ "src/app/custom_modules/calendar/angular-calendar-openk.css",
+ "node_modules/bootstrap/dist/css/bootstrap.min.css",
+ "node_modules/font-awesome/css/font-awesome.css",
+ "src/styles.css"
+ ],
+ "assets": [
+ "src/assets",
+ "src/favicon.ico"
+ ]
+ }
+ },
+ "lint": {
+ "builder": "@angular-devkit/build-angular:tslint",
+ "options": {
+ "tsConfig": [
+ "src/tsconfig.app.json",
+ "src/tsconfig.spec.json"
+ ],
+ "exclude": [
+ "**/node_modules/**"
+ ],
+ "force": true,
+ "format": "checkstyle"
+ }
+ }
+ }
+ },
+ "gridmeasure-fe-e2e": {
+ "root": "",
+ "sourceRoot": "e2e",
+ "projectType": "application",
+ "architect": {
+ "e2e": {
+ "builder": "@angular-devkit/build-angular:protractor",
+ "options": {
+ "protractorConfig": "./protractor.conf.js",
+ "devServerTarget": "gridmeasure-fe:serve"
+ }
+ },
+ "lint": {
+ "builder": "@angular-devkit/build-angular:tslint",
+ "options": {
+ "tsConfig": [
+ "e2e/tsconfig.e2e.json"
+ ],
+ "exclude": [
+ "**/node_modules/**"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "defaultProject": "gridmeasure-fe",
+ "schematics": {
+ "@schematics/angular:component": {
+ "prefix": "app",
+ "styleext": "css"
+ },
+ "@schematics/angular:directive": {
+ "prefix": "app"
+ }
+ }
+}
\ No newline at end of file
diff --git a/e2e/app.e2e-spec.ts b/e2e/app.e2e-spec.ts
new file mode 100644
index 0000000..080f3ff
--- /dev/null
+++ b/e2e/app.e2e-spec.ts
@@ -0,0 +1,14 @@
+import { AppPage } from './app.po';
+
+describe('gridmeasure-fe App', () => {
+ let page: AppPage;
+
+ beforeEach(() => {
+ page = new AppPage();
+ });
+
+ it('should display welcome message', () => {
+ page.navigateTo();
+ expect(page.getParagraphText()).toEqual('Welcome to app!');
+ });
+});
diff --git a/e2e/app.po.ts b/e2e/app.po.ts
new file mode 100644
index 0000000..82ea75b
--- /dev/null
+++ b/e2e/app.po.ts
@@ -0,0 +1,11 @@
+import { browser, by, element } from 'protractor';
+
+export class AppPage {
+ navigateTo() {
+ return browser.get('/');
+ }
+
+ getParagraphText() {
+ return element(by.css('app-root h1')).getText();
+ }
+}
diff --git a/e2e/tsconfig.e2e.json b/e2e/tsconfig.e2e.json
new file mode 100644
index 0000000..1d9e5ed
--- /dev/null
+++ b/e2e/tsconfig.e2e.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../out-tsc/e2e",
+ "baseUrl": "./",
+ "module": "commonjs",
+ "target": "es5",
+ "types": [
+ "jasmine",
+ "jasminewd2",
+ "node"
+ ]
+ }
+}
diff --git a/karma.conf.js b/karma.conf.js
new file mode 100644
index 0000000..b0a37c5
--- /dev/null
+++ b/karma.conf.js
@@ -0,0 +1,49 @@
+// Karma configuration file, see link for more information
+// https://karma-runner.github.io/1.0/config/configuration-file.html
+
+module.exports = function (config) {
+ config.set({
+ basePath: '',
+ browserNoActivityTimeout: 30000,
+ browserDisconnectTimeout: 10000,
+ browserDisconnectTolerance: 3,
+ frameworks: ['jasmine', '@angular-devkit/build-angular'],
+ plugins: [
+ require('karma-jasmine'),
+ require('karma-chrome-launcher'),
+ require('karma-jasmine-html-reporter'),
+ require('karma-coverage-istanbul-reporter'),
+ require('@angular-devkit/build-angular/plugins/karma'),
+ require('karma-junit-reporter')
+ ],
+ client: {
+ clearContext: false // leave Jasmine Spec Runner output visible in browser
+ },
+ coverageIstanbulReporter: {
+ dir: require('path').join(__dirname, 'coverage'),
+ reports: ['html', 'lcovonly'],
+ fixWebpackSourcePaths: true
+ },
+ reporters: config.angularCli && config.angularCli.codeCoverage ? ['progress', 'coverage-istanbul'] : ['progress', 'kjhtml', 'dots', 'junit'],
+ junitReporter: {
+ outputDir: 'unittests',
+ outputFile: 'test-results.xml'
+ },
+ port: 9876,
+ colors: true,
+ logLevel: config.LOG_INFO,
+ autoWatch: true,
+ browsers: ['ChromeDebugging'],
+ customLaunchers: {
+ ChromeDebugging: {
+ base: 'Chrome',
+ flags: ['--remote-debugging-port=9333']
+ },
+ ChromiumNoSandbox: {
+ base: 'ChromiumHeadless',
+ flags: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-translate', '--disable-extensions']
+ }
+ },
+ singleRun: false
+ });
+};
diff --git a/launchExampleHowToRunDebugger.json b/launchExampleHowToRunDebugger.json
new file mode 100644
index 0000000..c850071
--- /dev/null
+++ b/launchExampleHowToRunDebugger.json
@@ -0,0 +1,19 @@
+{
+ // Verwendet IntelliSense zum Ermitteln möglicher Attribute.
+ // Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen.
+ // Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "chrome",
+ "request": "launch",
+ "name": "Launch Chrome 4220",
+ "url": "http://localhost:4220/",
+ "sourceMaps": true,
+ "webRoot": "${workspaceRoot}",
+ "sourceMapPathOverrides": {
+ "webpack:///./src/*": "${workspaceRoot}/src/*"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..ee5521f
--- /dev/null
+++ b/package.json
@@ -0,0 +1,77 @@
+{
+ "name": "gridmeasure-fe",
+ "version": "0.0.0",
+ "license": "MIT",
+ "scripts": {
+ "ng": "ng",
+ "start": "ng serve --source-map --proxy-config proxy.conf.json --port 4220",
+ "build": "ng build --prod",
+ "test": "ng test --browsers=ChromeDebugging",
+ "lint": "ng lint",
+ "lint-ci": "ng lint > tsLint-Result.xml",
+ "e2e": "ng e2e",
+ "test-ci": "ng test --watch=false --browsers=ChromiumNoSandbox --code-coverage"
+ },
+ "private": true,
+ "dependencies": {
+ "@angular/animations": "5.2.11",
+ "@angular/cdk": "5.2.5",
+ "@angular/common": "5.2.11",
+ "@angular/compiler": "5.2.11",
+ "@angular/core": "5.2.11",
+ "@angular/forms": "5.2.11",
+ "@angular/http": "5.2.11",
+ "@angular/platform-browser": "5.2.11",
+ "@angular/platform-browser-dynamic": "5.2.11",
+ "@angular/router": "5.2.11",
+ "@auth0/angular-jwt": "1.0",
+ "ag-grid": "18.0.1",
+ "ag-grid-angular": "18.0.1",
+ "ajv": "6.5.2",
+ "angular-calendar": "0.23.7",
+ "angular2-uuid": "1.1.1",
+ "bootstrap": "3.3.7",
+ "bootstrap-toggle": "2.2.2",
+ "classlist.js": "1.1.20150312",
+ "core-js": "2.5.7",
+ "file-saver": "1.3.8",
+ "font-awesome": "4.7.0",
+ "jquery": "3.3.1",
+ "ng2-daterangepicker": "2.0.12",
+ "ng2-tree": "^2.0.0-rc.11",
+ "popper.js": "1.14.3",
+ "primeicons": "1.0.0-beta.10",
+ "primeng": "6.1.3",
+ "rxjs": "5.5.11",
+ "web-animations-js": "2.3.1",
+ "zone.js": "0.8.26"
+ },
+ "devDependencies": {
+ "@angular-devkit/build-angular": "0.6.8",
+ "@angular-devkit/build-optimizer": "0.6.8",
+ "@angular-devkit/core": "0.6.8",
+ "@angular-devkit/schematics": "0.6.8",
+ "@angular/cli": "6.0.8",
+ "@angular/compiler-cli": "5.2.11",
+ "@angular/language-service": "5.2.11",
+ "@types/bootstrap": "4.1.2",
+ "@types/file-saver": "1.3.0",
+ "@types/jasmine": "2.8.8",
+ "@types/jasminewd2": "2.0.3",
+ "@types/jquery": "3.3.4",
+ "@types/node": "6.0.113",
+ "codelyzer": "4.4.2",
+ "jasmine-core": "2.8.0",
+ "jasmine-spec-reporter": "4.2.1",
+ "karma": "2.0.4",
+ "karma-chrome-launcher": "2.2.0",
+ "karma-coverage-istanbul-reporter": "1.4.3",
+ "karma-jasmine": "1.1.2",
+ "karma-jasmine-html-reporter": "0.2.2",
+ "karma-junit-reporter": "1.2.0",
+ "protractor": "5.4.0",
+ "ts-node": "4.1.0",
+ "tslint": "5.9.1",
+ "typescript": "2.5.3"
+ }
+}
\ No newline at end of file
diff --git a/protractor.conf.js b/protractor.conf.js
new file mode 100644
index 0000000..7ee3b5e
--- /dev/null
+++ b/protractor.conf.js
@@ -0,0 +1,28 @@
+// Protractor configuration file, see link for more information
+// https://github.com/angular/protractor/blob/master/lib/config.ts
+
+const { SpecReporter } = require('jasmine-spec-reporter');
+
+exports.config = {
+ allScriptsTimeout: 11000,
+ specs: [
+ './e2e/**/*.e2e-spec.ts'
+ ],
+ capabilities: {
+ 'browserName': 'chrome'
+ },
+ directConnect: true,
+ baseUrl: 'http://localhost:4200/',
+ framework: 'jasmine',
+ jasmineNodeOpts: {
+ showColors: true,
+ defaultTimeoutInterval: 30000,
+ print: function() {}
+ },
+ onPrepare() {
+ require('ts-node').register({
+ project: 'e2e/tsconfig.e2e.json'
+ });
+ jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
+ }
+};
diff --git a/proxy.conf.json b/proxy.conf.json
new file mode 100644
index 0000000..8b6ac7a
--- /dev/null
+++ b/proxy.conf.json
@@ -0,0 +1,6 @@
+{
+ "/mics-home-service": {
+ "target": "http://localhost:8080/",
+ "secure": false
+ }
+}
diff --git a/sonar-project.properties b/sonar-project.properties
new file mode 100644
index 0000000..c72e4d8
--- /dev/null
+++ b/sonar-project.properties
@@ -0,0 +1,18 @@
+# must be unique in a given SonarQube instance
+sonar.projectKey=openk.pta.de:plannedGridMeasures-FE
+# this is the name and version displayed in the SonarQube UI. Was mandatory prior to SonarQube 6.1.
+sonar.projectName=plannedGridMeasures-FE
+sonar.projectVersion=0.0.1_Snapshot
+
+# Language
+sonar.language=ts
+
+sonar.sourceEncoding=UTF-8
+sonar.sources=src
+sonar.exclusions=**/node_modules/**,**/*.spec.ts,**/test-data/*.ts,**/testing/*.ts,**/assets/js/*.js
+sonar.tests=src
+sonar.test.inclusions=**/*.spec.ts
+sonar.test.exclusions=**/multiselect-dropdown/*.ts
+sonar.ts.tslintconfigpath=tslint.json
+
+sonar.typescript.lcov.reportPaths=coverage/lcov.info
\ No newline at end of file
diff --git a/src/app/app.component.css b/src/app/app.component.css
new file mode 100644
index 0000000..6b92ab3
--- /dev/null
+++ b/src/app/app.component.css
@@ -0,0 +1,21 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+.app-content {
+ padding-top: 33px;
+}
+
+@media screen and (max-width: 767px) {
+ .app-content {
+ padding-top: 100px;
+ }
+}
\ No newline at end of file
diff --git a/src/app/app.component.html b/src/app/app.component.html
new file mode 100644
index 0000000..bd74741
--- /dev/null
+++ b/src/app/app.component.html
@@ -0,0 +1,16 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+-->
+<app-main-navigation [hidden]="router.url == '/logout' ||router.url == '/loggedout' "></app-main-navigation>
+<app-toaster></app-toaster>
+<div class="app-content">
+ <router-outlet></router-outlet>
+</div>
\ No newline at end of file
diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts
new file mode 100644
index 0000000..b0928ff
--- /dev/null
+++ b/src/app/app.component.spec.ts
@@ -0,0 +1,189 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { TestBed, async } from '@angular/core/testing';
+import { ActivatedRoute, Router } from '@angular/router';
+import { AppComponent } from './app.component';
+import { SessionContext } from './common/session-context';
+import { HttpResponseInterceptorService } from './services/http-response-interceptor.service';
+import { BaseDataLoaderService } from './services/jobs/base-data-loader.service';
+import { MessageServiceCustom } from './services/message.service';
+import { ReminderCallerJobService } from './services/jobs/reminder-caller-job.service';
+import { AbstractMockObservableService } from './testing/abstract-mock-observable.service';
+import { MockComponent } from './testing/mock.component';
+import { HttpClient } from '@angular/common/http';
+import { ToasterMessageService } from './services/toaster-message.service';
+
+class FakeRouter {
+ navigate(commands: any[]) {
+ return commands[0];
+ }
+}
+
+class MockHttp extends AbstractMockObservableService {
+ get() {
+ return this;
+ }
+}
+
+class MockHttpRes {
+ public content: any;
+ json() { return this.content; }
+}
+
+describe('AppComponent', () => {
+ const mockService = new MockHttp();
+ let sessionContext: SessionContext;
+ let routerStub: FakeRouter;
+
+
+
+ beforeEach(() => {
+ sessionContext = new SessionContext();
+ routerStub = {
+ navigate: jasmine.createSpy('navigate').and.callThrough()
+ };
+
+ TestBed.configureTestingModule({
+ declarations: [
+ AppComponent,
+ MockComponent({ selector: 'app-main-navigation' }),
+ MockComponent({ selector: 'app-toaster' }),
+ MockComponent({ selector: 'router-outlet' })
+ ],
+ providers: [
+ { provide: HttpClient, useValue: mockService },
+ { provide: Router, useValue: routerStub },
+ { provide: BaseDataLoaderService, useValue: {} },
+ { provide: ActivatedRoute },
+ { provide: HttpResponseInterceptorService },
+ { provide: SessionContext, useValue: sessionContext },
+ { provide: MessageServiceCustom },
+ { provide: ToasterMessageService },
+ { provide: ReminderCallerJobService }
+ ],
+ });
+ TestBed.compileComponents();
+
+ const httpRes = new MockHttpRes();
+ httpRes.content = {};
+ mockService.content = httpRes;
+
+
+ });
+
+ it('should create the app', async(() => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.debugElement.componentInstance;
+ expect(app).toBeTruthy();
+ }));
+
+
+ it('should extract the user data from the JWT-AccessToken', async(() => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.debugElement.componentInstance;
+
+ const accessToken = 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJodVl0e' +
+ 'VByUEVLQ1phY3FfMW5sOGZscENETnFHdmZEZHctYUxGQXNoWHZVIn0.eyJqdGkiOiI4ZmY5NTlhZC' +
+ '02ODQ1LTRlOGEtYjRiYi02ODQ0YjAwMjU0ZjgiLCJleHAiOjE1MDY2MDA0NTAsIm5iZiI6MCwiaWF' +
+ '0IjoxNTA2NjAwMTUwLCJpc3MiOiJodHRwOi8vZW50amF2YTAwMjo4MDgwL2F1dGgvcmVhbG1zL2Vs' +
+ 'b2dib29rIiwiYXVkIjoiZWxvZ2Jvb2stYmFja2VuZCIsInN1YiI6IjM1OWVmOWM5LTc3ZGYtNGEzZ' +
+ 'C1hOWM5LWY5NmQ4MzdkMmQ1NyIsInR5cCI6IkJlYXJlciIsImF6cCI6ImVsb2dib29rLWJhY2tlbm' +
+ 'QiLCJhdXRoX3RpbWUiOjAsInNlc3Npb25fc3RhdGUiOiI5NjVmNzM1MS0yZThiLTQ1MjgtOWYzZC1' +
+ 'lZTYyODNhOTViMTYiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNj' +
+ 'ZXNzIjp7InJvbGVzIjpbImVsb2dib29rLXN1cGVydXNlciIsImVsb2dib29rLW5vcm1hbHVzZXIiL' +
+ 'CJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7InJlYWxtLW1hbmFnZW1lbn' +
+ 'QiOnsicm9sZXMiOlsidmlldy11c2VycyIsInF1ZXJ5LWdyb3VwcyIsInF1ZXJ5LXVzZXJzIl19LCJ' +
+ 'hY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3Mi' +
+ 'LCJ2aWV3LXByb2ZpbGUiXX19LCJuYW1lIjoiQWRtaW5pc3RyYXRvciBBZG1pbmlzdHJhdG93aWNoI' +
+ 'iwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWRtaW4iLCJnaXZlbl9uYW1lIjoiQWRtaW5pc3RyYXRvci' +
+ 'IsImZhbWlseV9uYW1lIjoiQWRtaW5pc3RyYXRvd2ljaCIsImVtYWlsIjoic2VyZ2VqLmtlcm5AcHRh' +
+ 'LmRlIiwicm9sZXN0ZXN0IjoiW2Vsb2dib29rLXN1cGVydXNlciwgZWxvZ2Jvb2stbm9ybWFsdXNlc' +
+ 'iwgdW1hX2F1dGhvcml6YXRpb24sIG9mZmxpbmVfYWNjZXNzLCB1bWFfYXV0aG9yaXphdGlvbiwgZW' +
+ 'xvZ2Jvb2stbm9ybWFsdXNlcl0ifQ.o94Bl43oqyLNzZRABvIq9z-XI8JQjqj2FSDdUUEZGZPTN4uw' +
+ 'D5fyi0sONbDxmTFvgWPh_8ZhX6tlDGiupVDBY4eRH43Eettm-t4CDauL7FzB3w3dDPFMB5DhP4rrp' +
+ 'k_kATwnY2NKLRbequnh8Z6wLXjcmQNLgrgknXB_gogWAqH29dqKexwceMNIbq-kjaeLsmHSXM9TE9' +
+ 'q7_Ln9el04OlkpOVspVguedfINcNFg0DmYLJWyD2ORkOHLmYigN6YnyB9P2NFOnKGlLuQ87GjosI0' +
+ '0zBniRGi3PhE9NGd51Qggdbcsm0aM8GiMaZ7SO5i8iQWL10TRFRFyTEfy6hSO8g';
+
+ const incognito: any = app;
+ incognito.processAccessToken(accessToken);
+
+ expect(sessionContext.getCurrUser().name).toBe('Administrator Administratowich');
+ expect(sessionContext.getAccessToken()).toBe(accessToken);
+ }));
+
+ it('read the param access token', () => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.debugElement.componentInstance;
+ const token = 'AJDHDND';
+ const uri = 'a=X&b=y&accessToken=' + token;
+ const inkognito: any = app;
+ const token2 = inkognito.readParamAccessToken(uri, 'accessToken');
+
+ expect(token2).toBe(token);
+ });
+
+ it('RC ==== 401', () => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.debugElement.componentInstance;
+ const inkognito: any = app;
+ inkognito.onRcFromHttpService(401);
+
+ expect(routerStub.navigate).toHaveBeenCalledWith(['/logout']);
+ });
+
+ it('RC !=== 401', () => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.debugElement.componentInstance;
+ const inkognito: any = app;
+ inkognito.onRcFromHttpService(402);
+
+ expect(routerStub.navigate).not.toHaveBeenCalledWith(['/logout']);
+ });
+
+ it('should go into both IFs of procesAcessToken', async(() => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.debugElement.componentInstance;
+
+ const accessToken = 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJIYlI3Z' +
+ '2pobmE2eXJRZnZJTWhUSV9tY2g3ZmtTQWVFX3hLTjBhZVl0bjdjIn0.eyJqdGkiOiI1MmM1ZmMxOS' +
+ '04ZGVkLTRmZDktODVmOC1kNzBkZDY1YWZlZDMiLCJleHAiOjE1MzA3MDY3NTEsIm5iZiI6MCwiaWF' +
+ '0IjoxNTMwNzA2NDUxLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvRWxv' +
+ 'Z2Jvb2siLCJhdWQiOiJlbG9nYm9vay1iYWNrZW5kIiwic3ViIjoiOWE2OGQwYmQtMDMzNS00YjNiL' +
+ 'WIxMjgtN2E1ZTc0N2QzZmY5IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiZWxvZ2Jvb2stYmFja2VuZC' +
+ 'IsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6IjI1OWE3OTQ2LWQ0OTMtNGFmZC04YWI5LTg' +
+ '1NGRhZGE3NDkxMSIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiKiJdLCJyZWFsbV9hY2Nl' +
+ 'c3MiOnsicm9sZXMiOlsicGxhbm5lZC1wb2xpY2llcy1hY2Nlc3MiLCJwbGFubmVkLXBvbGljaWVzL' +
+ 'W5vcm1hbHVzZXIiLCJwbGFubmVkLXBvbGljaWVzLW1lYXN1cmVhcHBsaWNhbnQiXX0sInJlc291cm' +
+ 'NlX2FjY2VzcyI6e30sInByZWZlcnJlZF91c2VybmFtZSI6InRlc3R1c2VyMSJ9.YGDE--qV8XiRas' +
+ 'R7S3_QOC34gz_eMLx9MWhUHM2KcckY79YcknDQZ9rBaqjLTfw8AEbsAyTuiL8zaw17u1ge-1TVmPh' +
+ 'VaUgvbctb1lUnn-4ZMyX8JObYuTxMnPxZSYVQjwqgE80q15aHd_TtXG8OXNbR6941Ymoel2B-lWuo' +
+ '3UJR2Aoiq-bVX4OSn1_C_4Ca4EbWSIdN85lfV1xiXKu97l8nv1MjaxuET7LrcFT4z12Qsq7EEXcFC' +
+ '7KsJRVpOLEW2xBRgRuWV_wvIyIrA2vacQRvlfbOHmrltqjfAuOEZArQsLwNtunL4duDh4U8qefiXL' +
+ 'JFPnEN6SpCh2pjGEdgBg';
+
+ const incognito: any = app;
+ incognito.processAccessToken(accessToken);
+
+ expect(sessionContext.getAccessToken()).toBe(accessToken);
+ }));
+
+ it('getParametersFromURL is entered', () => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.debugElement.componentInstance;
+ const inkognito: any = app;
+ const param1 = inkognito.getParametersFromUrl();
+ // ist das richtig? Soll das so? Was steht im window.location.search.substr(1)?
+ expect(param1).toBe(null);
+ });
+
+});
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
new file mode 100644
index 0000000..5621017
--- /dev/null
+++ b/src/app/app.component.ts
@@ -0,0 +1,121 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Component, OnInit } from '@angular/core';
+import { Router, ActivatedRoute, Params } from '@angular/router';
+import { BaseDataLoaderService } from './services/jobs/base-data-loader.service';
+import { HttpResponseInterceptorService } from './services/http-response-interceptor.service';
+import { SessionContext } from './common/session-context';
+import { MessageServiceCustom, MessageDefines } from './services/message.service';
+import { BannerMessageStatusEn } from './common/enums';
+import { User } from './model/user';
+import { JwtHelperService } from '@auth0/angular-jwt';
+import { HttpClient } from '@angular/common/http';
+import { ReminderCallerJobService } from './services/jobs/reminder-caller-job.service';
+import { Globals } from './common/globals';
+
+@Component({
+ selector: 'app-root',
+ templateUrl: './app.component.html',
+ styleUrls: ['./app.component.css']
+})
+export class AppComponent implements OnInit {
+ title = 'app';
+ private bannerMessageStatus = BannerMessageStatusEn;
+
+ constructor(private http: HttpClient, public router: Router,
+ public baseDataLoader: BaseDataLoaderService,
+ public reminderCallerJobService: ReminderCallerJobService,
+ private activatedRoute: ActivatedRoute,
+ public httpInterceptor: HttpResponseInterceptorService,
+ private sessionContext: SessionContext,
+ private messageService: MessageServiceCustom) {
+
+ this.http.get('assets/settings.json')
+ .subscribe(res => this.sessionContext.settings = res);
+
+ this.sessionContext.centralHttpResultCode$.subscribe(rc => {
+ this.onRcFromHttpService(rc);
+ });
+ }
+ ngOnInit() {
+ this.extractTokenFromParameters();
+ this.processDirectLinkToGridMeasureDetail();
+ }
+
+ private processAccessToken(accessToken: string) {
+ const jwtHelper: JwtHelperService = new JwtHelperService();
+ const decoded: any = jwtHelper.decodeToken(accessToken);
+ const user: User = new User();
+ user.id = decoded.sub;
+ user.username = decoded.preferred_username;
+ user.itemName = decoded.preferred_username;
+ let firstName = decoded.given_name;
+ if (!firstName) {
+ firstName = '';
+ }
+ let lastName = decoded.family_name;
+ if (!lastName) {
+ lastName = '';
+ }
+
+ user.name = firstName + ' ' + lastName;
+ user.roles = decoded.realm_access.roles.filter(r => r.includes('planned-policies'));
+
+ this.sessionContext.setCurrUser(user);
+ this.sessionContext.setAccessToken(accessToken);
+ }
+
+ /**
+ * Extract the params (suscribe to router event) and store them in the sessionContext.
+ */
+ private extractTokenFromParameters() {
+ this.activatedRoute.params.subscribe((params) => {
+ const accessToken = this.getParametersFromUrl('accessToken');
+
+ if (accessToken) {
+ this.processAccessToken(accessToken);
+ }
+ this.messageService.loginLogoff$.emit(MessageDefines.MSG_LOG_IN_SUCCEEDED);
+ });
+
+ }
+
+ private processDirectLinkToGridMeasureDetail() {
+ const fwdId = this.getParametersFromUrl('fwdId');
+ if (fwdId) {
+ this.router.navigate(['/gridMeasureDetail/', fwdId, Globals.MODE.EDIT]);
+ }
+ }
+
+ private getParametersFromUrl(findParam) {
+ const parameterUrl = window.location.search.substr(1);
+ return parameterUrl != null && parameterUrl !== '' ? this.readParamAccessToken(parameterUrl, findParam) : null;
+ }
+
+
+ private readParamAccessToken(prmstr, findParam) {
+ const params = {};
+ const prmarr = prmstr.split('&');
+ for (let i = 0; i < prmarr.length; i++) {
+ const tmparr = prmarr[i].split('=');
+ params[tmparr[0]] = tmparr[1];
+ }
+ return params[findParam];
+ }
+
+ private onRcFromHttpService(rc: number): void {
+ if (rc === 401) {
+ this.router.navigate(['/logout']);
+ }
+ }
+
+}
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
new file mode 100644
index 0000000..070ee77
--- /dev/null
+++ b/src/app/app.module.ts
@@ -0,0 +1,166 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { BrowserModule } from '@angular/platform-browser';
+import { CalendarModule } from 'angular-calendar';
+import { Daterangepicker } from 'ng2-daterangepicker';
+import { AppComponent } from './app.component';
+import { ButtonsContainerComponent } from './common-components/buttons-container/buttons-container.component';
+import { LoadingSpinnerComponent } from './common-components/loading-spinner/loading-spinner.component';
+import { MainNavigationComponent } from './common-components/main-navigation/main-navigation.component';
+import { MessageBannerComponent } from './common-components/message-banner/message-banner.component';
+import { FormattedDatePipe } from './common-components/pipes/formatted-date.pipe';
+import { FormattedTimestampPipe } from './common-components/pipes/formatted-timestamp.pipe';
+import { StringToDatePipe } from './common-components/pipes/string-to-date.pipe';
+import { UniquePipe } from './common-components/pipes/unique.pipe';
+import { VersionInfoComponent } from './common-components/version-info/version-info.component';
+import { SessionContext } from './common/session-context';
+import { CustomCalendarComponent } from './custom_modules/calendar/calendar.component';
+import { CustomCalendarModule } from './custom_modules/calendar/calendar.module';
+import { AppRoutingModule } from './custom_modules/routing/app-routing.module';
+import { UndoRedoStackModule } from './custom_modules/undo-redo-stack/undo-redo-stack.module';
+import { AbstractListComponent } from './lists/common-components/abstract-list/abstract-list.component';
+import { GridMeasuresComponent } from './lists/grid-measures/grid-measures.component';
+import { GridMeasureDetailComponent } from './pages/grid-measure-detail/grid-measure-detail.component';
+import { StatusChangesComponent } from './lists/status-changes/status-changes.component';
+import { StepsComponent } from './lists/steps/steps.component';
+import { StepComponent } from './pages/step/step.component';
+import { EmailDistributionListComponent } from './lists/email-distribution-list/email-distribution-list.component';
+import { EmailDistributionEntryComponent } from './pages/email-distribution-entry/email-distribution-entry.component';
+import { LoggedoutPageComponent } from './pages/loggedout/loggedout.component';
+import { LogoutPageComponent } from './pages/logout/logout.component';
+import { OverviewComponent } from './pages/overview/overview.component';
+import { AuthenticationService } from './services/authentication.service';
+import { BaseDataService } from './services/base-data.service';
+import { BaseHttpService } from './services/base-http.service';
+import { DocumentService } from './services/document.service';
+import { GridMeasureService } from './services/grid-measure.service';
+import { HttpResponseInterceptorService } from './services/http-response-interceptor.service';
+import { BaseDataLoaderService } from './services/jobs/base-data-loader.service';
+import { LockHelperService } from './services/lock-helper.service';
+import { LockService } from './services/lock.service';
+import { MessageServiceCustom } from './services/message.service';
+import { ReminderCallerJobService } from './services/jobs/reminder-caller-job.service';
+import { ReminderService } from './services/reminder.service';
+import { UserSettingsService } from './services/user-settings.service';
+import { UserService } from './services/user.service';
+import { VersionInfoService } from './services/version-info.service';
+import { RoleAccessHelperService } from './services/jobs/role-access-helper.service';
+import { RequestInterceptor } from './services/request-interceptor.service';
+import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
+import { LOCALE_ID, NgModule } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import localeDe from '@angular/common/locales/de';
+import { registerLocaleData } from '@angular/common';
+import { ModeValidator } from './custom_modules/helpers/mode-validator';
+import { SingleGridMeasureDetailTabComponent } from './pages/single-grid-measure-detail-tab/single-grid-measure-detail-tab.component';
+import { BackendSettingsService } from './services/backend-settings.service';
+import { GridMeasureDetailTabComponent } from './pages/grid-measure-detail-tab/grid-measure-detail-tab.component';
+import { RoleAccessService } from './services/role-access.service';
+import { AgGridModule } from 'ag-grid-angular';
+import { CimCacheService } from './services/cim-cache.service';
+import { AuthGuard } from './services/auth-guard.service';
+import { GridConfigModifierComponent } from './pages/grid-config-modifier/grid-config-modifier.component';
+import { ModifyGridConfigService } from './services/modify-grid-config.service';
+import { CancelGridMeasureComponent } from './pages/cancel-grid-measure/cancel-grid-measure.component';
+import { GridMeasureDetailHeaderComponent } from './pages/grid-measure-detail-header/grid-measure-detail-header.component';
+import { TreeModule } from 'ng2-tree';
+import { ToasterComponent } from './common-components/toaster/toaster.component';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { ToastModule } from 'primeng/toast';
+import { MessageService } from 'primeng/api';
+import { ToasterMessageService } from './services/toaster-message.service';
+
+registerLocaleData(localeDe, 'de');
+
+@NgModule({
+ declarations: [
+ AppComponent,
+ CustomCalendarComponent,
+ MainNavigationComponent,
+ VersionInfoComponent,
+ LoadingSpinnerComponent,
+ MessageBannerComponent,
+ GridMeasuresComponent,
+ StatusChangesComponent,
+ StepsComponent,
+ StepComponent,
+ EmailDistributionListComponent,
+ EmailDistributionEntryComponent,
+ OverviewComponent,
+ GridMeasureDetailComponent,
+ ButtonsContainerComponent,
+ LogoutPageComponent,
+ LoggedoutPageComponent,
+ AbstractListComponent,
+ CustomCalendarComponent,
+ FormattedDatePipe,
+ FormattedTimestampPipe,
+ StringToDatePipe,
+ UniquePipe,
+ GridMeasureDetailTabComponent,
+ SingleGridMeasureDetailTabComponent,
+ GridConfigModifierComponent,
+ CancelGridMeasureComponent,
+ GridMeasureDetailHeaderComponent,
+ ToasterComponent
+ ],
+ imports: [
+ BrowserModule,
+ AgGridModule.withComponents([]),
+ FormsModule,
+ Daterangepicker,
+ CalendarModule.forRoot(),
+ UndoRedoStackModule,
+ CustomCalendarModule,
+ HttpClientModule,
+ AppRoutingModule,
+ ReactiveFormsModule,
+ TreeModule,
+ BrowserAnimationsModule,
+ ToastModule
+ ],
+ providers: [
+ SessionContext,
+ ModeValidator,
+ {
+ provide: HTTP_INTERCEPTORS,
+ useClass: RequestInterceptor,
+ multi: true
+ },
+ { provide: LOCALE_ID, useValue: 'de' },
+ MessageServiceCustom,
+ MessageService,
+ ToasterMessageService,
+ RoleAccessService,
+ BaseHttpService,
+ BaseDataLoaderService,
+ BaseDataService,
+ ReminderCallerJobService,
+ ReminderService,
+ UserService,
+ DocumentService,
+ UserSettingsService,
+ HttpResponseInterceptorService,
+ VersionInfoService,
+ AuthenticationService,
+ GridMeasureService,
+ LockService,
+ LockHelperService,
+ RoleAccessHelperService,
+ BackendSettingsService,
+ CimCacheService,
+ AuthGuard,
+ ModifyGridConfigService
+ ],
+ bootstrap: [AppComponent]
+})
+export class AppModule { }
diff --git a/src/app/common-components/buttons-container/buttons-container.component.css b/src/app/common-components/buttons-container/buttons-container.component.css
new file mode 100644
index 0000000..a08ae7b
--- /dev/null
+++ b/src/app/common-components/buttons-container/buttons-container.component.css
@@ -0,0 +1,30 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+******************************************************************************
+ */
+
+.button-container {
+ position: fixed;
+ z-index: 800;
+ background-color: #f8fafd;
+ padding: 12px 30px 18px;
+ width: 100%;
+}
+
+.button-container .btn.pull-left {
+ margin-right: 6px;
+}
+
+.button-container .btn.pull-right {
+ margin-left: 6px;
+}
+
+#statuschange {
+ margin-left: 60px;
+}
\ No newline at end of file
diff --git a/src/app/common-components/buttons-container/buttons-container.component.html b/src/app/common-components/buttons-container/buttons-container.component.html
new file mode 100644
index 0000000..862f5aa
--- /dev/null
+++ b/src/app/common-components/buttons-container/buttons-container.component.html
@@ -0,0 +1,27 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+******************************************************************************
+-->
+<div class="button-container col-md-12">
+ <button id="quit" (click)="onClickEvents('quit')" type="button" class="btn btn-primary pull-left">{{Globals.STATUS_BUTTON_LABEL['quit']}}</button>
+
+ <!-- <button id="cancel" *ngIf="!hideCancelButtonForStatus && !hideCancelButtonForRole" (click)="onClickEvents('cancel')" type="button"
+ class="btn btn-primary pull-left">{{Globals.STATUS_BUTTON_LABEL['cancel']}}</button> -->
+
+ <button id="reject" *ngIf="!hideRejectButton" (click)="onClickEvents('reject')" type="button" class="btn btn-primary pull-left">{{Globals.STATUS_BUTTON_LABEL['reject']}}</button>
+
+ <button id="save" *ngIf="!hideSaveButton" (click)="onClickEvents('save')" type="button" class="btn btn-primary pull-right"
+ [disabled]="!validForSave" title="{{ !validForSave ? 'Bitte Titel der Maßnahme ausfüllen' : ''}}">{{Globals.STATUS_BUTTON_LABEL['save']}}</button>
+
+ <button id="statuschange" *ngIf="!noActivePositiveBtn" [disabled]="!validForm" (click)="onClickEvents(positiveStatusChange)"
+ type="button" class="btn btn-primary pull-right btn-margin-right" title="{{ !validForm ? 'Bitte alle Pflichtfelder ausfüllen' : ''}}">{{positiveBtnLabel}}</button>
+
+ <button id="duplicate" *ngIf="!hideDuplicateButtonForRole" (click)="onClickEvents('duplicate')" type="button" class="btn btn-primary pull-right btn-margin-right">{{Globals.STATUS_BUTTON_LABEL['duplicate']}}</button>
+
+</div>
\ No newline at end of file
diff --git a/src/app/common-components/buttons-container/buttons-container.component.spec.ts b/src/app/common-components/buttons-container/buttons-container.component.spec.ts
new file mode 100644
index 0000000..1b0aa87
--- /dev/null
+++ b/src/app/common-components/buttons-container/buttons-container.component.spec.ts
@@ -0,0 +1,518 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+******************************************************************************
+ */
+import { async, ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing';
+import { SimpleChange } from '@angular/core';
+import { SessionContext } from '../../common/session-context';
+import { ButtonsContainerComponent } from './buttons-container.component';
+import { Globals } from '../../common/globals';
+import { RoleAccessHelperService } from '../../services/jobs/role-access-helper.service';
+import { ROLE_ACCESS } from '../../test-data/role-access-definition';
+
+describe('ButtonsContainerComponent', () => {
+ let component: ButtonsContainerComponent;
+ let fixture: ComponentFixture<ButtonsContainerComponent>;
+ let sessionContext: SessionContext;
+ let roleAccessHelper: RoleAccessHelperService;
+
+ beforeEach(async(() => {
+ sessionContext = new SessionContext();
+ roleAccessHelper = new RoleAccessHelperService();
+
+ TestBed.configureTestingModule({
+ declarations: [ButtonsContainerComponent],
+ providers: [
+ SessionContext,
+ { provide: RoleAccessHelperService, useValue: roleAccessHelper }
+ ]
+ })
+ .compileComponents().then(() => {
+ fixture = TestBed.createComponent(ButtonsContainerComponent);
+ component = fixture.componentInstance;
+ component.hideDuplicateButtonForRole = false;
+ });
+ }));
+
+ beforeEach(fakeAsync(() => {
+ const roleAcess = ROLE_ACCESS;
+ roleAccessHelper.init(roleAcess);
+ fixture.detectChanges();
+ }));
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ //////// negative buttons - left side //////////
+ it('should react on quit button click', async(() => {
+ spyOn(component, 'onClickEvents').and.callThrough();
+ const button = fixture.debugElement.nativeElement.querySelector('button#quit');
+ button.click();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.onClickEvents).toHaveBeenCalledWith('quit');
+ });
+
+ }));
+
+ it('should react on reject button click', async(() => {
+ component.readOnlyForm = false;
+ spyOn(component, 'onClickEvents').and.callThrough();
+ const button = fixture.debugElement.nativeElement.querySelector('button#reject');
+ button.click();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.onClickEvents).toHaveBeenCalledWith('reject');
+ });
+
+ }));
+
+ // duplicate
+ it('should react on duplicate button click', async(() => {
+ component.readOnlyForm = false;
+ fixture.detectChanges();
+ spyOn(component, 'onClickEvents').and.callThrough();
+ const button = fixture.debugElement.nativeElement.querySelector('button#duplicate');
+ button.click();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.onClickEvents).toHaveBeenCalledWith('duplicate');
+ });
+
+ }));
+
+ //////// positive buttons - right side //////////
+ it('should react on save button click', async(() => {
+
+ component.validForSave = true;
+ component.validForm = true;
+ component.readOnlyForm = false;
+ fixture.detectChanges();
+ spyOn(component, 'onClickEvents').and.callThrough();
+ const button = fixture.debugElement.nativeElement.querySelector('button#save');
+ button.click();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.onClickEvents).toHaveBeenCalledWith('save');
+ });
+
+ }));
+
+ it('should react on apply button click', async(() => {
+ component.positiveStatusChange = 'apply';
+ component.validForm = true;
+ component.readOnlyForm = false;
+ fixture.detectChanges();
+ spyOn(component, 'onClickEvents').and.callThrough();
+ const button = fixture.debugElement.nativeElement.querySelector('button#statuschange');
+ button.click();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.onClickEvents).toHaveBeenCalledWith('apply');
+ });
+
+ }));
+
+ it('should react on forapproval button click', async(() => {
+ this.gridMeasureStatusId = '1';
+ component.positiveStatusChange = 'forapproval';
+ component.validForm = true;
+ component.readOnlyForm = false;
+ fixture.detectChanges();
+ spyOn(component, 'onClickEvents').and.callThrough();
+ const button = fixture.debugElement.nativeElement.querySelector('button#statuschange');
+ button.click();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.onClickEvents).toHaveBeenCalledWith('forapproval');
+ });
+
+ }));
+
+ it('should react on approve button click', async(() => {
+ this.gridMeasureStatusId = '3';
+ component.positiveStatusChange = 'approve';
+ component.validForm = true;
+ component.readOnlyForm = false;
+ fixture.detectChanges();
+ spyOn(component, 'onClickEvents').and.callThrough();
+ const button = fixture.debugElement.nativeElement.querySelector('button#statuschange');
+ button.click();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.onClickEvents).toHaveBeenCalledWith('approve');
+ });
+
+ }));
+
+
+
+ it('should react on request button click', async(() => {
+ this.gridMeasureStatusId = '4';
+ component.positiveStatusChange = 'request';
+ component.validForm = true;
+ component.readOnlyForm = false;
+ fixture.detectChanges();
+ spyOn(component, 'onClickEvents').and.callThrough();
+ const button = fixture.debugElement.nativeElement.querySelector('button#statuschange');
+ button.click();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.onClickEvents).toHaveBeenCalledWith('request');
+ });
+
+ }));
+
+ it('should react on release button click', async(() => {
+ this.gridMeasureStatusId = '5';
+ component.positiveStatusChange = 'release';
+ component.validForm = true;
+ component.readOnlyForm = false;
+ fixture.detectChanges();
+ spyOn(component, 'onClickEvents').and.callThrough();
+ const button = fixture.debugElement.nativeElement.querySelector('button#statuschange');
+ button.click();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.onClickEvents).toHaveBeenCalledWith('release');
+ });
+
+ }));
+
+ it('should react on activate button click', async(() => {
+ this.gridMeasureStatusId = '6';
+ component.positiveStatusChange = 'activate';
+ component.validForm = true;
+ component.readOnlyForm = false;
+ fixture.detectChanges();
+ spyOn(component, 'onClickEvents').and.callThrough();
+ const button = fixture.debugElement.nativeElement.querySelector('button#statuschange');
+ button.click();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.onClickEvents).toHaveBeenCalledWith('activate');
+ });
+
+ }));
+
+ it('should react on inwork button click', async(() => {
+ this.gridMeasureStatusId = '7';
+ component.positiveStatusChange = 'inwork';
+ component.validForm = true;
+ component.readOnlyForm = false;
+ fixture.detectChanges();
+ spyOn(component, 'onClickEvents').and.callThrough();
+ const button = fixture.debugElement.nativeElement.querySelector('button#statuschange');
+ button.click();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.onClickEvents).toHaveBeenCalledWith('inwork');
+ });
+
+ }));
+
+ it('should react on workfinish button click', async(() => {
+ this.gridMeasureStatusId = '8';
+ component.positiveStatusChange = 'workfinish';
+ component.validForm = true;
+ component.readOnlyForm = false;
+ fixture.detectChanges();
+ spyOn(component, 'onClickEvents').and.callThrough();
+ const button = fixture.debugElement.nativeElement.querySelector('button#statuschange');
+ button.click();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.onClickEvents).toHaveBeenCalledWith('workfinish');
+ });
+
+ }));
+
+ it('should react on finish button click', async(() => {
+ this.gridMeasureStatusId = '9';
+ component.positiveStatusChange = 'finish';
+ component.validForm = true;
+ component.readOnlyForm = false;
+ fixture.detectChanges();
+ spyOn(component, 'onClickEvents').and.callThrough();
+ const button = fixture.debugElement.nativeElement.querySelector('button#statuschange');
+ button.click();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.onClickEvents).toHaveBeenCalledWith('finish');
+ });
+
+ }));
+
+ it('should react on close button click', async(() => {
+ this.gridMeasureStatusId = '10';
+ component.positiveStatusChange = 'close';
+ component.validForm = true;
+ component.readOnlyForm = false;
+ fixture.detectChanges();
+ spyOn(component, 'onClickEvents').and.callThrough();
+ const button = fixture.debugElement.nativeElement.querySelector('button#statuschange');
+ button.click();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.onClickEvents).toHaveBeenCalledWith('close');
+ });
+
+ }));
+
+ it('should set form valid for save', () => {
+ component.validForSave = false;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ isValidForSave: new SimpleChange(component.validForSave, true, true)
+ });
+ fixture.detectChanges();
+ expect(component.validForSave).toBeTruthy();
+ });
+
+ it('should set form valid', () => {
+ component.validForm = false;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ isValidForm: new SimpleChange(component.validForm, true, true)
+ });
+ fixture.detectChanges();
+ expect(component.validForm).toBeTruthy();
+ });
+
+ it('should set form in readonly mode', () => {
+ component.readOnlyForm = false;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ isReadOnlyForm: new SimpleChange(component.readOnlyForm, true, true)
+ });
+ fixture.detectChanges();
+ expect(component.readOnlyForm).toBeTruthy();
+ });
+
+ it('should check all cases and deactivate all negative buttons - no buttons available in the list', async(() => {
+
+ component.gridMeasureStatusId = '20';
+ component.readOnlyForm = true;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ isReadOnlyForm: new SimpleChange(component.readOnlyForm, false, true)
+ });
+ fixture.detectChanges();
+ spyOn(component, 'handleNegativeButtonsForStatus').and.callThrough();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.hideRejectButton).toBeTruthy();
+ });
+ }));
+
+ it('should show only quit button - no other buttons available in the list', async(() => {
+
+ component.gridMeasureStatusId = undefined; // status = nothing
+ component.readOnlyForm = true;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ isReadOnlyForm: new SimpleChange(component.readOnlyForm, false, true)
+ });
+ fixture.detectChanges();
+ spyOn(component, 'getButtonsForStatus').and.callThrough();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.gmStatus).toBe(Globals.STATUS.NEW);
+ });
+ }));
+
+ it('should deactivate save button', async(() => {
+
+ component.gridMeasureStatusId = '20';
+ component.readOnlyForm = true;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ isReadOnlyForm: new SimpleChange(component.readOnlyForm, false, true)
+ });
+ fixture.detectChanges();
+ spyOn(component, 'handlePositiveButtonsForStatus').and.callThrough();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.hideSaveButton).toBeTruthy();
+ });
+ }));
+
+ it('should show for apply button', async(() => {
+
+ component.gridMeasureStatusId = '0';
+ component.readOnlyForm = true;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ isReadOnlyForm: new SimpleChange(component.readOnlyForm, false, true)
+ });
+ fixture.detectChanges();
+ spyOn(component, 'handlePositiveButtonsForStatus').and.callThrough();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.positiveStatusChange).toBe('apply');
+ expect(component.positiveBtnLabel).toBe(Globals.STATUS_BUTTON_LABEL['apply']);
+ });
+ }));
+
+ it('should show for approval button', async(() => {
+
+ component.gridMeasureStatusId = '1';
+ component.readOnlyForm = true;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ isReadOnlyForm: new SimpleChange(component.readOnlyForm, false, true)
+ });
+ fixture.detectChanges();
+ spyOn(component, 'handlePositiveButtonsForStatus').and.callThrough();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.positiveStatusChange).toBe('forapproval');
+ expect(component.positiveBtnLabel).toBe(Globals.STATUS_BUTTON_LABEL['forapproval']);
+ });
+ }));
+
+ it('should show for approve button', async(() => {
+
+ component.gridMeasureStatusId = '3';
+ component.readOnlyForm = true;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ isReadOnlyForm: new SimpleChange(component.readOnlyForm, false, true)
+ });
+ fixture.detectChanges();
+ spyOn(component, 'handlePositiveButtonsForStatus').and.callThrough();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.positiveStatusChange).toBe('approve');
+ expect(component.positiveBtnLabel).toBe(Globals.STATUS_BUTTON_LABEL['approve']);
+ });
+ }));
+
+ it('should show for request button', async(() => {
+
+ component.gridMeasureStatusId = '4';
+ component.readOnlyForm = true;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ isReadOnlyForm: new SimpleChange(component.readOnlyForm, false, true)
+ });
+ fixture.detectChanges();
+ spyOn(component, 'handlePositiveButtonsForStatus').and.callThrough();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.positiveStatusChange).toBe('request');
+ expect(component.positiveBtnLabel).toBe(Globals.STATUS_BUTTON_LABEL['request']);
+ });
+ }));
+
+ it('should show for release button', async(() => {
+
+ component.gridMeasureStatusId = '5';
+ component.readOnlyForm = true;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ isReadOnlyForm: new SimpleChange(component.readOnlyForm, false, true)
+ });
+ fixture.detectChanges();
+ spyOn(component, 'handlePositiveButtonsForStatus').and.callThrough();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.positiveStatusChange).toBe('release');
+ expect(component.positiveBtnLabel).toBe(Globals.STATUS_BUTTON_LABEL['release']);
+ });
+ }));
+
+ it('should show for activate button', async(() => {
+
+ component.gridMeasureStatusId = '6';
+ component.readOnlyForm = true;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ isReadOnlyForm: new SimpleChange(component.readOnlyForm, false, true)
+ });
+ fixture.detectChanges();
+ spyOn(component, 'handlePositiveButtonsForStatus').and.callThrough();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.positiveStatusChange).toBe('activate');
+ expect(component.positiveBtnLabel).toBe(Globals.STATUS_BUTTON_LABEL['activate']);
+ });
+ }));
+
+ it('should show for inwork button', async(() => {
+
+ component.gridMeasureStatusId = '7';
+ component.readOnlyForm = true;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ isReadOnlyForm: new SimpleChange(component.readOnlyForm, false, true)
+ });
+ fixture.detectChanges();
+ spyOn(component, 'handlePositiveButtonsForStatus').and.callThrough();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.positiveStatusChange).toBe('inwork');
+ expect(component.positiveBtnLabel).toBe(Globals.STATUS_BUTTON_LABEL['inwork']);
+ });
+ }));
+
+ it('should show for workfinish button', async(() => {
+
+ component.gridMeasureStatusId = '8';
+ component.readOnlyForm = true;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ isReadOnlyForm: new SimpleChange(component.readOnlyForm, false, true)
+ });
+ fixture.detectChanges();
+ spyOn(component, 'handlePositiveButtonsForStatus').and.callThrough();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.positiveStatusChange).toBe('workfinish');
+ expect(component.positiveBtnLabel).toBe(Globals.STATUS_BUTTON_LABEL['workfinish']);
+ });
+ }));
+
+ it('should show for finish button', async(() => {
+
+ component.gridMeasureStatusId = '9';
+ component.readOnlyForm = true;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ isReadOnlyForm: new SimpleChange(component.readOnlyForm, false, true)
+ });
+ fixture.detectChanges();
+ spyOn(component, 'handlePositiveButtonsForStatus').and.callThrough();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.positiveStatusChange).toBe('finish');
+ expect(component.positiveBtnLabel).toBe(Globals.STATUS_BUTTON_LABEL['finish']);
+ });
+ }));
+
+ it('should show for close button', async(() => {
+
+ component.gridMeasureStatusId = '10';
+ component.readOnlyForm = true;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ isReadOnlyForm: new SimpleChange(component.readOnlyForm, false, true)
+ });
+ fixture.detectChanges();
+ spyOn(component, 'handlePositiveButtonsForStatus').and.callThrough();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.positiveStatusChange).toBe('close');
+ expect(component.positiveBtnLabel).toBe(Globals.STATUS_BUTTON_LABEL['close']);
+ });
+ }));
+
+
+});
diff --git a/src/app/common-components/buttons-container/buttons-container.component.ts b/src/app/common-components/buttons-container/buttons-container.component.ts
new file mode 100644
index 0000000..c256d04
--- /dev/null
+++ b/src/app/common-components/buttons-container/buttons-container.component.ts
@@ -0,0 +1,177 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+******************************************************************************
+ */
+import { SessionContext } from './../../common/session-context';
+import { Controls } from './../../model/role-access';
+import { Component, Input, EventEmitter, Output, SimpleChanges, OnChanges } from '@angular/core';
+import { Globals } from '../../common/globals';
+import { RoleAccessHelperService } from '../../services/jobs/role-access-helper.service';
+
+@Component({
+ selector: 'app-buttons-container',
+ templateUrl: './buttons-container.component.html',
+ styleUrls: ['./buttons-container.component.css']
+})
+export class ButtonsContainerComponent implements OnChanges {
+ Globals = Globals;
+ @Output() clickSaveButton: EventEmitter<string> = new EventEmitter();
+ @Output() clickApplyButton: EventEmitter<string> = new EventEmitter();
+ @Output() clickQuitButton: EventEmitter<string> = new EventEmitter();
+ @Output() clickCancelButton: EventEmitter<string> = new EventEmitter();
+ @Output() clickForApprovalButton: EventEmitter<string> = new EventEmitter();
+ @Output() clickApprovedButton: EventEmitter<string> = new EventEmitter();
+ @Output() clickRejectButton: EventEmitter<string> = new EventEmitter();
+ @Output() clickReleaseButton: EventEmitter<string> = new EventEmitter();
+ @Output() clickActivateButton: EventEmitter<string> = new EventEmitter();
+ @Output() clickInWorkButton: EventEmitter<string> = new EventEmitter();
+ @Output() clickWorkFinishButton: EventEmitter<string> = new EventEmitter();
+ @Output() clickCloseButton: EventEmitter<string> = new EventEmitter();
+ @Output() clickRequestButton: EventEmitter<string> = new EventEmitter();
+ @Output() clickFinishButton: EventEmitter<string> = new EventEmitter();
+ @Output() clickDuplicateButton: EventEmitter<string> = new EventEmitter();
+
+ @Input() isValidForSave: boolean;
+ @Input() isValidForm: boolean;
+ @Input() isReadOnlyForm: boolean;
+ @Input() gridMeasureStatusId: string;
+ @Input() gridMeasureId: number;
+
+ validForSave: boolean;
+ validForm: boolean;
+ readOnlyForm: boolean;
+ gmStatus: number;
+ hideSaveButton = false;
+ hideDuplicateButtonForRole = true;
+ hideRejectButton = false;
+ positiveStatusChange: string;
+ noActivePositiveBtn = false;
+ positiveBtnLabel: string;
+ lisOfActiveButtons: string[] = [];
+ controls: Controls;
+
+ constructor(
+ public sessionContext: SessionContext,
+ public roleAccessHelper: RoleAccessHelperService
+ ) { }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes['isValidForSave']) {
+ this.validForSave = changes['isValidForSave'].currentValue;
+ }
+ if (changes['isValidForm']) {
+ this.validForm = changes['isValidForm'].currentValue;
+ }
+ if (changes['isReadOnlyForm']) {
+ this.readOnlyForm = changes['isReadOnlyForm'].currentValue;
+ this.gmStatus = Number(this.gridMeasureStatusId);
+ this.getButtonsForStatus();
+ }
+ }
+
+ getButtonsForStatus() {
+ if (isNaN(this.gmStatus)) {
+ this.gmStatus = Globals.STATUS.NEW;
+ }
+ this.controls = this.roleAccessHelper.getRoleAccessDefinitions().controls.
+ filter(statusId => statusId.gridMeasureStatusId === this.gmStatus)[0];
+ if (!this.readOnlyForm) {
+ this.lisOfActiveButtons = this.controls.activeButtons;
+ } else {
+ this.lisOfActiveButtons = [];
+ }
+ this.handleNegativeButtonsForStatus();
+ this.handlePositiveButtonsForStatus();
+ this.handleDuplicateButton();
+ }
+
+ handleNegativeButtonsForStatus() {
+ // negative buttons (left side)
+ this.hideRejectButton = !this.lisOfActiveButtons.includes(Globals.STATUS_TEXT.REJECT);
+ }
+
+ handlePositiveButtonsForStatus() {
+ this.hideSaveButton = !this.lisOfActiveButtons.includes(Globals.STATUS_TEXT.SAVE);
+
+ // positive buttons (right side)
+ const positiveButtonsForStatus = this.lisOfActiveButtons.filter(btn =>
+ btn !== Globals.STATUS_TEXT.SAVE && btn !== Globals.STATUS_TEXT.CANCEL && btn !== Globals.STATUS_TEXT.REJECT);
+
+ // if there are more than one positive buttons in JSON, the first one will be taken
+ const btnToShow = positiveButtonsForStatus[0];
+ if (btnToShow && positiveButtonsForStatus.length > 0) {
+ this.noActivePositiveBtn = false;
+ this.positiveStatusChange = btnToShow;
+ this.positiveBtnLabel = Globals.STATUS_BUTTON_LABEL[btnToShow];
+ } else {
+ this.noActivePositiveBtn = true;
+ }
+ }
+
+ handleDuplicateButton() {
+ if (this.gridMeasureId) {
+ const currRoles = this.sessionContext.getCurrUser().roles;
+ const allowedRolesForDupicate = this.roleAccessHelper.getRoleAccessDefinitions().duplicateSection.duplicateRoles;
+ allowedRolesForDupicate.forEach(allowedRole => {
+ if (currRoles.includes(allowedRole)) {
+ this.hideDuplicateButtonForRole = false;
+ return;
+ }
+ });
+ }
+ }
+
+ onClickEvents(action: string) {
+ switch (action) {
+ case Globals.STATUS_TEXT.QUIT:
+ this.clickQuitButton.emit();
+ break;
+ case Globals.STATUS_TEXT.REJECT:
+ this.clickRejectButton.emit();
+ break;
+ case Globals.STATUS_TEXT.SAVE:
+ this.clickSaveButton.emit();
+ break;
+ case Globals.STATUS_TEXT.APPLY:
+ this.clickApplyButton.emit();
+ break;
+ case Globals.STATUS_TEXT.FORAPPROVAL:
+ this.clickForApprovalButton.emit();
+ break;
+ case Globals.STATUS_TEXT.APPROVE:
+ this.clickApprovedButton.emit();
+ break;
+ case Globals.STATUS_TEXT.REQUEST:
+ this.clickRequestButton.emit();
+ break;
+ case Globals.STATUS_TEXT.RELEASE:
+ this.clickReleaseButton.emit();
+ break;
+ case Globals.STATUS_TEXT.ACTIVE:
+ this.clickActivateButton.emit();
+ break;
+ case Globals.STATUS_TEXT.IN_WORK:
+ this.clickInWorkButton.emit();
+ break;
+ case Globals.STATUS_TEXT.WORK_FINISH:
+ this.clickWorkFinishButton.emit();
+ break;
+ case Globals.STATUS_TEXT.FINISH:
+ this.clickFinishButton.emit();
+ break;
+ case Globals.STATUS_TEXT.CLOSE:
+ this.clickCloseButton.emit();
+ break;
+ case Globals.STATUS_TEXT.DUPLICATE:
+ this.clickDuplicateButton.emit();
+ break;
+ }
+ }
+
+}
diff --git a/src/app/common-components/loading-spinner/loading-spinner.component.css b/src/app/common-components/loading-spinner/loading-spinner.component.css
new file mode 100644
index 0000000..0a7f644
--- /dev/null
+++ b/src/app/common-components/loading-spinner/loading-spinner.component.css
@@ -0,0 +1,63 @@
+/**
+******************************************************************************
+* Copyright © 2017-2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+.spinner {
+ margin: 10px auto;
+ width: 50px;
+ height: 40px;
+ text-align: center;
+ font-size: 10px;
+ position: relative;
+ }
+
+ .spinner > div {
+ background-color: #409D6D;
+ height: 100%;
+ width: 6px;
+ display: inline-block;
+
+ -webkit-animation: sk-stretchdelay 1.2s infinite ease-in-out;
+ animation: sk-stretchdelay 1.2s infinite ease-in-out;
+ }
+
+ .spinner .rect2 {
+ background-color: #28928E;
+ -webkit-animation-delay: -1.1s;
+ animation-delay: -1.1s;
+ }
+
+ .spinner .rect3 {
+ background-color: #28928E;
+ -webkit-animation-delay: -1.0s;
+ animation-delay: -1.0s;
+ }
+
+ .spinner .rect4 {
+ background-color: #0B85B6;
+ -webkit-animation-delay: -0.9s;
+ animation-delay: -0.9s;
+ }
+
+ .spinner .rect5 {
+ background-color: #0281C4;
+ -webkit-animation-delay: -0.8s;
+ animation-delay: -0.8s;
+ }
+
+ @-webkit-keyframes sk-stretchdelay {
+ 0%, 40%, 100% { -webkit-transform: scaleY(0.4) }
+ 20% { -webkit-transform: scaleY(1.0) }
+ }
+
+ @keyframes sk-stretchdelay {
+ 0%, 40%, 100% { transform: scaleY(0.4); -webkit-transform: scaleY(0.4);}
+ 20% { transform: scaleY(1.0); -webkit-transform: scaleY(1.0);}
+ }
\ No newline at end of file
diff --git a/src/app/common-components/loading-spinner/loading-spinner.component.html b/src/app/common-components/loading-spinner/loading-spinner.component.html
new file mode 100644
index 0000000..da9aa27
--- /dev/null
+++ b/src/app/common-components/loading-spinner/loading-spinner.component.html
@@ -0,0 +1,19 @@
+<!--
+******************************************************************************
+* Copyright © 2017-2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+-->
+
+<div class="spinner">
+ <div class="rect1"></div>
+ <div class="rect2"></div>
+ <div class="rect3"></div>
+ <div class="rect4"></div>
+ <div class="rect5"></div>
+ </div>
\ No newline at end of file
diff --git a/src/app/common-components/loading-spinner/loading-spinner.component.spec.ts b/src/app/common-components/loading-spinner/loading-spinner.component.spec.ts
new file mode 100644
index 0000000..c80a5c4
--- /dev/null
+++ b/src/app/common-components/loading-spinner/loading-spinner.component.spec.ts
@@ -0,0 +1,11 @@
+/**
+******************************************************************************
+* Copyright � 2017-2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
diff --git a/src/app/common-components/loading-spinner/loading-spinner.component.ts b/src/app/common-components/loading-spinner/loading-spinner.component.ts
new file mode 100644
index 0000000..535b62f
--- /dev/null
+++ b/src/app/common-components/loading-spinner/loading-spinner.component.ts
@@ -0,0 +1,27 @@
+/**
+******************************************************************************
+* Copyright © 2017-2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Component, OnInit, Input } from '@angular/core';
+
+@Component({
+ selector: 'app-loading-spinner',
+ templateUrl: './loading-spinner.component.html',
+ styleUrls: ['./loading-spinner.component.css']
+})
+export class LoadingSpinnerComponent implements OnInit {
+
+
+ constructor() { }
+
+ ngOnInit() {
+ }
+
+}
diff --git a/src/app/common-components/main-navigation/main-navigation.component.css b/src/app/common-components/main-navigation/main-navigation.component.css
new file mode 100644
index 0000000..7f3837a
--- /dev/null
+++ b/src/app/common-components/main-navigation/main-navigation.component.css
@@ -0,0 +1,86 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+.navbar-nav{
+ display: inline-flex;
+}
+
+.overview-navbar {
+ z-index: 999;
+}
+
+.nav-overview-user {
+ float: right;
+ vertical-align: middle;
+ padding-top: 13px;
+ padding-right: 20px;
+}
+
+.divider-right {
+ border-right: 1px solid #ccc;
+ margin-right: 25px;
+}
+
+.active-navitem {
+ border-bottom: 2px solid;
+ padding-bottom: 3px;
+ width: auto;
+}
+
+.glyphicon:hover {
+ border-bottom: 2px solid;
+ padding-bottom: 3px;
+ width: auto;
+}
+
+#bell {
+ font-size: 130%;
+}
+
+#bell:hover {
+ border-bottom: 0px solid !important;
+ padding-bottom: 3px;
+ width: auto;
+ cursor: default;
+}
+
+.dropdown-open:hover {
+ background-color: transparent;
+}
+
+.navbar-default .navbar-nav > .open > a,
+.navbar-default .navbar-nav > .open > a:focus {
+ filter: brightness(95%);
+}
+
+.dropdown-menu>li>a {
+ cursor: pointer;
+}
+
+.dropdown-menu>li>div {
+ margin-left: 20px;
+ color: #333;
+}
+
+.btn-logout {
+ background-color: #ccdbe6;
+ color: #0080c0;
+ border: none;
+}
+
+.btn-logout:hover {
+ filter: brightness(95%);
+}
+
+.version-info-simple {
+ position: inherit;
+
+}
diff --git a/src/app/common-components/main-navigation/main-navigation.component.html b/src/app/common-components/main-navigation/main-navigation.component.html
new file mode 100644
index 0000000..b416bb6
--- /dev/null
+++ b/src/app/common-components/main-navigation/main-navigation.component.html
@@ -0,0 +1,49 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+-->
+
+<nav class="navbar navbar-default navbar-fixed-top masthead overview-navbar" role="banner">
+ <div class="container-fluid">
+ <div class="navbar-header">
+ <a href="#/overview" class="navbar-brand">
+ <span class="open">Open</span>
+ <span class="konsequenz">KONSEQUENZ</span>
+ </a>
+ </div>
+ <div class="nav navbar-nav navbar-right">
+
+ <div *ngIf="router.url === '/overview' || router.url === '/reminders'" style="border-right: solid 1px #c3c3c3; margin: 10px 0 10px 0">
+ </div>
+ <div>
+ <button style="float: right" type="button" class="btn btn-link navbar-btn" (click)="goToOverview()">
+ <span class="glyphicon glyphicon-home active-navitem-hover" [ngClass]="{'active-navitem': router.url === '/overview'}"></span>
+ </button>
+ <span style="float: right; cursor: default" type="text" class="btn btn-link navbar-btn">
+ <span class="glyphicon glyphicon-bell" id="bell" [title]="sessionContext.getUpcomingReminder()? 'Es gibt mindestens einen fälligen Maßnahmeantrag' : ''"
+ [style.color]="sessionContext.setBellColor()">
+ </span>
+ </span>
+ </div>
+ <div class="dropdown-open nav-user nav-overview-user" style="padding-top: 10px">
+ <a class="btn btn-logout dropdown-toggle" style="min-width: 160px;" type="button" data-toggle="dropdown" href="#" aria-expanded="true">
+ {{sessionContext.getAccessTokenDecoded()?.name}}
+ <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu" style="left: auto; right: auto; top: 40px;">
+ <li class="dropdown">
+ <a (click)="goToLogout()">Abmelden</a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ <app-version-info class="version-info-simple"></app-version-info>
+</nav>
\ No newline at end of file
diff --git a/src/app/common-components/main-navigation/main-navigation.component.spec.ts b/src/app/common-components/main-navigation/main-navigation.component.spec.ts
new file mode 100644
index 0000000..957f86a
--- /dev/null
+++ b/src/app/common-components/main-navigation/main-navigation.component.spec.ts
@@ -0,0 +1,88 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { NgModule } from '@angular/core';
+import { MockComponent } from '../../testing/mock.component';
+import { RouterTestingModule } from '@angular/router/testing';
+import { Router } from '../../testing/router-stubs';
+import { SessionContext } from '../../common/session-context';
+import { MainNavigationComponent } from './main-navigation.component';
+import { VersionInfoComponent } from '../../common-components/version-info/version-info.component';
+
+class FakeRouter {
+ navigate(commands: any[]) {
+ return commands[0];
+ }
+}
+
+@NgModule({
+ declarations: [],
+ entryComponents: [
+ VersionInfoComponent
+ // LogoutComponent
+ ]
+})
+class TestModule { }
+
+describe('MainNavigationComponent', () => {
+ let component: MainNavigationComponent;
+ let fixture: ComponentFixture<MainNavigationComponent>;
+ let routerStub: FakeRouter;
+ let sessionContext: SessionContext;
+
+ routerStub = {
+ navigate: jasmine.createSpy('navigate').and.callThrough()
+ };
+
+
+ beforeEach(async(() => {
+ sessionContext = new SessionContext();
+ TestBed.configureTestingModule({
+ imports: [
+ RouterTestingModule.withRoutes([]),
+ ],
+ declarations: [
+ MainNavigationComponent,
+ MockComponent({ selector: 'input', inputs: ['options'] }),
+ MockComponent({ selector: 'app-version-info' })
+ ],
+ providers: [
+ { provide: Router, useValue: routerStub },
+ { provide: SessionContext, useValue: sessionContext }
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MainNavigationComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should navigate to overview on home-button click', () => {
+ component.goToOverview();
+ expect(routerStub.navigate).toHaveBeenCalledWith(['/overview']);
+ });
+
+ it('should navigate to mainview on logout', () => {
+ spyOn(sessionContext, 'clearStorage').and.callThrough();
+ component.logout();
+ expect(sessionContext.clearStorage).toHaveBeenCalled();
+ });
+
+ it('should navigate to Logout on logout click', () => {
+ component.goToLogout();
+ expect(routerStub.navigate).toHaveBeenCalledWith(['/logout']);
+ });
+
+
+});
diff --git a/src/app/common-components/main-navigation/main-navigation.component.ts b/src/app/common-components/main-navigation/main-navigation.component.ts
new file mode 100644
index 0000000..9709a6a
--- /dev/null
+++ b/src/app/common-components/main-navigation/main-navigation.component.ts
@@ -0,0 +1,44 @@
+/**
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Component, OnInit } from '@angular/core';
+import { SessionContext } from '../../common/session-context';
+import { Router } from '@angular/router';
+
+@Component({
+ selector: 'app-main-navigation',
+ templateUrl: './main-navigation.component.html',
+ styleUrls: ['./main-navigation.component.css']
+})
+export class MainNavigationComponent implements OnInit {
+
+ constructor(
+ public sessionContext: SessionContext,
+
+ public router: Router
+ ) { }
+
+ ngOnInit() {
+ }
+
+ logout() {
+ this.sessionContext.clearStorage();
+ }
+
+ goToOverview() {
+ this.router.navigate(['/overview']);
+ }
+
+ goToLogout() {
+ this.router.navigate(['/logout']);
+ }
+
+}
diff --git a/src/app/common-components/message-banner/message-banner.component.css b/src/app/common-components/message-banner/message-banner.component.css
new file mode 100644
index 0000000..1952fd3
--- /dev/null
+++ b/src/app/common-components/message-banner/message-banner.component.css
@@ -0,0 +1,26 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+#banner {
+ margin-top: 37px;
+ position: fixed;
+ width: 100%;
+ z-index: 900;
+}
+
+.actions {
+ float: right;
+}
+
+.actions .btn {
+ margin-right: 15px;
+}
\ No newline at end of file
diff --git a/src/app/common-components/message-banner/message-banner.component.html b/src/app/common-components/message-banner/message-banner.component.html
new file mode 100644
index 0000000..e18fbc6
--- /dev/null
+++ b/src/app/common-components/message-banner/message-banner.component.html
@@ -0,0 +1,25 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+-->
+
+<!-- <div id="banner" *ngIf="zOrder===getMaxMessageBannerZOrder() && sessionContext.bannerMessage.isActive" [ngClass]="{'alert':true,
+ 'alert-danger':sessionContext.bannerMessage.status===bannerMessageStatus.error,
+ 'alert-info':sessionContext.bannerMessage.status===bannerMessageStatus.info,
+ 'alert-warning':sessionContext.bannerMessage.status===bannerMessageStatus.warning,
+ 'alert-success':sessionContext.bannerMessage.status===bannerMessageStatus.success}">
+ <div class="close" (click)="sessionContext.bannerMessage.hide()">x</div>
+ {{sessionContext.bannerMessage.text}}
+ <div class="actions">
+ <button *ngIf="sessionContext.bannerMessage.showButton" class="btn btn-primary" (click)="onButtonClick(1)">Sperrung aufheben</button>
+ <button *ngIf="sessionContext.bannerMessage.showDecisionButtons" class="btn btn-primary" (click)="onButtonClick(2)">Ja</button>
+ <button *ngIf="sessionContext.bannerMessage.showDecisionButtons" class="btn btn-primary" (click)="onButtonClick(3)">Nein</button>
+ </div>
+</div> -->
\ No newline at end of file
diff --git a/src/app/common-components/message-banner/message-banner.component.spec.ts b/src/app/common-components/message-banner/message-banner.component.spec.ts
new file mode 100644
index 0000000..c15fb75
--- /dev/null
+++ b/src/app/common-components/message-banner/message-banner.component.spec.ts
@@ -0,0 +1,96 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { SessionContext } from '../../common/session-context';
+import { By } from '@angular/platform-browser';
+import { DebugElement } from '@angular/core';
+import { MessageBannerComponent } from './message-banner.component';
+import { BannerMessage } from '../../common/banner-message';
+import { BannerMessageStatusEn, ErrorType, MessageScopeEn, ToasterButtonEventEn } from '../../common/enums';
+import { MessageServiceCustom } from '../../services/message.service';
+
+describe('MessageBannerComponent', () => {
+ let component: MessageBannerComponent;
+ let fixture: ComponentFixture<MessageBannerComponent>;
+ let sessionContext: SessionContext;
+ let messageService: MessageServiceCustom;
+
+ beforeEach(async(() => {
+ sessionContext = new SessionContext();
+ messageService = new MessageServiceCustom(sessionContext);
+
+ TestBed.configureTestingModule({
+ declarations: [MessageBannerComponent],
+ providers: [
+ { provide: SessionContext, useValue: sessionContext },
+ { provide: MessageServiceCustom, useValue: messageService }
+ ],
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MessageBannerComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+
+ // it('should count the zOrder correctly and toggle the visibility due to zOrder', () => {
+ // spyOn(component, 'onClearLocal').and.callThrough();
+ // spyOn(component, 'onButtonClick').and.callThrough();
+
+ // const fixture2 = TestBed.createComponent(MessageBannerComponent);
+ // const component2: any = fixture2.componentInstance;
+ // const bannerMessage = new BannerMessage();
+ // bannerMessage.isSetTimeout = false;
+ // bannerMessage.isActive = true;
+ // sessionContext.setBannerMessage(bannerMessage);
+ // const x1: any = component;
+ // x1.ngOnInit();
+
+ // expect(component.getMaxMessageBannerZOrder()).toBe(0);
+ // fixture.detectChanges();
+
+ // // Component 1 is the only one and has the topmost zOrder=>Visible
+ // expect(fixture.debugElement.query(By.css('.close'))).not.toBeNull();
+
+ // component2.zOrder = 666;
+ // component2.ngOnInit();
+
+ // expect(component.getMaxMessageBannerZOrder()).toBe(666);
+ // fixture.detectChanges();
+ // fixture2.detectChanges();
+
+ // // Component 2 has a bigger zOrder => Component1=invisible, Component2=visible
+ // expect(fixture.debugElement.query(By.css('.close'))).toBeNull();
+ // expect(fixture2.debugElement.query(By.css('.close'))).not.toBeNull();
+
+ // sessionContext.bannerMessage.scope = MessageScopeEn.local;
+
+ // expect(bannerMessage.isActive).toBe(true);
+ // messageService.clearBannerLocalEvent$.emit();
+
+ // expect(component.onClearLocal).toHaveBeenCalled();
+ // expect(bannerMessage.isActive).toBe(false);
+
+ // bannerMessage.showButton = true;
+ // bannerMessage.isActive = true;
+
+ // messageService.bannerButtonEvent$.emit(ToasterButtonEventEn.hideGridMeasureButton);
+ // expect(bannerMessage.isActive).toBe(false);
+
+ // });
+});
diff --git a/src/app/common-components/message-banner/message-banner.component.ts b/src/app/common-components/message-banner/message-banner.component.ts
new file mode 100644
index 0000000..c861dd8
--- /dev/null
+++ b/src/app/common-components/message-banner/message-banner.component.ts
@@ -0,0 +1,75 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Component, OnInit, OnDestroy, Input } from '@angular/core';
+import { BannerMessageStatusEn, ToasterButtonEventEn, MessageScopeEn } from '../../common/enums';
+import { SessionContext } from '../../common/session-context';
+
+import { MessageServiceCustom } from './../../services/message.service';
+@Component({
+ selector: 'app-message-banner',
+ templateUrl: './message-banner.component.html',
+ styleUrls: ['./message-banner.component.css']
+})
+
+
+export class MessageBannerComponent implements OnInit, OnDestroy {
+ static allComponents: Map<MessageBannerComponent, number> = new Map<MessageBannerComponent, number>();
+ @Input() zOrder = 0;
+
+ bannerMessageStatus = BannerMessageStatusEn;
+
+ constructor(public sessionContext: SessionContext, public messageService: MessageServiceCustom) {
+ }
+
+ ngOnInit() {
+ MessageBannerComponent.allComponents.set(this, this.zOrder);
+ // this.messageService.bannerButtonEvent$.subscribe(bannerButtonEventEn => {
+ // if (this.sessionContext.bannerMessage.showButton) {
+ // this.sessionContext.bannerMessage.isActive = bannerButtonEventEn !== ToasterButtonEventEn.hideGridMeasureButton;
+ // }
+ // });
+
+ // this.messageService.clearBannerLocalEvent$.subscribe(clearLocalEvent => this.onClearLocal(false));
+ this.messageService.clearDecisionBannerLocalEvent$.subscribe(clearLocalEvent => this.onClearLocal(true));
+
+ }
+
+ ngOnDestroy() {
+ MessageBannerComponent.allComponents.delete(this);
+ }
+
+ // onButtonClick(bannerButtonEventEn: ToasterButtonEventEn): void {
+ // this.messageService.bannerButtonEvent$.emit(bannerButtonEventEn);
+ // this.sessionContext.bannerMessage.hide();
+ // }
+
+ onClearLocal(onlyDecisionBanners: boolean): void {
+ if (this.sessionContext.bannerMessage && this.sessionContext.bannerMessage.scope === MessageScopeEn.local
+ && (!onlyDecisionBanners || this.sessionContext.bannerMessage.showDecisionButtons)) {
+ this.sessionContext.bannerMessage.hide();
+ }
+ }
+ getMaxMessageBannerZOrder(): number {
+ const iter = MessageBannerComponent.allComponents.values();
+ let maxValue = 0;
+ let item = iter.next();
+ while (item != null && !item.done) {
+ if (item.value > maxValue) {
+ maxValue = item.value;
+ }
+ item = iter.next();
+ }
+
+ return maxValue;
+ }
+
+}
diff --git a/src/app/common-components/pipes/formatted-date.pipe.spec.ts b/src/app/common-components/pipes/formatted-date.pipe.spec.ts
new file mode 100644
index 0000000..911a007
--- /dev/null
+++ b/src/app/common-components/pipes/formatted-date.pipe.spec.ts
@@ -0,0 +1,29 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+import { TestBed } from '@angular/core/testing';
+import { FormattedDatePipe } from './formatted-date.pipe';
+
+describe('Test FormattedDatePipe ', () => {
+ const pipe = new FormattedDatePipe();
+
+ it('should perform correct with different inputs ', () => {
+ expect(pipe.transform('xxx')).toBe('xxx');
+ expect(pipe.transform(null)).toBe(null);
+ expect(pipe.transform('')).toBe('');
+ expect(pipe.transform(undefined)).toBeNull();
+
+ const dateTest = pipe.transform('19.11.2017 13:16');
+ expect(dateTest.match('[0-3][0-9]\.[0-1][0-9]\.[0-9]{4}')).toBeTruthy();
+
+ });
+});
diff --git a/src/app/common-components/pipes/formatted-date.pipe.ts b/src/app/common-components/pipes/formatted-date.pipe.ts
new file mode 100644
index 0000000..4c4cc22
--- /dev/null
+++ b/src/app/common-components/pipes/formatted-date.pipe.ts
@@ -0,0 +1,47 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import {Pipe, PipeTransform} from '@angular/core';
+import * as moment from 'moment';
+/**
+ * Pipe to transform a string to a date
+ */
+@Pipe({name: 'formattedDate'})
+export class FormattedDatePipe implements PipeTransform {
+ /**
+ * Constructor
+ */
+ constructor() {
+ }
+ /**
+ * Transform a date that is passed as string into a date
+ * @param value The date passed as string
+ * @param format The date passed as string
+ */
+ transform(value: any, format: string = ''): string {
+ // Try and parse the passed value.
+ if ( value === undefined) {
+ return null;
+ }
+ const momentDate = moment(value, 'DD.MM.YYYY');
+
+ // If moment didn't understand the value, return it unformatted.
+ if (!momentDate.isValid()) {
+ return value;
+ }
+
+ // Otherwise, return the date formatted as requested.
+ if (format) {
+ return momentDate.format(format);
+ }
+ return momentDate.format('DD.MM.YYYY');
+ }
+}
diff --git a/src/app/common-components/pipes/formatted-timestamp.pipe.spec.ts b/src/app/common-components/pipes/formatted-timestamp.pipe.spec.ts
new file mode 100644
index 0000000..4e28e72
--- /dev/null
+++ b/src/app/common-components/pipes/formatted-timestamp.pipe.spec.ts
@@ -0,0 +1,30 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+import { FormattedTimestampPipe } from './formatted-timestamp.pipe';
+
+describe('Test FormattedTimestampPipe ', () => {
+ const pipe = new FormattedTimestampPipe();
+
+ it('should perform correct with different inputs ', () => {
+ expect(pipe.transform('xxx')).toBeNull();
+ expect(pipe.transform(null)).toBe('');
+ expect(pipe.transform('')).toBe('');
+ expect(pipe.transform(undefined)).toBe('');
+
+ expect(pipe.transform('19.11.2017 13:16', 'DD.MM.YYYY HH:mm')).toBe('19.11.2017 13:16');
+
+ const dateTest = pipe.transform('19.11.2017 13:16');
+ expect(dateTest.match('[0-3][0-9]\.[0-1][0-9]\.[0-9]{4} [0-2][0-9]:[0-5][0-9]')).toBeTruthy();
+
+ });
+});
diff --git a/src/app/common-components/pipes/formatted-timestamp.pipe.ts b/src/app/common-components/pipes/formatted-timestamp.pipe.ts
new file mode 100644
index 0000000..140bde5
--- /dev/null
+++ b/src/app/common-components/pipes/formatted-timestamp.pipe.ts
@@ -0,0 +1,51 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Pipe, PipeTransform } from '@angular/core';
+import * as moment from 'moment';
+
+@Pipe({
+ name: 'formattedTimestamp'
+})
+export class FormattedTimestampPipe implements PipeTransform {
+ transform(value: any, format: string = ''): string {
+
+ if (value === '') {
+ return '';
+ }
+
+ if (value === 'xxx') {
+ return null;
+ }
+
+ if (value === null) {
+ return '';
+ }
+
+ if (value === undefined) {
+ return '';
+ }
+
+ // Try and parse the passed value.
+ const momentDate = moment(value);
+
+ // If moment didn't understand the value, return it unformatted.
+ if (!momentDate.isValid()) {
+ return value;
+ }
+
+ // Otherwise, return the date formatted as requested.
+ if (format) {
+ return momentDate.format(format);
+ }
+ return momentDate.format('DD.MM.YYYY HH:mm');
+ }
+}
diff --git a/src/app/common-components/pipes/string-to-date.pipe.spec.ts b/src/app/common-components/pipes/string-to-date.pipe.spec.ts
new file mode 100644
index 0000000..4e61b9b
--- /dev/null
+++ b/src/app/common-components/pipes/string-to-date.pipe.spec.ts
@@ -0,0 +1,32 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+import { TestBed } from '@angular/core/testing';
+import { StringToDatePipe } from './string-to-date.pipe';
+
+describe('Test StringToDatePipe ', () => {
+ const pipe = new StringToDatePipe();
+
+ it('should perform correct with different inputs ', () => {
+ expect( pipe.transform('xxx') ).toBeNull();
+ expect( pipe.transform( null) ).toBeNull();
+ expect( pipe.transform( '' ) ).toBeNull();
+ expect( pipe.transform( undefined ) ).toBeNull();
+
+ const dateTest = pipe.transform( '2017-11-19T13:16:06');
+ expect( dateTest.getFullYear() ).toBe( 2017 );
+ expect( dateTest.getMonth() ).toBe( 10 ); // month in jscript is zero based!
+ expect( dateTest.getDate() ).toBe( 19 );
+ expect( dateTest.getMinutes() ).toBe( 16 );
+ expect( dateTest.getSeconds() ).toBe( 6 );
+ });
+});
diff --git a/src/app/common-components/pipes/string-to-date.pipe.ts b/src/app/common-components/pipes/string-to-date.pipe.ts
new file mode 100644
index 0000000..d138011
--- /dev/null
+++ b/src/app/common-components/pipes/string-to-date.pipe.ts
@@ -0,0 +1,40 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Pipe, PipeTransform } from '@angular/core';
+import * as moment from 'moment';
+/**
+ * Pipe to transform a string to a date
+ */
+@Pipe({ name: 'stringToDate' })
+export class StringToDatePipe implements PipeTransform {
+ /**
+ * Constructor
+ */
+ constructor() {
+ }
+ /**
+ * Transform a date that is passed as string into a date
+ * @param value The date passed as string
+ * @returns {Date} The Date object
+ */
+ transform(value: string, format: string = ''): Date {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const ret = new Date(value);
+ if (ret + '' === 'Invalid Date') {
+ return null;
+ } else {
+ return ret;
+ }
+ }
+}
diff --git a/src/app/common-components/pipes/unique.pipe.spec.ts b/src/app/common-components/pipes/unique.pipe.spec.ts
new file mode 100644
index 0000000..371c1e5
--- /dev/null
+++ b/src/app/common-components/pipes/unique.pipe.spec.ts
@@ -0,0 +1,56 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { UniquePipe } from './unique.pipe';
+
+describe('Pipe: Uniquee', () => {
+
+ let uniquePipe: UniquePipe;
+
+ // synchronous beforeEach
+ beforeEach(() => {
+ uniquePipe = new UniquePipe();
+ });
+
+ it('should be instanciated', () => {
+ expect(uniquePipe).toBeDefined();
+ });
+
+ it('should return empty array if no items given', () => {
+
+ const items = [];
+
+ const filtered = uniquePipe.transform(items, 'filedNameText');
+
+ expect(filtered.length).toBe(0);
+ expect(filtered).toEqual([]);
+ });
+
+ it('should return empty array if no items given (2)', () => {
+
+ const items = [];
+ items.push({ branch: 'W', title: 'its a test title', statusId: '3', createUser: 'otto' });
+
+ const filtered = uniquePipe.transform(items, '');
+
+ expect(filtered.length).toBe(0);
+ });
+
+ it('should return empty array if items are undefined', () => {
+
+ const items = undefined;
+ const filtered = uniquePipe.transform(items, 'filedNameText');
+
+ expect(filtered.length).toBe(0);
+ expect(filtered).toEqual([]);
+ });
+
+});
diff --git a/src/app/common-components/pipes/unique.pipe.ts b/src/app/common-components/pipes/unique.pipe.ts
new file mode 100644
index 0000000..e8c134b
--- /dev/null
+++ b/src/app/common-components/pipes/unique.pipe.ts
@@ -0,0 +1,38 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'unique'
+})
+export class UniquePipe implements PipeTransform {
+
+ transform(items: any[], fieldName: string): any[] {
+ const cArray = [];
+ if (!items) {
+ return [];
+ }
+ return items.filter((item, index, arr) => {
+ const itemExists = cArray.find(ia => {
+ return ia === item[fieldName];
+ }
+ );
+ if ((item[fieldName] || item[fieldName] === 0) && !itemExists && itemExists !== 0) {
+ cArray.push(item[fieldName]);
+ return true;
+ } else {
+ return false;
+ }
+ });
+ }
+}
diff --git a/src/app/common-components/toaster/toaster.component.css b/src/app/common-components/toaster/toaster.component.css
new file mode 100644
index 0000000..7bf7f3a
--- /dev/null
+++ b/src/app/common-components/toaster/toaster.component.css
@@ -0,0 +1,32 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+#deleteSGMYesDiv {
+ padding-left: 15%;
+}
+
+#deleteSGYNoDiv {
+ padding-left: 10%;
+}
+
+#deleteSGMYesButton,
+#deleteSGMNoButton {
+ width: 80px;
+}
+
+#unlockToaster {
+ padding-left: 23%;
+}
+
+#unlockButton {
+ width: 180px;
+}
\ No newline at end of file
diff --git a/src/app/common-components/toaster/toaster.component.html b/src/app/common-components/toaster/toaster.component.html
new file mode 100644
index 0000000..b37ee9a
--- /dev/null
+++ b/src/app/common-components/toaster/toaster.component.html
@@ -0,0 +1,52 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+-->
+
+<p-toast [style]="{marginTop: '80px'}"></p-toast>
+
+<p-toast [style]="{marginTop: '80px'}" position="top-left" key="tl"></p-toast>
+
+<p-toast [style]="{marginTop: '80px'}" position="top-center" key="tc"></p-toast>
+
+<p-toast [style]="{marginTop: '80px'}" styleClass="custom-toast" key="custom" position="bottom-center"></p-toast>
+
+<p-toast position="center" key="deletec" (onClose)="onSingleGMReject()" [modal]="true" [baseZIndex]="5000">
+ <ng-template let-message pTemplate="message">
+ <div style="text-align: center">
+ <i class="pi pi-exclamation-triangle" style="font-size: 3em"></i>
+ <h3>{{message.summary}}</h3>
+ <p>{{message.detail}}</p>
+ </div>
+ <div class="ui-g ui-fluid">
+ <div class="ui-g-6" id="deleteSGMYesDiv">
+ <button type="button" id="deleteSGMYesButton" (click)="onSingleGMDeleteConfirm()" class="btn btn-success">Ja</button>
+ </div>
+ <div class="ui-g-6" id="deleteSGYNoDiv">
+ <button type="button" id="deleteSGMNoButton" (click)="onSingleGMReject()" class="btn btn-default">Nein</button>
+ </div>
+ </div>
+ </ng-template>
+</p-toast>
+
+<p-toast position="center" key="unlockc" (onClose)="onUnlcokReject()" [modal]="true" [baseZIndex]="5000">
+ <ng-template let-message pTemplate="message">
+ <div style="text-align: center">
+ <i class="pi pi-exclamation-triangle" style="font-size: 3em"></i>
+ <h3>{{message.summary}}</h3>
+ <p>{{message.detail}}</p>
+ </div>
+ <div class="ui-g ui-fluid">
+ <div class="ui-g-6" id="unlockToaster">
+ <button type="button" id="unlockButton" (click)="onUnlockConfirm()" class="btn btn-success">Sperrung aufheben</button>
+ </div>
+ </div>
+ </ng-template>
+</p-toast>
\ No newline at end of file
diff --git a/src/app/common-components/toaster/toaster.component.spec.ts b/src/app/common-components/toaster/toaster.component.spec.ts
new file mode 100644
index 0000000..d156061
--- /dev/null
+++ b/src/app/common-components/toaster/toaster.component.spec.ts
@@ -0,0 +1,104 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { ToasterComponent } from './toaster.component';
+import { MessageService } from 'primeng/api';
+import { MockComponent } from '../../testing/mock.component';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+import { SessionContext } from '../../common/session-context';
+import { ToasterButtonEventEn } from '../../common/enums';
+
+describe('ToasterComponent', () => {
+ let component: ToasterComponent;
+ let fixture: ComponentFixture<ToasterComponent>;
+ let messageService: MessageService;
+ let toasterMessageService: ToasterMessageService;
+ let sessionStorage: SessionContext;
+
+ beforeEach(async(() => {
+ sessionStorage = new SessionContext;
+ messageService = new MessageService;
+ toasterMessageService = new ToasterMessageService(sessionStorage, messageService);
+ TestBed.configureTestingModule({
+ declarations: [ToasterComponent,
+ MockComponent({ selector: 'p-toast', inputs: ['modal', 'baseZIndex'] })],
+ providers: [
+ { provide: ToasterMessageService, useValue: toasterMessageService }
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ToasterComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should emit delete single grid measure after click on yes button', () => {
+ spyOn(component, 'onSingleGMDeleteConfirm').and.callThrough();
+
+ component.onSingleGMDeleteConfirm();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect((component as any).toasterMessageService.clear).toHaveBeenCalledWith('deletec');
+ expect((component as any).toasterMessageService.toasterEmitter$.emit).
+ toHaveBeenCalledWith(ToasterButtonEventEn.deleteSingleGridMeasure);
+ });
+ });
+
+ it('should clear toaster if question rejected', () => {
+ spyOn(component, 'onSingleGMReject').and.callThrough();
+
+ component.onSingleGMReject();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect((component as any).toasterMessageService.clear).toHaveBeenCalledWith('deletec');
+ });
+ });
+
+ it('should emit unlock grid measure after click on button', () => {
+ spyOn(component, 'onUnlockConfirm').and.callThrough();
+
+ component.onUnlockConfirm();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect((component as any).toasterMessageService.clear).toHaveBeenCalledWith('unlockc');
+ expect((component as any).toasterMessageService.toasterEmitter$.emit).
+ toHaveBeenCalledWith(ToasterButtonEventEn.unlockGridMeasure);
+ });
+ });
+
+ it('should clear toaster if question rejected', () => {
+ spyOn(component, 'onUnlcokReject').and.callThrough();
+
+ component.onUnlcokReject();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect((component as any).toasterMessageService.clear).toHaveBeenCalledWith('unlockc');
+ });
+ });
+
+ it('should clear toaster', () => {
+ spyOn(component, 'clear').and.callThrough();
+
+ component.clear();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect((component as any).toasterMessageService.clear).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/app/common-components/toaster/toaster.component.ts b/src/app/common-components/toaster/toaster.component.ts
new file mode 100644
index 0000000..f706627
--- /dev/null
+++ b/src/app/common-components/toaster/toaster.component.ts
@@ -0,0 +1,56 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Component, NgModule } from '@angular/core';
+import { ToasterButtonEventEn } from '../../common/enums';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+import { ToastModule } from 'primeng/toast';
+
+@Component({
+ selector: 'app-toaster',
+ templateUrl: './toaster.component.html',
+ styleUrls: ['./toaster.component.css']
+})
+
+@NgModule({
+ exports: [ToastModule],
+ declarations: [ToastModule],
+})
+
+export class ToasterComponent {
+
+ constructor(public messageService: ToasterMessageService) { }
+
+
+ onSingleGMDeleteConfirm() {
+ this.messageService.toasterEmitter$.emit(ToasterButtonEventEn.deleteSingleGridMeasure);
+ this.messageService.clear('deletec');
+ }
+
+ onSingleGMReject() {
+ this.messageService.clear('deletec');
+ }
+
+ onUnlockConfirm() {
+ this.messageService.toasterEmitter$.emit(ToasterButtonEventEn.unlockGridMeasure);
+ this.messageService.clear('unlockc');
+
+ }
+
+ onUnlcokReject() {
+ this.messageService.clear('unlockc');
+ }
+
+ clear() {
+ this.messageService.clear();
+ }
+
+}
diff --git a/src/app/common-components/version-info/version-info.component.css b/src/app/common-components/version-info/version-info.component.css
new file mode 100644
index 0000000..4d83e67
--- /dev/null
+++ b/src/app/common-components/version-info/version-info.component.css
@@ -0,0 +1,18 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+.version-info {
+ position: fixed;
+ bottom: 0;
+ right: 0;
+ font-size: x-small;
+}
+
diff --git a/src/app/common-components/version-info/version-info.component.html b/src/app/common-components/version-info/version-info.component.html
new file mode 100644
index 0000000..e3ea9ab
--- /dev/null
+++ b/src/app/common-components/version-info/version-info.component.html
@@ -0,0 +1,15 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+-->
+
+<div class="version-info">
+ Version: {{currVersion.frontendVersion}} / {{currVersion.backendVersion}} / {{currVersion.dbVersion}}
+</div>
diff --git a/src/app/common-components/version-info/version-info.component.spec.ts b/src/app/common-components/version-info/version-info.component.spec.ts
new file mode 100644
index 0000000..cdf35cc
--- /dev/null
+++ b/src/app/common-components/version-info/version-info.component.spec.ts
@@ -0,0 +1,75 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { DebugElement } from '@angular/core';
+
+import { AbstractMockObservableService } from '../../testing/abstract-mock-observable.service';
+import { VersionInfoComponent } from './version-info.component';
+import { VersionInfo } from '../../model/version-info';
+import { VersionInfoService } from '../../services/version-info.service';
+import { Globals } from '../../common/globals';
+
+
+describe('VersionInfoComponent', () => {
+ let component: VersionInfoComponent;
+ let fixture: ComponentFixture<VersionInfoComponent>;
+ let de: DebugElement; // the DebugElement with the welcome message
+ let el: HTMLElement; // the DOM element with the welcome message
+ class MockService extends AbstractMockObservableService {
+ loadBackendServerInfo() {
+ return this;
+ }
+ }
+ let mockService;
+
+ beforeEach(async(() => {
+ mockService = new MockService();
+
+ TestBed.configureTestingModule({
+ declarations: [VersionInfoComponent],
+ providers: [{ provide: VersionInfoService, useValue: mockService }],
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(VersionInfoComponent);
+ component = fixture.componentInstance;
+
+ });
+
+ it('should show VersionString after loadBackendServerInfo', () => {
+ const vinfo = { frontendVersion: 'AVersion', backendVersion: '1.0', dbVersion: '2.0' };
+ mockService.content = vinfo;
+ de = fixture.debugElement.query(By.css('.version-info'));
+ el = de.nativeElement;
+
+ fixture.detectChanges();
+
+ const targetString = Globals.FRONTEND_VERSION + ' / ' + vinfo.backendVersion + ' / ' + vinfo.dbVersion;
+ expect(el.textContent).toContain(targetString);
+ });
+
+ it('should show ??? while not init', () => {
+ mockService.error = 'any Error';
+ de = fixture.debugElement.query(By.css('.version-info'));
+ el = de.nativeElement;
+
+ fixture.detectChanges();
+
+ const targetString = '? / ? / ?';
+ expect(el.textContent).toContain(targetString);
+ });
+
+
+});
diff --git a/src/app/common-components/version-info/version-info.component.ts b/src/app/common-components/version-info/version-info.component.ts
new file mode 100644
index 0000000..2f1464f
--- /dev/null
+++ b/src/app/common-components/version-info/version-info.component.ts
@@ -0,0 +1,41 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Component, OnInit } from '@angular/core';
+import { VersionInfo } from '../../model/version-info';
+import { VersionInfoService } from '../../services/version-info.service';
+import { Globals } from '../../common/globals';
+
+@Component({
+ selector: 'app-version-info',
+ templateUrl: './version-info.component.html',
+ styleUrls: ['./version-info.component.css'],
+})
+export class VersionInfoComponent implements OnInit {
+ currVersion: VersionInfo = {
+ frontendVersion: '?',
+ backendVersion: '?',
+ dbVersion: '?'
+ };
+
+ constructor(private _btbService: VersionInfoService) { }
+
+ ngOnInit() {
+ this._btbService.loadBackendServerInfo().subscribe(vinfo => this.setVersionInfo(vinfo),
+ error => console.log(error));
+ }
+
+ private setVersionInfo(vinfo: VersionInfo) {
+ this.currVersion = vinfo;
+ this.currVersion.frontendVersion = Globals.FRONTEND_VERSION;
+ }
+
+}
diff --git a/src/app/common/banner-message.ts b/src/app/common/banner-message.ts
new file mode 100644
index 0000000..8cf8d8b
--- /dev/null
+++ b/src/app/common/banner-message.ts
@@ -0,0 +1,26 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Component, Optional, OnInit } from '@angular/core';
+import { ErrorType, MessageScopeEn } from '../common/enums';
+export class BannerMessage {
+ status: number;
+ text: string;
+ isActive = false;
+ showButton = false;
+ showDecisionButtons = false;
+ isSetTimeout = true;
+ errorType: ErrorType;
+ scope?: MessageScopeEn;
+ public hide() {
+ this.isActive = false;
+ }
+}
diff --git a/src/app/common/enums.ts b/src/app/common/enums.ts
new file mode 100644
index 0000000..6fe9bee
--- /dev/null
+++ b/src/app/common/enums.ts
@@ -0,0 +1,47 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+export enum StatusEn {
+ open = 1,
+ inWork = 2,
+ done = 3,
+ closed = 4
+}
+
+export enum ErrorType {
+ create = 1,
+ update = 2,
+ delete = 3,
+ retrieve = 4,
+ authentication = 5,
+ upload = 6,
+ locked = 7,
+ datedependency = 8,
+ stornoLocked = 9
+}
+export enum BannerMessageStatusEn {
+ warning = 1,
+ success = 2,
+ error = 3,
+ info = 4
+}
+
+export enum ToasterButtonEventEn {
+
+ unlockGridMeasure = 1,
+ deleteSingleGridMeasure = 2
+}
+
+export enum MessageScopeEn {
+ local = 0,
+ global = 1
+}
+
diff --git a/src/app/common/globals.ts b/src/app/common/globals.ts
new file mode 100644
index 0000000..d781c0f
--- /dev/null
+++ b/src/app/common/globals.ts
@@ -0,0 +1,154 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+export class Globals {
+ static ROLE_ACCESS = 'ROLE_ACCESS';
+ static FRONTEND_VERSION = '0.0.1_SNAPSHOT';
+ static SESSION_TOKEN_TAG = 'X-XSRF-TOKEN';
+ static BACKEND_SETTINGS = 'BACKEND_SETTINGS';
+ static CURRENT_USER = 'CURRENT_USER';
+ static ALL_USERS = 'ALL_USERS';
+ static ALL_USER_DEPARTMENTS = 'ALL_USER_DEPARTMENTS';
+ static BASE_URL = '/mics-home-service/rest/mics/home';
+ static BRANCHESNAME = 'BRANCHES';
+ static BRANCHLEVELSNAME = 'BRANCHLEVELS';
+ static RESPONSIBILITIESNAME = 'RESPONSIBILITIES';
+ static NETWORKCONTROLSNAME = 'NETWORKCONTROLS';
+ static TERRITORY = 'TERRITORY';
+ static USER_SETTINGS = 'USER_SETTINGS';
+ static FILTERING_SEARCH_TEXT = 'FILTERING_SEARCH_TEXT';
+ static COLUMN_STATE = 'COLUMN_STATE';
+ static STATUS_MAIN_FILTER = 'STATUS_MAIN_FILTER';
+ static DIRTY_STATE_FILTER = 'DIRTY_STATE_FILTER';
+ static COLLAPSE_STATE = 'COLLAPSE_STATE';
+ static GRID_MEASURE_COLLAPSABLE = 'GRID_MEASURE_COLLAPSABLE';
+ static GRID_MEASURE = 'GRID_MEASURE';
+ static READ_ONLY_FORM = 'READ_ONLY_FORM';
+ static CANCEL_STAGE = 'CANCEL_STAGE';
+ static GRID_MEASURE_MODE = 'GRID_MEASURE_MODE';
+ static GRID_MEASURES_SERVICE_NAME = 'planned-grid-measures.openK';
+ static CIM_CACHE_SERVICE = 'cim-cache';
+ static LOCK_SERVICE_NAME = 'planned-grid-measures.lock.openK';
+ static STATUSES = 'STATUSES';
+ static OVERDUE_REMINDERS = 'OVERDUE_REMINDERS';
+ static UPCOMING_REMINDERS = 'UPCOMING_REMINDERS';
+ static CURRENT_REMINDERS = 'CURRENT_REMINDERS';
+ static EXPIRED_REMINDERS = 'EXPIRED_REMINDERS';
+ static COSTCENTERS = 'COSTCENTERS';
+ static EMAILADDRESSES_FROM_TEMPLATE = 'EMAILADDRESSES_FROM_TEMPLATE';
+ static EMAILADDRESSES_FROM_GREIDMEASURE = 'EMAILADDRESSES_FROM_GREIDMEASURE';
+ static TAB_FILTERING_TEXT = 'TAB_FILTERING_TEXT';
+ static GRIDMEASURE_LOCK_TAG = 'gridmeasure';
+ static FORCE_UNLOCK = 'FORCE';
+ static NO_FORCE_UNLOCK = 'NO_FORCE';
+ static APPOINTMENT_NUMBER_OF_MAX_VALUE = 999;
+ static MAX_NUMBER_OF_TABS = 10;
+
+ static AUTH_AND_AUTH_SERVICE_NAME = 'authNauth.openK';
+
+ static OAUTH2CONF_SUPERUSER_ROLE = 'planned-policies-superuser';
+ static OAUTH2CONF_MEASUREPLANNER_ROLE = 'planned-policies-measureplanner';
+ static OAUTH2CONF_MEASUREAPPLICANT_ROLE = 'planned-policies-measureapplicant';
+
+ static ACCESS_TOKEN = 'ACCESS_TOKEN';
+ static TIME_TO_HIDE_MESSAGE_BANNER = 20 * 1000;
+ static MEGABYT_UNIT = 1000 * 1000;
+ static MAX_UPLOADFILE_SIZE = 20;
+ static BRANCHES = class Branches {
+ static power = '1';
+ static gas = '2';
+ static heating = '3';
+ static water = '4';
+
+ };
+
+ static MODE = class Mode {
+ static EDIT = 'edit';
+ static VIEW = 'view';
+ static CANCEL = 'cancel';
+ };
+
+ static STATUS = class Status {
+ static NEW = 0;
+ static APPLIED = 1;
+ static CANCELED = 2;
+ static FORAPPROVAL = 3;
+ static APPROVED = 4;
+ static REQUESTED = 5;
+ static RELEASED = 6;
+ static ACTIVE = 7;
+ static IN_WORK = 8;
+ static WORK_FINISHED = 9;
+ static FINISHED = 10;
+ static CLOSED = 11;
+ static REJECTED = 12;
+ };
+
+ static STATUS_TEXT = class StatusText {
+ static CANCEL = 'cancel';
+ static QUIT = 'quit';
+ static REJECT = 'reject';
+ static SAVE = 'save';
+ static APPLY = 'apply';
+ static FORAPPROVAL = 'forapproval';
+ static APPROVE = 'approve';
+ static REQUEST = 'request';
+ static RELEASE = 'release';
+ static ACTIVE = 'activate';
+ static IN_WORK = 'inwork';
+ static WORK_FINISH = 'workfinish';
+ static FINISH = 'finish';
+ static CLOSE = 'close';
+ static DUPLICATE = 'duplicate';
+ };
+
+ static STATUS_BUTTON_LABEL = {
+ 'cancel': 'Stornieren',
+ 'quit': 'Abbrechen',
+ 'reject': 'Zurückweisen',
+ 'save': 'Speichern',
+ 'apply': 'Beantragen',
+ 'forapproval': 'Zur Genehmigung',
+ 'approve': 'Genehmigen',
+ 'request': 'Anfordern',
+ 'release': 'Freigeben',
+ 'activate': 'Schalten aktiv',
+ 'inwork': 'In Arbeit',
+ 'workfinish': 'Arbeit beenden',
+ 'finish': 'Maßnahme beenden',
+ 'close': 'Maßnahme schließen',
+ 'duplicate': 'Netzmaßnahme duplizieren'
+ };
+
+ static LOCALSTORAGE_SESSION_ID = '/elogbook/session-id';
+ static SORTING_STATE = 'SORTING_STATE';
+
+ static TYPE_WHITELIST = ['application/pdf',
+ 'application/vnd.ms-excel',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'application/vnd.ms-excel.sheet.macroEnabled.12',
+ 'application/msword',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'application/vnd.ms-word.document.macroEnabled.12',
+ 'image/png',
+ 'image/jpeg'
+ ];
+
+ static REMINDER_TIME = 48;
+ static ONE_HOUR = 1000 * 60 * 60;
+ static REMINDER_JOB_POLLING_INTERVALL = 60000;
+ static REMINDER_JOB_POLLING_START_DELAY = 2000;
+
+ static DATEPICKER_HEIGHT = 360;
+
+ static TEMP_ID_TO_SHOW_NEW_STEPS = -1;
+ static TEMP_ID_TO_SHOW_NEW_EMAILDISTRIBUTIONENTRYS = -1;
+}
diff --git a/src/app/common/list-helper-tool.spec.ts b/src/app/common/list-helper-tool.spec.ts
new file mode 100644
index 0000000..04b2c77
--- /dev/null
+++ b/src/app/common/list-helper-tool.spec.ts
@@ -0,0 +1,31 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { TestBed, async, inject } from '@angular/core/testing';
+import { ListHelperTool } from './list-helper-tool';
+
+describe('ListHelperTool', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ });
+
+ });
+ it('should detect today correctly', () => {
+ const tool = new ListHelperTool();
+ expect( tool.checkIfToday(null)).toBeFalsy();
+ const today = new Date();
+ today.setHours( 12, 13, 14, 59 );
+ expect( tool.checkIfToday(today.toISOString()) ).toBeTruthy();
+
+ today.setHours( 25, 1, 1, 1 );
+ expect( tool.checkIfToday(today.toISOString()) ).toBeFalsy();
+ });
+});
diff --git a/src/app/common/list-helper-tool.ts b/src/app/common/list-helper-tool.ts
new file mode 100644
index 0000000..3484a6c
--- /dev/null
+++ b/src/app/common/list-helper-tool.ts
@@ -0,0 +1,26 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+export class ListHelperTool {
+
+public checkIfToday( compareDateString: string ): boolean {
+ const now = new Date();
+ const compareDate: Date = new Date( Date.parse(compareDateString) );
+
+ if ( !compareDate ) {
+ return false;
+ }
+
+ return compareDate.getFullYear() === now.getFullYear() &&
+ compareDate.getMonth() === now.getMonth() &&
+ compareDate.getDate() === now.getDate();
+ }
+}
diff --git a/src/app/common/session-context.spec.ts b/src/app/common/session-context.spec.ts
new file mode 100644
index 0000000..594c309
--- /dev/null
+++ b/src/app/common/session-context.spec.ts
@@ -0,0 +1,284 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { TestBed, async, inject } from '@angular/core/testing';
+import { SessionContext } from './session-context';
+import { User } from '../model/user';
+import { USERS } from '../test-data/users';
+import { UserMap } from './user-map';
+import { Branch } from '../model/branch';
+import { UserDepartment } from '../model/user-department';
+import { CostCenter } from '../model/cost-center';
+import { GRIDMEASURE } from '../test-data/grid-measures';
+import { GridMeasure } from '../model/grid-measure';
+import { StatusMainFilter } from '../model/status-main-filter';
+import { UserSettings, SettingType, SettingValue } from '../model/user-settings';
+import { Territory } from '../model/territory';
+
+
+describe('SessionContext', () => {
+ const gridmeasures: GridMeasure[] = JSON.parse(JSON.stringify(GRIDMEASURE));
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [SessionContext]
+ });
+ });
+
+ it('can instatiate service when inject service', inject([SessionContext], (service: SessionContext) => {
+ expect(service instanceof SessionContext);
+ }));
+
+ it('can store a SessionId and a User', inject([SessionContext], (service: SessionContext) => {
+ service.setCurrSessionId('SpecialSessionId');
+ const usr = new User();
+ usr.id = '44';
+ usr.name = 'Rudi';
+ service.setCurrUser(usr);
+ expect(service.getCurrSessionId()).toBe('SpecialSessionId');
+ expect(service.getCurrUser()).not.toBeNull();
+ expect(service.getCurrUser().id).toBe('44');
+ expect(service.getCurrUser().name).toBe('Rudi');
+ }));
+
+ it('create a usermapper should fail when empty', inject([SessionContext], (service: SessionContext) => {
+ let errorOccured = false;
+ try {
+ const um = service.getUserMap();
+ } catch (ea) {
+ errorOccured = true;
+ }
+
+ expect(errorOccured).toBe(true);
+ }));
+
+ it('create a usermapper should work with all users set', inject([SessionContext], (service: SessionContext) => {
+ let errorOccured = false;
+ let um: UserMap;
+ service.setAllUsers(USERS);
+ try {
+ um = service.getUserMap();
+ } catch (ea) {
+ errorOccured = true;
+ }
+
+ expect(errorOccured).toBe(false);
+ expect(um.findUser('otto').username).toBe('otto');
+ }));
+
+ it('should get and set the collapse state correctly', inject([SessionContext], (sessionContext: SessionContext) => {
+ sessionContext.setCollapseState(true, 'Bruno');
+ expect(sessionContext.getCollapseState('Bruno')).toBeTruthy();
+
+ sessionContext.setCollapseState(false, 'Balduin');
+ expect(sessionContext.getCollapseState('Balduin')).toBeFalsy();
+
+ }));
+
+ it('should return the correct statusById', inject([SessionContext], (sessionContext: SessionContext) => {
+ sessionContext.setStatuses([{ id: 1, name: 'offen' }, { id: 2, name: 'in Bearbeitung' }, { id: 3, name: 'beendet' }]);
+ expect(sessionContext.getStatusById(666)).toBeNull();
+ expect(sessionContext.getStatusById(3).id).toBe(3);
+
+ sessionContext.setStatuses(null);
+ expect(sessionContext.getStatusById(45).name).toBe('NOSTATUS');
+ }));
+
+ it('should return the correct branchClassByName', inject([SessionContext], (sessionContext: SessionContext) => {
+ const branches: Branch[] = [
+ { 'id': 1, 'name': 'S', 'description': 'Strom', 'colorCode': '' },
+ { 'id': 2, 'name': 'G', 'description': 'Gas', 'colorCode': '' },
+ { 'id': 4, 'name': 'W', 'description': 'Wasser', 'colorCode': '' },
+ { 'id': 3, 'name': 'F', 'description': 'Fernwärme', 'colorCode': '' }
+ ];
+ sessionContext.setBranches(branches);
+ expect(sessionContext.getBranchById(1).name).toBe('S');
+ expect(sessionContext.getBranchById(666)).toBeNull();
+ expect(sessionContext.getBranchById(undefined)).toBeNull();
+
+ sessionContext.setBranches(null);
+ expect(sessionContext.getBranchById(45).name).toBe('NOBRANCHES');
+ }));
+
+ it('should get and set territory', inject([SessionContext], (sessionContext: SessionContext) => {
+ const territories: any = [];
+ sessionContext.setTerritories(territories);
+
+ expect(sessionContext.getTerritories()).toBeDefined();
+ }));
+
+ it('should get and set sorting state', inject([SessionContext], (sessionContext: SessionContext) => {
+ const sortState: any = {};
+ sessionContext.setSortingState(JSON.stringify(sortState));
+
+ expect(sessionContext.getSortingState()).toBeDefined();
+ }));
+
+ it('should get and set filtering search text', inject([SessionContext], (sessionContext: SessionContext) => {
+ const filterText = 'text';
+ sessionContext.setFilteringSearchText(JSON.stringify(filterText));
+
+ expect(sessionContext.getFilteringSearchText()).toBeDefined();
+ expect(sessionContext.getFilteringSearchText()).toBe('text');
+ }));
+
+ it('should get and set column state', inject([SessionContext], (sessionContext: SessionContext) => {
+ const sortState: any = {};
+ sessionContext.setColumnState(JSON.stringify(sortState));
+
+ expect(sessionContext.getColumnState()).toBeDefined();
+ }));
+
+ it('should get and set status main filter state', inject([SessionContext], (sessionContext: SessionContext) => {
+ const filterState = new StatusMainFilter();
+ filterState.item.isCanceledStatusActive = true;
+ filterState.item.isClosedStatusActive = true;
+
+ sessionContext.setStatusMainFilter(filterState);
+
+ expect(sessionContext.getStatusMainFilter()).toBeDefined();
+ expect(sessionContext.getStatusMainFilter().item.isCanceledStatusActive).toBe(true);
+ expect(sessionContext.getStatusMainFilter().item.isClosedStatusActive).toBe(true);
+ }));
+
+ it('should get and set filters dirty state', inject([SessionContext], (sessionContext: SessionContext) => {
+ const dirtyState = true;
+
+ sessionContext.setFilterDirtyState(dirtyState);
+
+ expect(sessionContext.getFilterDirtyState()).toBeTruthy();
+ }));
+
+ it('should get and set user settings', inject([SessionContext], (sessionContext: SessionContext) => {
+ const usersettings = new UserSettings();
+ usersettings.settingType = new SettingType();
+ usersettings.username = 'otto';
+ usersettings.value = new SettingValue();
+
+ sessionContext.setUserSettings(JSON.stringify(usersettings));
+
+ expect(sessionContext.getUserSettings()).toBeDefined();
+ }));
+
+ it('should get and set tabFilterState', inject([SessionContext], (sessionContext: SessionContext) => {
+ const filterState = {
+ branchId: '3',
+ title: 'Bruno',
+ statusId: '5'
+
+ };
+ sessionContext.setTabFilteringState(filterState);
+
+ expect(sessionContext.getTabFilteringState().title).toBe('Bruno');
+ }));
+
+
+ it('should get and set FilterExpansionState', inject([SessionContext], (sessionContext: SessionContext) => {
+ sessionContext.setFilterExpansionState(true);
+ expect(sessionContext.getFilterExpansionState()).toBeTruthy();
+
+ sessionContext.setFilterExpansionState(false);
+ expect(sessionContext.getFilterExpansionState()).toBeFalsy();
+ }));
+
+
+ it('should get and set AllUserDepartments', inject([SessionContext], (sessionContext: SessionContext) => {
+ const depts: UserDepartment[] = [{ id: 666, name: 'Claudio' }];
+ sessionContext.setAllUserDepartments(depts);
+
+ expect(sessionContext.getAllUserDepartments()[0].id).toBe(666);
+ }));
+
+
+ it('should get and set CostCenters correctly', inject([SessionContext], (sessionContext: SessionContext) => {
+ const cc: CostCenter[] = [{ id: 666, name: 'Claudio' }, { id: 4711, name: 'Bertil' }];
+ sessionContext.setCostCenters(null);
+ expect(sessionContext.getCostCenterById(333).name).toBe('NOCOSTCENTER');
+ sessionContext.setCostCenters(cc);
+ expect(sessionContext.getCostCenters().length).toBe(2);
+ expect(sessionContext.getCostCenterById(4711).name).toBe('Bertil');
+ expect(sessionContext.getCostCenterById(333)).toBeNull();
+
+ }));
+
+ it('should get and set EmailAddresses correctly', inject([SessionContext], (sessionContext: SessionContext) => {
+ const emailAddresses: string[] = ['testmail@test.de', 'testmail2@test.de'];
+ sessionContext.setEmailAddressesFromTemplates(null);
+ expect(sessionContext.getEmailAddressesFromTemplates()).toBe(null);
+ sessionContext.setEmailAddressesFromTemplates(emailAddresses);
+ expect(sessionContext.getEmailAddressesFromTemplates().length).toBe(2);
+ expect(sessionContext.getEmailAddressesFromTemplates()[0]).toBe('testmail@test.de');
+
+ }));
+
+ it('should return the correct info isShorttermNotification', inject([SessionContext], (sessionContext: SessionContext) => {
+
+ sessionContext.setCurrentReminders([1111, 2222]);
+ expect(sessionContext.getCurrentReminders().length).toBe(2);
+ expect(sessionContext.isShorttermNotification(666)).toBeFalsy();
+ expect(sessionContext.isShorttermNotification(2222)).toBeTruthy();
+
+ sessionContext.setCurrentReminders(null);
+ expect(sessionContext.getCurrentReminders().length).toBe(0);
+ expect(sessionContext.isShorttermNotification(2222)).toBeFalsy();
+ }));
+
+ it('should get and set isReminder', inject([SessionContext], (sessionContext: SessionContext) => {
+
+ sessionContext.setUpcomingReminder(false);
+ expect(sessionContext.getUpcomingReminder()).toBe(false);
+ sessionContext.setUpcomingReminder(true);
+ expect(sessionContext.getUpcomingReminder()).toBe(true);
+ }));
+
+ it('should get and set AccessToken', inject([SessionContext], (sessionContext: SessionContext) => {
+ const accessToken = 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJodVl0e' +
+ 'VByUEVLQ1phY3FfMW5sOGZscENETnFHdmZEZHctYUxGQXNoWHZVIn0.eyJqdGkiOiI4ZmY5NTlhZC' +
+ '02ODQ1LTRlOGEtYjRiYi02ODQ0YjAwMjU0ZjgiLCJleHAiOjE1MDY2MDA0NTAsIm5iZiI6MCwiaWF' +
+ '0IjoxNTA2NjAwMTUwLCJpc3MiOiJodHRwOi8vZW50amF2YTAwMjo4MDgwL2F1dGgvcmVhbG1zL2Vs' +
+ 'b2dib29rIiwiYXVkIjoiZWxvZ2Jvb2stYmFja2VuZCIsInN1YiI6IjM1OWVmOWM5LTc3ZGYtNGEzZ' +
+ 'C1hOWM5LWY5NmQ4MzdkMmQ1NyIsInR5cCI6IkJlYXJlciIsImF6cCI6ImVsb2dib29rLWJhY2tlbm' +
+ 'QiLCJhdXRoX3RpbWUiOjAsInNlc3Npb25fc3RhdGUiOiI5NjVmNzM1MS0yZThiLTQ1MjgtOWYzZC1' +
+ 'lZTYyODNhOTViMTYiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNj' +
+ 'ZXNzIjp7InJvbGVzIjpbImVsb2dib29rLXN1cGVydXNlciIsImVsb2dib29rLW5vcm1hbHVzZXIiL' +
+ 'CJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7InJlYWxtLW1hbmFnZW1lbn' +
+ 'QiOnsicm9sZXMiOlsidmlldy11c2VycyIsInF1ZXJ5LWdyb3VwcyIsInF1ZXJ5LXVzZXJzIl19LCJ' +
+ 'hY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3Mi' +
+ 'LCJ2aWV3LXByb2ZpbGUiXX19LCJuYW1lIjoiQWRtaW5pc3RyYXRvciBBZG1pbmlzdHJhdG93aWNoI' +
+ 'iwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWRtaW4iLCJnaXZlbl9uYW1lIjoiQWRtaW5pc3RyYXRvci' +
+ 'IsImZhbWlseV9uYW1lIjoiQWRtaW5pc3RyYXRvd2ljaCIsImVtYWlsIjoic2VyZ2VqLmtlcm5AcHRh' +
+ 'LmRlIiwicm9sZXN0ZXN0IjoiW2Vsb2dib29rLXN1cGVydXNlciwgZWxvZ2Jvb2stbm9ybWFsdXNlc' +
+ 'iwgdW1hX2F1dGhvcml6YXRpb24sIG9mZmxpbmVfYWNjZXNzLCB1bWFfYXV0aG9yaXphdGlvbiwgZW' +
+ 'xvZ2Jvb2stbm9ybWFsdXNlcl0ifQ.o94Bl43oqyLNzZRABvIq9z-XI8JQjqj2FSDdUUEZGZPTN4uw' +
+ 'D5fyi0sONbDxmTFvgWPh_8ZhX6tlDGiupVDBY4eRH43Eettm-t4CDauL7FzB3w3dDPFMB5DhP4rrp' +
+ 'k_kATwnY2NKLRbequnh8Z6wLXjcmQNLgrgknXB_gogWAqH29dqKexwceMNIbq-kjaeLsmHSXM9TE9' +
+ 'q7_Ln9el04OlkpOVspVguedfINcNFg0DmYLJWyD2ORkOHLmYigN6YnyB9P2NFOnKGlLuQ87GjosI0' +
+ '0zBniRGi3PhE9NGd51Qggdbcsm0aM8GiMaZ7SO5i8iQWL10TRFRFyTEfy6hSO8g';
+ sessionContext.setAccessToken(accessToken);
+ expect(sessionContext.getAccessTokenDecoded().name).toBe('Administrator Administratowich');
+
+ }));
+
+ it('should get and set the grid measure correctly', inject([SessionContext], (sessionContext: SessionContext) => {
+ const gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ sessionContext.setGridMeasureDetail(gridMeasureDetail);
+ expect(sessionContext.getGridMeasureDetail().title).toBe('T1');
+ expect(sessionContext.getGridMeasureDetail().remark).toBe('TESTREMARK1');
+ }));
+
+ it('should get and set the cancel page stage', inject([SessionContext], (sessionContext: SessionContext) => {
+ sessionContext.setCancelStage(true);
+ expect(sessionContext.isInCancelPage()).toBeTruthy();
+
+ sessionContext.setCancelStage(false);
+ expect(sessionContext.isInCancelPage()).toBeFalsy();
+ }));
+
+});
diff --git a/src/app/common/session-context.ts b/src/app/common/session-context.ts
new file mode 100644
index 0000000..6a9b862
--- /dev/null
+++ b/src/app/common/session-context.ts
@@ -0,0 +1,430 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Branch } from './../model/branch';
+import { Injectable, EventEmitter } from '@angular/core';
+import { User } from '../model/user';
+import { Status } from '../model/status';
+import { BannerMessage } from '../common/banner-message';
+import { Globals } from '../common/globals';
+import { UserMap } from '../common/user-map';
+import { JwtPayload } from '../model/jwt-payload';
+import { JwtHelperService } from '@auth0/angular-jwt';
+import { UserDepartment } from '../model/user-department';
+import { CostCenter } from '../model/cost-center';
+import { BackendSettings } from './../model/backend-settings';
+import { GridMeasure } from '../model/grid-measure';
+import { UserSettings } from '../model/user-settings';
+import { StatusMainFilter } from '../model/status-main-filter';
+import { Territory } from '../model/territory';
+
+@Injectable()
+export class SessionContext {
+
+ private timeoutId: any;
+ public centralHttpResultCode$: EventEmitter<number> = new EventEmitter<number>();
+ public settings;
+ public reminderAvailable = false;
+ public importFileAvailable = false;
+ public filterExpanded = false;
+ public userMap: UserMap = null;
+ public userAuthenticated: boolean;
+ public inactiveFields: Array<string> = [];
+ public collapseState: boolean;
+ bannerMessage: BannerMessage = new BannerMessage();
+ getCurrSessionId(): string { return localStorage.getItem(Globals.LOCALSTORAGE_SESSION_ID); }
+ setCurrSessionId(sid: string): void { localStorage.setItem(Globals.LOCALSTORAGE_SESSION_ID, sid); }
+
+ initBannerMessage() {
+ this.bannerMessage = new BannerMessage();
+ }
+
+ clearStorage() {
+ this.initBannerMessage();
+ localStorage.clear();
+ }
+
+ getCollapseState(type: string): boolean {
+ const colapseState = localStorage.getItem(Globals.COLLAPSE_STATE + type);
+ if (!colapseState) {
+ return false;
+ }
+ return JSON.parse(colapseState);
+ }
+
+ setCollapseState(colapseState: boolean, type: string): void {
+ localStorage.setItem(Globals.COLLAPSE_STATE + type, JSON.stringify(colapseState));
+ }
+
+ setGridMeasureDetail(gm: GridMeasure): void {
+ localStorage.setItem(Globals.GRID_MEASURE, JSON.stringify(gm));
+ }
+
+ getGridMeasureDetail(): GridMeasure {
+ const gm = localStorage.getItem(Globals.GRID_MEASURE);
+ return JSON.parse(gm);
+ }
+
+ setCancelStage(flag: boolean): void {
+ localStorage.setItem(Globals.CANCEL_STAGE, JSON.stringify(flag));
+ }
+
+ isInCancelPage(): boolean {
+ const flag = localStorage.getItem(Globals.CANCEL_STAGE);
+ return JSON.parse(flag);
+ }
+
+ getBackendsettings(): BackendSettings {
+ return this.getSaveFromLocalStorage(Globals.BACKEND_SETTINGS);
+ }
+
+ setBackendsettings(settings: any): void {
+ localStorage.setItem(Globals.BACKEND_SETTINGS, JSON.stringify(settings));
+ }
+
+ getCurrUser(): User {
+ return this.getSaveFromLocalStorage(Globals.CURRENT_USER);
+ }
+
+ setCurrUser(usr: any): void {
+ localStorage.setItem(Globals.CURRENT_USER, JSON.stringify(usr));
+ }
+
+ getAllUsers(): User[] {
+ return this.getSaveFromLocalStorage(Globals.ALL_USERS);
+ }
+
+ setAllUsers(allUsr: User[]) {
+ localStorage.setItem(Globals.ALL_USERS, JSON.stringify(allUsr));
+ }
+
+ setTabFilteringState(filterSearchText: any): void {
+ localStorage.setItem(Globals.TAB_FILTERING_TEXT, JSON.stringify(filterSearchText));
+ }
+
+ getTabFilteringState(): any {
+ const filterSearchText = localStorage.getItem(Globals.TAB_FILTERING_TEXT);
+ return JSON.parse(filterSearchText);
+ }
+
+ getUserMap(): UserMap {
+ if (this.userMap == null) {
+ this.userMap = new UserMap(this.getAllUsers());
+ }
+ return this.userMap;
+ }
+
+ getSaveFromLocalStorage(key: string): any {
+ const retValue = localStorage.getItem(key);
+ if (!retValue) {
+ console.log('WARNING: Try to access LocalStorage key [' + key + '] which is empty!');
+ return null;
+ }
+ return JSON.parse(retValue);
+ }
+
+ setBannerMessage(bannerMessage: BannerMessage): void {
+ this.bannerMessage = bannerMessage;
+
+ if (this.timeoutId) {
+ clearTimeout(this.timeoutId);
+ this.timeoutId = null;
+ }
+
+ if (bannerMessage.isSetTimeout) {
+ this.timeoutId = setTimeout(() => {
+ this.bannerMessage.hide();
+ }, Globals.TIME_TO_HIDE_MESSAGE_BANNER);
+ }
+
+ }
+
+ setFilterExpansionState(filterExpanded: boolean): void {
+ this.filterExpanded = filterExpanded;
+ }
+
+ getFilterExpansionState(): boolean {
+ return this.filterExpanded;
+ }
+
+ setAllUserDepartments(usrDep: UserDepartment[]): void {
+ localStorage.setItem(Globals.ALL_USER_DEPARTMENTS, JSON.stringify(usrDep));
+ }
+
+ getAllUserDepartments(): UserDepartment[] {
+ const usrDep = localStorage.getItem(Globals.ALL_USER_DEPARTMENTS);
+ return JSON.parse(usrDep);
+ }
+
+ setBranches(branches: Branch[]): void {
+ localStorage.setItem(Globals.BRANCHESNAME, JSON.stringify(branches));
+ }
+
+ getBranches(): Branch[] {
+ const branches = localStorage.getItem(Globals.BRANCHESNAME);
+ return JSON.parse(branches);
+ }
+
+ setResponsiblesOnSiteFromGridmeasures(responsibilities: string[]): void {
+ localStorage.setItem(Globals.RESPONSIBILITIESNAME, JSON.stringify(responsibilities));
+ }
+
+ getResponsiblesOnSiteFromGridmeasures(): string[] {
+ const responsibilities = localStorage.getItem(Globals.RESPONSIBILITIESNAME);
+ return JSON.parse(responsibilities);
+ }
+
+ setNetworkControlsFromSingleGridmeasures(networkControls: string[]): void {
+ localStorage.setItem(Globals.NETWORKCONTROLSNAME, JSON.stringify(networkControls));
+ }
+
+ getNetworkControlsFromSingleGridmeasures(): string[] {
+ const networkControls = localStorage.getItem(Globals.NETWORKCONTROLSNAME);
+ return JSON.parse(networkControls);
+ }
+
+ setStatuses(statuses: Status[]): void {
+ localStorage.setItem(Globals.STATUSES, JSON.stringify(statuses));
+ }
+ setCostCenters(costCenters: CostCenter[]): void {
+ localStorage.setItem(Globals.COSTCENTERS, JSON.stringify(costCenters));
+ }
+ getCostCenters(): CostCenter[] {
+ const costCenter = localStorage.getItem(Globals.COSTCENTERS);
+ return JSON.parse(costCenter);
+ }
+ setEmailAddressesFromTemplates(emailAddresses: string[]): void {
+ localStorage.setItem(Globals.EMAILADDRESSES_FROM_TEMPLATE, JSON.stringify(emailAddresses));
+ }
+ getEmailAddressesFromTemplates(): string[] {
+ const emailAddresses = localStorage.getItem(Globals.EMAILADDRESSES_FROM_TEMPLATE);
+ return JSON.parse(emailAddresses);
+ }
+
+ setTerritories(ter: Territory[]): void {
+ localStorage.setItem(Globals.TERRITORY, JSON.stringify(ter));
+ }
+
+ getTerritories(): Territory[] {
+ const ter = localStorage.getItem(Globals.TERRITORY);
+ return JSON.parse(ter);
+ }
+
+ getStatuses(): Status[] {
+ const statuses = localStorage.getItem(Globals.STATUSES);
+ return JSON.parse(statuses);
+ }
+ getCostCenterById(id: number): CostCenter {
+ const costCenters = this.getCostCenters();
+ if (costCenters) {
+ const costCenter = costCenters.filter(s => s.id === id)[0];
+ if (costCenter) {
+ return costCenter;
+ } else {
+ return null;
+ }
+ }
+ return { id: 0, name: 'NOCOSTCENTER' };
+ }
+ getStatusById(id: number): Status {
+ const statuses = this.getStatuses();
+ if (statuses) {
+ const status = statuses.filter(s => s.id === id)[0];
+ if (status) {
+ return status;
+ } else {
+ return null;
+ }
+ }
+ return { id: 0, name: 'NOSTATUS' };
+ }
+
+ getBranchById(id: number): Branch {
+ const bracnh = this.getBranches();
+ if (bracnh) {
+ // tslint:disable-next-line:triple-equals
+ const branch = bracnh.filter(s => s.id == id)[0];
+ if (branch) {
+ return branch;
+ } else {
+ return null;
+ }
+ }
+ return { id: 0, name: 'NOBRANCHES', description: 'nobranches', colorCode: '' };
+ }
+ public getAccessToken(): string {
+ return localStorage.getItem(Globals.ACCESS_TOKEN);
+ }
+
+ public setAccessToken(accessToken: string): void {
+ localStorage.setItem(Globals.ACCESS_TOKEN, accessToken);
+ }
+ public getAccessTokenDecoded(): JwtPayload {
+ const jwtHelper: JwtHelperService = new JwtHelperService();
+ const jwtPayload: JwtPayload = new JwtPayload();
+ const decoded: any = jwtHelper.decodeToken(this.getAccessToken());
+ jwtPayload.name = decoded ? decoded.name : '';
+ return jwtPayload || new JwtPayload();
+ }
+
+ public isUserAuthenticated(): boolean {
+ return this.userAuthenticated;
+ }
+
+ public setUserAuthenticated(flag: boolean) {
+ this.userAuthenticated = flag;
+ }
+
+ public getOverdueReminder(): boolean {
+ return JSON.parse(localStorage.getItem(Globals.OVERDUE_REMINDERS));
+ }
+
+ public setOverdueReminder(flag: boolean) {
+ localStorage.setItem(Globals.OVERDUE_REMINDERS, JSON.stringify(flag));
+ }
+
+ public getUpcomingReminder(): boolean {
+ return JSON.parse(localStorage.getItem(Globals.UPCOMING_REMINDERS));
+ }
+
+ public setUpcomingReminder(flag: boolean) {
+ localStorage.setItem(Globals.UPCOMING_REMINDERS, JSON.stringify(flag));
+ }
+
+ public setBellColor(): string {
+ const error = this.getOverdueReminder();
+ const warning = this.getUpcomingReminder();
+ if (error) {
+ return 'red';
+ } else if (warning) {
+ return '#f79e60';
+ } else {
+ return 'grey';
+ }
+ }
+
+
+ public getCurrentReminders(): number[] {
+ return JSON.parse(localStorage.getItem(Globals.CURRENT_REMINDERS));
+ }
+
+ public setCurrentReminders(currentReminders: number[]) {
+ const tmpCurrentReminders = currentReminders ? currentReminders : [];
+ localStorage.setItem(Globals.CURRENT_REMINDERS, JSON.stringify(tmpCurrentReminders));
+ }
+
+ public getExpiredReminders(): number[] {
+ return JSON.parse(localStorage.getItem(Globals.EXPIRED_REMINDERS));
+ }
+
+ public setExpiredReminders(expiredReminders: number[]) {
+ const tmpExpiredReminders = expiredReminders ? expiredReminders : [];
+ localStorage.setItem(Globals.EXPIRED_REMINDERS, JSON.stringify(tmpExpiredReminders));
+ }
+
+ public isShorttermNotification(ind: number): boolean {
+ const tmpCurrentReminders = this.getCurrentReminders();
+ if (tmpCurrentReminders) {
+ const status = tmpCurrentReminders.filter(s => s === ind)[0];
+ if (status && status.valueOf() > 0) {
+ return true;
+ }
+ return false;
+ }
+ this.setCurrentReminders([]);
+ return false;
+ }
+ public isOverdueNotification(ind: number): boolean {
+ const tmpExpiredReminders = this.getExpiredReminders();
+ if (tmpExpiredReminders) {
+ const status = tmpExpiredReminders.filter(s => s === ind)[0];
+ if (status && status.valueOf() > 0) {
+ return true;
+ }
+ return false;
+ }
+ this.setExpiredReminders([]);
+ return false;
+ }
+
+ public setInactiveFieldsArray(fields: Array<string>) {
+ this.inactiveFields = fields;
+ }
+
+ public getInactiveFields() {
+ return this.inactiveFields;
+ }
+
+ public isReadOnlyForStatus(gm: GridMeasure) {
+ const status = gm.statusId;
+ if (status !== Globals.STATUS.NEW && status !== Globals.STATUS.APPLIED) {
+ return true;
+ }
+ }
+
+
+ getUserSettings(): UserSettings {
+ return JSON.parse(localStorage.getItem(Globals.USER_SETTINGS));
+ }
+
+ setUserSettings(sett: any): void {
+ localStorage.setItem(Globals.USER_SETTINGS, JSON.stringify(sett));
+ }
+
+ getSortingState(): any {
+ return JSON.parse(localStorage.getItem(Globals.SORTING_STATE));
+ }
+
+ setSortingState(state: any): void {
+ localStorage.setItem(Globals.SORTING_STATE, state);
+ }
+
+ getFilteringSearchText(): any {
+ return JSON.parse(localStorage.getItem(Globals.FILTERING_SEARCH_TEXT));
+ }
+
+ setFilteringSearchText(txt: any): void {
+ localStorage.setItem(Globals.FILTERING_SEARCH_TEXT, txt);
+ }
+
+ getColumnState(): any {
+ return JSON.parse(localStorage.getItem(Globals.COLUMN_STATE));
+ }
+
+ setColumnState(state: any): void {
+ localStorage.setItem(Globals.COLUMN_STATE, state);
+ }
+
+ getStatusMainFilter(): StatusMainFilter {
+ return JSON.parse(localStorage.getItem(Globals.STATUS_MAIN_FILTER));
+ }
+
+ setStatusMainFilter(state: StatusMainFilter): void {
+ localStorage.setItem(Globals.STATUS_MAIN_FILTER, JSON.stringify(state));
+ }
+
+ getFilterDirtyState(): boolean {
+ return JSON.parse(localStorage.getItem(Globals.DIRTY_STATE_FILTER));
+ }
+
+ setFilterDirtyState(state: boolean): void {
+ localStorage.setItem(Globals.DIRTY_STATE_FILTER, JSON.stringify(state));
+ }
+
+ isLocked(): boolean {
+ return JSON.parse(localStorage.getItem(Globals.READ_ONLY_FORM));
+ }
+
+ setIsLocked(state: boolean): void {
+ localStorage.setItem(Globals.READ_ONLY_FORM, JSON.stringify(state));
+ }
+
+}
diff --git a/src/app/common/user-map.spec.ts b/src/app/common/user-map.spec.ts
new file mode 100644
index 0000000..6793df3
--- /dev/null
+++ b/src/app/common/user-map.spec.ts
@@ -0,0 +1,52 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+import { TestBed, async, inject } from '@angular/core/testing';
+import { USERS } from '../test-data/users';
+import { UserMap } from './user-map';
+
+
+describe('UserMap', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ });
+
+ });
+
+ it('can render a valid user', () => {
+ const userMap = new UserMap( USERS );
+ expect( userMap.findAndRenderUser('otto')).toBe('Otto Normalverbraucher');
+ });
+
+ it('can render a unknown user', () => {
+ const userMap = new UserMap( USERS );
+ expect( userMap.findAndRenderUser('Unknown')).toBe('[Unknown]');
+ });
+
+ it('throws an exception when not initialized', () => {
+ let errorOccured: boolean;
+
+ try {
+ const userMap = new UserMap(null);
+ } catch (e) {
+ errorOccured = true;
+ }
+
+ expect(errorOccured).toBeTruthy();
+ });
+
+ it('handle empty user', () => {
+ const userMap = new UserMap( USERS );
+ expect( userMap.findAndRenderUser( null )).toBe('');
+ });
+});
+
diff --git a/src/app/common/user-map.ts b/src/app/common/user-map.ts
new file mode 100644
index 0000000..11b152d
--- /dev/null
+++ b/src/app/common/user-map.ts
@@ -0,0 +1,44 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { User } from '../model/user';
+
+export class UserMap {
+ private mappedUsers: User[];
+
+ constructor(allUsers: User[]) {
+ this.mappedUsers = [];
+ if (!allUsers) {
+ console.log('UserMap was created without any Users!');
+ throw new EvalError('UserMap was created without any Users!');
+ }
+ for (const usr of allUsers) {
+ this.mappedUsers[usr.username] = usr;
+ }
+ }
+
+ public findUser(usrShort: string): User {
+ return this.mappedUsers[usrShort];
+ }
+
+ public findAndRenderUser(shortUsr: string): string {
+ if ( !shortUsr ) {
+ return '';
+ }
+
+ const usr = this.findUser(shortUsr);
+ if (!usr) {
+ return '[' + shortUsr + ']';
+ } else {
+ return usr.firstName + (usr.lastName ? ' ' + usr.lastName : '');
+ }
+ }
+}
diff --git a/src/app/common/util.spec.ts b/src/app/common/util.spec.ts
new file mode 100644
index 0000000..9b1971b
--- /dev/null
+++ b/src/app/common/util.spec.ts
@@ -0,0 +1,69 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Util } from './util';
+import { Globals } from './globals';
+import {
+ INPUTDATESTRINGARRAY0,
+ INPUTDATESTRINGARRAY1,
+ INPUTDATESTRINGARRAY2,
+ INPUTDATESTRINGARRAY3,
+ INPUTDATESTRINGARRAY4,
+ INPUTDATESTRINGARRAY5,
+ INPUTDATESTRINGARRAY6
+} from './../test-data/datestringarrays';
+
+describe('Util', () => {
+ let element: HTMLElement;
+
+ beforeEach(() => {
+ element = document.createElement('div');
+ });
+
+ it('calculates the correct drop orientation', () => {
+ spyOn(element, 'getBoundingClientRect').and.returnValue({'top': Globals.DATEPICKER_HEIGHT});
+ expect(Util.calcDatepickerDropOrientation(element)).toBe('down');
+ });
+
+ it('calculates the correct drop orientation', () => {
+ spyOn(element, 'getBoundingClientRect').and.returnValue({'top': Globals.DATEPICKER_HEIGHT - 1});
+ expect(Util.calcDatepickerDropOrientation(element)).toBe('down');
+ });
+
+ it('calculates the correct drop orientation', () => {
+ spyOn(element, 'getBoundingClientRect').and.returnValue({'top': Globals.DATEPICKER_HEIGHT + 1});
+ expect(Util.calcDatepickerDropOrientation(element)).toBe('up');
+ });
+
+ it('calculates the correct drop orientation for no valid HTML element', () => {
+ expect(Util.calcDatepickerDropOrientation(null)).toBe('');
+ });
+
+ it('should calculate earliest datestring', () => {
+ expect(Util.getEarliestValidDateString(INPUTDATESTRINGARRAY0)).toBe('');
+ expect(Util.getEarliestValidDateString(INPUTDATESTRINGARRAY1)).toBe('');
+ expect(Util.getEarliestValidDateString(INPUTDATESTRINGARRAY2)).toBe('');
+ expect(Util.getEarliestValidDateString(INPUTDATESTRINGARRAY3)).toBe('2017-01-15T11:11:00z');
+ expect(Util.getEarliestValidDateString(INPUTDATESTRINGARRAY4)).toBe('2017-01-16T11:11:00z');
+ expect(Util.getEarliestValidDateString(INPUTDATESTRINGARRAY5)).toBe('2017-01-01T11:11:00z');
+ expect(Util.getEarliestValidDateString(INPUTDATESTRINGARRAY6)).toBe('2016-02-15T11:11:00z');
+ });
+
+ it('should calculate latest datestring', () => {
+ expect(Util.getLatestValidDateString(INPUTDATESTRINGARRAY0)).toBe('');
+ expect(Util.getLatestValidDateString(INPUTDATESTRINGARRAY1)).toBe('');
+ expect(Util.getLatestValidDateString(INPUTDATESTRINGARRAY2)).toBe('');
+ expect(Util.getLatestValidDateString(INPUTDATESTRINGARRAY3)).toBe('2017-01-15T11:11:00z');
+ expect(Util.getLatestValidDateString(INPUTDATESTRINGARRAY4)).toBe('2017-01-16T11:11:00z');
+ expect(Util.getLatestValidDateString(INPUTDATESTRINGARRAY5)).toBe('2017-02-15T11:11:00z');
+ expect(Util.getLatestValidDateString(INPUTDATESTRINGARRAY6)).toBe('2018-01-01T11:11:00z');
+ });
+});
diff --git a/src/app/common/util.ts b/src/app/common/util.ts
new file mode 100644
index 0000000..611c1a2
--- /dev/null
+++ b/src/app/common/util.ts
@@ -0,0 +1,77 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Globals } from './globals';
+import { StringToDatePipe } from '../common-components/pipes/string-to-date.pipe';
+import { isDate } from '../../../node_modules/moment';
+import { isNull } from 'util';
+declare var $: any;
+
+export class Util {
+ static calcDatepickerDropOrientation(element: HTMLElement): string {
+ if ( element instanceof HTMLElement ) {
+ return element.getBoundingClientRect().top > Globals.DATEPICKER_HEIGHT ? 'up' : 'down';
+ } else {
+ return '';
+ }
+ }
+
+ static showGridmeasureTab(): void {
+ $('.nav-tabs a[href="#gridmeasurepanel"]').tab('show');
+ }
+
+ static showSingleGridmeasureTab(sortorder: number): void {
+ // just wait a bit till the eventually new tab is rendered
+ setTimeout(() => $('.nav-tabs a[href="#singlegridmeasure"][id="' + sortorder + '"]').tab('show'));
+ }
+
+ static getEarliestValidDateString(dates: string[]): string {
+ let earliestDateString = null;
+ const pipe = new StringToDatePipe();
+
+ if (dates && dates.length > 0) {
+
+ for (const d of dates) {
+ const earliestDate = pipe.transform(earliestDateString);
+ const actualDate = pipe.transform(d);
+
+ if (actualDate) {
+ if (isNull(earliestDate) || actualDate.valueOf() < earliestDate.valueOf()) {
+ earliestDateString = d;
+ }
+ }
+ }
+ }
+
+ return isNull(earliestDateString) ? '' : earliestDateString;
+ }
+
+ static getLatestValidDateString(dates: string[]): string {
+ let latestDateString = null;
+ const pipe = new StringToDatePipe();
+
+ if (dates && dates.length > 0) {
+
+ for (const d of dates) {
+ const latestDate = pipe.transform(latestDateString);
+ const actualDate = pipe.transform(d);
+
+ if (actualDate) {
+ if (isNull(latestDate) || actualDate.valueOf() > latestDate.valueOf()) {
+ latestDateString = d;
+ }
+ }
+ }
+ }
+
+ return isNull(latestDateString) ? '' : latestDateString;
+ }
+}
diff --git a/src/app/custom_modules/calendar/angular-calendar-openk.css b/src/app/custom_modules/calendar/angular-calendar-openk.css
new file mode 100644
index 0000000..52e47e3
--- /dev/null
+++ b/src/app/custom_modules/calendar/angular-calendar-openk.css
@@ -0,0 +1,441 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+.cal-month-view .cal-header {
+ text-align: center;
+ font-weight: bolder;
+ font-family: Arial;
+}
+
+.cal-month-view .cal-cell-row:hover {
+ background-color: #fafafa;
+}
+
+.cal-month-view .cal-header .cal-cell {
+ padding: 5px 0;
+ overflow: hidden;
+ -o-text-overflow: ellipsis;
+ text-overflow: ellipsis;
+ display: block;
+ white-space: nowrap;
+}
+
+.cal-month-view .cal-cell-row .cal-cell:hover,
+.cal-month-view .cal-cell.cal-has-events.cal-open {
+ background-color: #ededed;
+}
+
+.cal-month-view .cal-days {
+ border: 1px solid #e1e1e1;
+ border-bottom: 0;
+}
+
+.cal-month-view .cal-cell-top {
+ min-height: 78px;
+ -webkit-box-flex: 1;
+ -ms-flex: 1;
+ flex: 1;
+}
+
+.cal-month-view .cal-cell-row {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ -js-display: flex;
+ display: flex;
+}
+
+.cal-month-view .cal-cell {
+ float: left;
+ -webkit-box-flex: 1;
+ -ms-flex: 1;
+ flex: 1;
+ display: -webkit-box;
+ display: -ms-flexbox;
+ -js-display: flex;
+ display: flex;
+ -webkit-box-orient: vertical;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ -webkit-box-align: stretch;
+ -ms-flex-align: stretch;
+ align-items: stretch;
+}
+
+.cal-month-view .cal-day-cell {
+ min-height: 100px;
+}
+
+.cal-month-view .cal-day-cell:not(:last-child) {
+ border-right: 1px solid #e1e1e1;
+}
+
+.cal-month-view .cal-days .cal-cell-row {
+ border-bottom: 1px solid #e1e1e1;
+}
+
+.cal-month-view .cal-day-badge {
+ margin-top: 18px;
+ margin-left: 10px;
+ background-color: #b94a48;
+ display: inline-block;
+ min-width: 10px;
+ padding: 3px 7px;
+ font-size: 12px;
+ font-weight: 700;
+ line-height: 1;
+ color: white;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: middle;
+ border-radius: 10px;
+}
+
+.cal-month-view .cal-day-number {
+ font-size: 1.2em;
+ font-weight: 400;
+ opacity: 0.5;
+ margin-top: 15px;
+ margin-right: 15px;
+ float: right;
+ margin-bottom: 10px;
+}
+
+.cal-month-view .cal-events {
+ -webkit-box-flex: 1;
+ -ms-flex: 1;
+ flex: 1;
+ -webkit-box-align: end;
+ -ms-flex-align: end;
+ align-items: flex-end;
+ margin: 3px;
+ line-height: 10px;
+ display: -webkit-box;
+ display: -ms-flexbox;
+ -js-display: flex;
+ display: flex;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+}
+
+.cal-month-view .cal-event {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ display: inline-block;
+ margin: 2px;
+}
+
+.cal-month-view .cal-day-cell.cal-in-month.cal-has-events {
+ cursor: pointer;
+}
+
+.cal-month-view .cal-day-cell.cal-out-month .cal-day-number {
+ opacity: 0.1;
+ cursor: default;
+}
+
+.cal-month-view .cal-day-cell.cal-weekend .cal-day-number {
+ color: darkred;
+}
+
+.cal-month-view .cal-day-cell.cal-today {
+ background-color: #e8fde7;
+}
+
+.cal-month-view .cal-day-cell.cal-today .cal-day-number {
+ font-size: 1.9em;
+}
+
+.cal-month-view .cal-day-cell.cal-drag-over {
+ background-color: #e0e0e0 !important;
+}
+
+.cal-month-view .cal-open-day-events {
+ padding: 15px;
+ color: white;
+ background-color: #ededed;
+ /* -webkit-box-shadow: inset 0 0 15px 0 rgba(0, 0, 0, 0.5); */
+ /* box-shadow: inset 0 0 15px 0 rgba(0, 0, 0, 0.5); */
+}
+
+.cal-month-view .cal-open-day-events .cal-event {
+ position: relative;
+ top: 2px;
+}
+
+.cal-month-view .cal-event-title {
+ color: #555;
+}
+
+.cal-month-view .cal-out-month .cal-day-badge,
+.cal-month-view .cal-out-month .cal-event {
+ opacity: 0.3;
+}
+
+.cal-week-view .cal-day-headers {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ -js-display: flex;
+ display: flex;
+ margin-bottom: 3px;
+ border: 1px solid #e1e1e1;
+ margin-left: 2px;
+ margin-right: 2px;
+}
+
+.cal-week-view .cal-day-headers .cal-header {
+ -webkit-box-flex: 1;
+ -ms-flex: 1;
+ flex: 1;
+ text-align: center;
+ padding: 5px;
+}
+
+.cal-week-view .cal-day-headers .cal-header:not(:last-child) {
+ border-right: 1px solid #e1e1e1;
+}
+
+.cal-week-view .cal-day-headers .cal-header:hover,
+.cal-week-view .cal-day-headers .cal-drag-over {
+ background-color: #ededed;
+}
+
+.cal-week-view .cal-day-headers span {
+ font-weight: 400;
+ opacity: 0.5;
+}
+
+.cal-week-view .cal-events-row {
+ position: relative;
+ height: 33px;
+}
+
+.cal-week-view .cal-event-container {
+ display: inline-block;
+ position: absolute;
+}
+
+.cal-week-view .cal-event {
+ padding: 0 10px;
+ font-size: 12px;
+ margin-left: 2px;
+ margin-right: 2px;
+ height: 30px;
+ line-height: 30px;
+}
+
+.cal-week-view .cal-draggable {
+ cursor: move;
+}
+
+.cal-week-view .cal-starts-within-week .cal-event {
+ border-top-left-radius: 5px;
+ border-bottom-left-radius: 5px;
+}
+
+.cal-week-view .cal-ends-within-week .cal-event {
+ border-top-right-radius: 5px;
+ border-bottom-right-radius: 5px;
+}
+
+.cal-week-view .cal-header.cal-today {
+ background-color: #e8fde7;
+}
+
+.cal-week-view .cal-header.cal-weekend span {
+ color: #8b0000;
+}
+
+.cal-week-view .cal-event,
+.cal-week-view .cal-header {
+ overflow: hidden;
+ -o-text-overflow: ellipsis;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.cal-day-view {
+ /* stylelint-disable-next-line selector-type-no-unknown */
+}
+
+.cal-day-view .cal-hour-rows {
+ width: 100%;
+ border: solid 1px #e1e1e1;
+ overflow-x: scroll;
+ position: relative;
+}
+
+.cal-day-view .cal-hour:nth-child(odd) {
+ background-color: #fafafa;
+}
+
+.cal-day-view mwl-calendar-day-view-hour-segment,
+.cal-day-view .cal-hour-segment {
+ display: block;
+}
+
+.cal-day-view .cal-hour-segment::after {
+ content: '\00a0';
+}
+
+.cal-day-view .cal-hour:not(:last-child) .cal-hour-segment,
+.cal-day-view .cal-hour:last-child :not(:last-child) .cal-hour-segment {
+ border-bottom: thin dashed #e1e1e1;
+}
+
+.cal-day-view .cal-time {
+ font-weight: bold;
+ padding-top: 5px;
+ width: 70px;
+ text-align: center;
+}
+
+.cal-day-view .cal-hour-segment.cal-after-hour-start .cal-time {
+ display: none;
+}
+
+.cal-day-view .cal-hour-segment:hover,
+.cal-day-view .cal-drag-over .cal-hour-segment {
+ background-color: #ededed;
+}
+
+.cal-day-view .cal-event-container {
+ position: absolute;
+}
+
+.cal-day-view .cal-event {
+ border: solid 1px;
+ padding: 5px;
+ font-size: 12px;
+ overflow: hidden;
+ -o-text-overflow: ellipsis;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ height: 100%;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+.cal-day-view .cal-draggable {
+ cursor: move;
+}
+
+.cal-day-view .cal-starts-within-day .cal-event {
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+}
+
+.cal-day-view .cal-ends-within-day .cal-event {
+ border-bottom-left-radius: 5px;
+ border-bottom-right-radius: 5px;
+}
+
+.cal-day-view .cal-all-day-event {
+ padding: 8px;
+ border: solid 1px;
+}
+
+.cal-tooltip {
+ position: absolute;
+ z-index: 1070;
+ display: block;
+ font-style: normal;
+ font-weight: normal;
+ letter-spacing: normal;
+ line-break: auto;
+ line-height: 1.5;
+ text-align: start;
+ text-decoration: none;
+ text-shadow: none;
+ text-transform: none;
+ white-space: normal;
+ word-break: normal;
+ word-spacing: normal;
+ font-size: 11px;
+ word-wrap: break-word;
+ opacity: 0.9;
+}
+
+.cal-tooltip.cal-tooltip-top {
+ padding: 5px 0;
+ margin-top: -3px;
+}
+
+.cal-tooltip.cal-tooltip-top .cal-tooltip-arrow {
+ bottom: 0;
+ left: 50%;
+ margin-left: -5px;
+ border-width: 5px 5px 0;
+ border-top-color: #000;
+}
+
+.cal-tooltip.cal-tooltip-right {
+ padding: 0 5px;
+ margin-left: 3px;
+}
+
+.cal-tooltip.cal-tooltip-right .cal-tooltip-arrow {
+ top: 50%;
+ left: 0;
+ margin-top: -5px;
+ border-width: 5px 5px 5px 0;
+ border-right-color: #000;
+}
+
+.cal-tooltip.cal-tooltip-bottom {
+ padding: 5px 0;
+ margin-top: 3px;
+}
+
+.cal-tooltip.cal-tooltip-bottom .cal-tooltip-arrow {
+ top: 0;
+ left: 50%;
+ margin-left: -5px;
+ border-width: 0 5px 5px;
+ border-bottom-color: #000;
+}
+
+.cal-tooltip.cal-tooltip-left {
+ padding: 0 5px;
+ margin-left: -3px;
+}
+
+.cal-tooltip.cal-tooltip-left .cal-tooltip-arrow {
+ top: 50%;
+ right: 0;
+ margin-top: -5px;
+ border-width: 5px 0 5px 5px;
+ border-left-color: #000;
+}
+
+.cal-tooltip-inner {
+ max-width: 200px;
+ padding: 3px 8px;
+ color: #fff;
+ text-align: center;
+ background-color: #000;
+ border-radius: 0.25rem;
+}
+
+.cal-tooltip-arrow {
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
+}
+
+.glyphicon-eye-open:hover,
+.glyphicon-pencil:hover {
+ filter: brightness(200%);
+}
\ No newline at end of file
diff --git a/src/app/custom_modules/calendar/calendar.component.css b/src/app/custom_modules/calendar/calendar.component.css
new file mode 100644
index 0000000..69579f4
--- /dev/null
+++ b/src/app/custom_modules/calendar/calendar.component.css
@@ -0,0 +1,33 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+.btn-group {
+ margin-top: 15px;
+}
+
+.btn-group.paging .btn {
+ width: 92px;
+}
+
+.btn-group.period .btn {
+ width: 70px;
+}
+
+.pta-day-view-template,
+.table-bordered>tbody>tr>td,
+.table-bordered>tbody>tr>th,
+.table-bordered>tfoot>tr>td,
+.table-bordered>tfoot>tr>th,
+.table-bordered>thead>tr>td,
+.table-bordered>thead>tr>th {
+ border: 0px;
+}
\ No newline at end of file
diff --git a/src/app/custom_modules/calendar/calendar.component.html b/src/app/custom_modules/calendar/calendar.component.html
new file mode 100644
index 0000000..8950bd5
--- /dev/null
+++ b/src/app/custom_modules/calendar/calendar.component.html
@@ -0,0 +1,71 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+-->
+<div class="panel panel-default">
+ <div class="panel-heading">
+ <h4 class="panel-title">
+ <div>
+ <div class="row text-center">
+ <div class="col-md-4">
+ <div class="btn-group paging">
+ <div class="btn btn-primary" mwlCalendarPreviousView [view]="view" [(viewDate)]="currDate" (viewDateChange)="activeDayIsOpen = false">
+ {{view === 'week' ? 'Vorherige' : 'Vorheriger'}}
+ </div>
+ <div class="btn btn-primary" mwlCalendarToday [(viewDate)]="currDate">
+ {{view === 'week' ? 'Aktuelle' : 'Aktueller'}}
+ </div>
+ <div class="btn btn-primary" mwlCalendarNextView [view]="view" [(viewDate)]="currDate" (viewDateChange)="activeDayIsOpen = false">
+ {{view === 'week' ? 'Nächste' : 'Nächster'}}
+ </div>
+ </div>
+ </div>
+ <div class="col-md-4">
+ <h3>{{ currDate | calendarDate:(view + 'ViewTitle'):'de-DE' }}</h3>
+ </div>
+ <div class="col-md-4">
+ <div class="btn-group period">
+ <div class="btn btn-primary" (click)="view = 'month'" [class.active]="view === 'month'">
+ Monat
+ </div>
+ <div class="btn btn-primary" (click)="view = 'week'" [class.active]="view === 'week'">
+ Woche
+ </div>
+ <div class="btn btn-primary" (click)="view = 'day'" [class.active]="view === 'day'">
+ Tag
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </h4>
+ </div>
+ <div id="collapse4" class="panel-collapse collapse in">
+
+ <div class="panel-body">
+
+ <div [ngSwitch]="view">
+ <mwl-calendar-month-view [weekStartsOn]="weekStartsOn" [locale]="locale" *ngSwitchCase=" 'month' " [viewDate]="currDate"
+ [events]="events" [refresh]="refresh" [activeDayIsOpen]="activeDayIsOpen" (eventClicked)="eventClicked( 'Clicked', $event.event)"
+ (dayClicked)="dayClicked($event.day)" (eventTimesChanged)="eventTimesChanged($event)">
+ </mwl-calendar-month-view>
+ <mwl-calendar-week-view [weekStartsOn]="weekStartsOn" [locale]="locale" *ngSwitchCase=" 'week' " [viewDate]="currDate" [events]="events"
+ [refresh]="refresh" (eventTimesChanged)="eventTimesChanged($event)" (eventClicked)="eventClicked( 'Clicked', $event.event)">
+ </mwl-calendar-week-view>
+ <mwl-calendar-day-view [locale]="locale" *ngSwitchCase=" 'day' " [viewDate]="currDate" [events]="events" [refresh]="refresh"
+ (eventTimesChanged)="eventTimesChanged($event)" (eventClicked)="eventClicked( 'Clicked', $event.event)">
+
+ </mwl-calendar-day-view>
+ </div>
+ </div>
+ </div>
+</div>
+
+<br>
\ No newline at end of file
diff --git a/src/app/custom_modules/calendar/calendar.component.spec.ts b/src/app/custom_modules/calendar/calendar.component.spec.ts
new file mode 100644
index 0000000..221fb47
--- /dev/null
+++ b/src/app/custom_modules/calendar/calendar.component.spec.ts
@@ -0,0 +1,495 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { ComponentFixture, TestBed, async, fakeAsync, tick, inject } from '@angular/core/testing';
+import { AbstractMockObservableService } from '../../testing/abstract-mock-observable.service';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { MockComponent } from '../../testing/mock.component';
+import { registerLocaleData } from '@angular/common';
+import localeDe from '@angular/common/locales/de';
+import { FormsModule } from '@angular/forms';
+import { Router } from '@angular/router';
+import { SessionContext } from './../../common/session-context';
+import { Globals } from '../../common/globals';
+import { CustomCalendarComponent } from './calendar.component';
+import { CustomDateFormatter } from './custom-date-formatter-provider';
+import { GridMeasureService } from '../../services/grid-measure.service';
+import { UserSettingsService } from '../../services/user-settings.service';
+import { RoleAccessHelperService } from '../../services/jobs/role-access-helper.service';
+import { RoleAccess } from '../../model/role-access';
+import { GridMeasure } from '../../model/grid-measure';
+import { UserSettings } from './../../model/user-settings';
+import { USERS } from './../../test-data/users';
+import { GRIDMEASURE } from '../../test-data/grid-measures';
+
+import {
+ startOfDay,
+ endOfDay,
+ subDays,
+ addDays,
+ endOfMonth,
+ isSameDay,
+ isSameMonth,
+ addHours
+} from 'date-fns';
+
+import {
+ CalendarEventTitleFormatter,
+ CalendarModule,
+ CalendarEvent,
+ CalendarEventAction,
+ CalendarEventTimesChangedEvent,
+ CalendarDateFormatter,
+ CalendarDayModule,
+ CalendarWeekViewComponent,
+ CalendarMonthViewComponent,
+ CalendarDayViewComponent
+} from 'angular-calendar';
+import { ModeValidator } from '../helpers/mode-validator';
+import { MessageServiceCustom } from '../../services/message.service';
+
+class FakeRouter {
+ navigate(commands: any[]) {
+ return commands[0];
+ }
+}
+
+describe('CustomCalendarComponent', () => {
+ registerLocaleData(localeDe);
+ let component: CustomCalendarComponent;
+ let fixture: ComponentFixture<CustomCalendarComponent>;
+ const gridmeasures: GridMeasure[] = JSON.parse(JSON.stringify(GRIDMEASURE));
+ let routerStub: FakeRouter;
+ let sessionContext;
+
+ routerStub = {
+ navigate: jasmine.createSpy('navigate').and.callThrough()
+ };
+
+ class GridmeasureToEventHelper {
+ public createEventsFromGridMeasures(gridMeasures: GridMeasure[], mode: string) {
+ const events: CalendarEvent<GridMeasure>[] = new Array<CalendarEvent>();
+ const editAction = <CalendarEventAction>{
+ label: '<i class="glyphicon glyphicon-pencil"></i>'
+ };
+ const viewAction = <CalendarEventAction>{
+ label: '<i class="glyphicon glyphicon-eye-open"></i>'
+ };
+ gridMeasures.forEach(gridMeasure => {
+ if (!gridMeasure.plannedStarttimeFirstSinglemeasure || !gridMeasure.plannedEndtimeGridmeasure) {
+ console.log('No valide date values for ' + gridMeasure.title);
+ console.log('Planned End Time Gridmeasure ' + gridMeasure.plannedEndtimeGridmeasure);
+ console.log('Start Time First Sequence: ' + gridMeasure.plannedStarttimeFirstSinglemeasure);
+ } else {
+ events.push(<CalendarEvent><GridMeasure>{
+ id: gridMeasure.id,
+ title: gridMeasure.title || 'TITLE NOT DEFINED',
+ start: new Date(gridMeasure.plannedStarttimeFirstSinglemeasure),
+ end: new Date(gridMeasure.plannedEndtimeGridmeasure),
+ color: {
+ primary: '#ad2121',
+ secondary: '#FAE3E3'
+ },
+ draggable: true,
+ actions: [mode === 'edit' ? editAction : viewAction],
+ resizable: {
+ beforeStart: true,
+ afterEnd: true
+ },
+ meta: gridMeasure
+ });
+ }
+ });
+ return events;
+ }
+ }
+
+ class MockGridMeasureService extends AbstractMockObservableService {
+ getGridMeasures() {
+ return this;
+ }
+ }
+
+ class MockUserSettingService extends AbstractMockObservableService {
+ savedUserSettings: UserSettings;
+ getUserSettings(gridId: string) {
+ return this;
+ }
+ setUserSettings(userSettings: UserSettings) {
+ this.savedUserSettings = userSettings;
+ return this;
+ }
+ }
+
+ let mockUserSettingService;
+ let roleAccessHelper: RoleAccessHelperService;
+ let mockGridMeasureService;
+
+ beforeEach(async(() => {
+ sessionContext = new SessionContext();
+ mockGridMeasureService = new MockGridMeasureService();
+ mockUserSettingService = new MockUserSettingService();
+ roleAccessHelper = new RoleAccessHelperService();
+
+ sessionContext.setCurrUser(USERS[1]);
+ sessionContext.setAllUsers(USERS);
+
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule,
+ BrowserAnimationsModule,
+ CalendarModule.forRoot({
+ dateFormatter: {
+ provide: CalendarDateFormatter,
+ useClass: CustomDateFormatter
+ }
+ })
+
+ ],
+ declarations: [
+ CustomCalendarComponent,
+ MockComponent({ selector: 'input', inputs: ['options'] }),
+ MockComponent({ selector: 'app-grid-measures', inputs: ['gridId', 'withEditButtons'] }),
+ MockComponent({ selector: 'app-loading-spinner', inputs: [] }),
+ MockComponent({
+ selector: 'app-buttons-container',
+ inputs: ['activeButtons', 'isValidForm', 'isValidForSave', 'isReadOnlyForm', 'gridMeasureStatusId']
+ })
+ ],
+ providers: [
+ MessageServiceCustom,
+ ModeValidator,
+ { provide: SessionContext, useValue: sessionContext },
+ { provide: Router, useValue: routerStub },
+ { provide: GridMeasureService, useValue: mockGridMeasureService },
+ { provide: RoleAccessHelperService, useValue: roleAccessHelper },
+ { provide: UserSettingsService, useValue: mockUserSettingService },
+ { provide: CalendarDateFormatter, useClass: CustomDateFormatter }
+ ]
+ }).compileComponents();
+ })
+ );
+
+ let eventTitle: CalendarEventTitleFormatter;
+ beforeEach(
+ inject([CalendarEventTitleFormatter], _eventTitle_ => {
+ eventTitle = _eventTitle_;
+ })
+ );
+
+ beforeEach(fakeAsync(() => {
+ fixture = TestBed.createComponent(CustomCalendarComponent);
+ tick();
+ component = fixture.componentInstance;
+ component.currDate = new Date('2017-01-15');
+ sessionContext.setCurrUser(USERS[1]);
+ sessionContext.setAllUsers(USERS);
+ fixture.detectChanges();
+
+ // we need to init the component and the path... because of OnInit
+ mockGridMeasureService.content = JSON.parse(JSON.stringify(GRIDMEASURE[0]));
+ const roleAcess: RoleAccess = {
+ editRoles: [{
+ name: 'planned-policies-measureplanner',
+ gridMeasureStatusIds: [
+ 0,
+ 1
+ ]
+ }, {
+ name: 'planned-policies-superuser',
+ gridMeasureStatusIds: [
+ 0,
+ 1
+ ]
+ }, {
+ name: 'planned-policies-measureapplicant',
+ gridMeasureStatusIds: [
+ 0,
+ 1
+ ]
+ }],
+ controls: [{
+ gridMeasureStatusId: 0,
+ activeButtons: [
+ 'save',
+ 'apply',
+ 'cancel'
+ ],
+ inactiveFields: [
+ 'titeldermassnahme'
+ ]
+ },
+ {
+ gridMeasureStatusId: 1,
+ activeButtons: [
+ 'save',
+ 'cancel',
+ 'forapproval'
+ ],
+ inactiveFields: [
+ 'titeldermassnahme'
+ ]
+ }],
+ stornoSection:
+ {
+ 'stornoRoles': [
+ 'planned-policies-measureapplicant',
+ 'planned-policies-measureplanner',
+ 'planned-policies-measureapprover',
+ 'planned-policies-requester',
+ 'planned-policies-clearance'
+ ]
+ },
+ duplicateSection:
+ {
+ 'duplicateRoles': [
+ 'planned-policies-measureapplicant'
+ ]
+ }
+
+ };
+ roleAccessHelper.init(roleAcess);
+
+
+ }));
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should generate the day view', () => {
+ const fixtureDayView: ComponentFixture<CalendarDayViewComponent> = TestBed.createComponent(CalendarDayViewComponent);
+ fixtureDayView.componentInstance.viewDate = new Date('2017-01-15');
+ fixtureDayView.componentInstance.events = [
+ {
+ start: new Date('2017-01-15'),
+ title: 'Test grid-measure',
+ color: {
+ primary: '',
+ secondary: ''
+ }
+ }
+ ];
+ fixtureDayView.componentInstance.ngOnChanges({ viewDate: {}, events: {} });
+ expect(fixtureDayView.componentInstance.view.events.length).toBe(1);
+ expect(fixtureDayView.componentInstance.view.events[0].event).toBe(
+ fixtureDayView.componentInstance.events[0]
+ );
+ expect(fixtureDayView.componentInstance.hours.length).toBe(24);
+
+ });
+
+ it('should generate the week view', () => {
+ const fixtureWeekView: ComponentFixture<CalendarWeekViewComponent> = TestBed.createComponent(CalendarWeekViewComponent);
+ fixtureWeekView.componentInstance.viewDate = new Date('2017-01-15');
+ fixtureWeekView.componentInstance.events = [
+ {
+ start: new Date('2017-01-15'),
+ title: 'Test grid-measure',
+ color: {
+ primary: '',
+ secondary: ''
+ }
+ }
+ ];
+ fixtureWeekView.componentInstance.ngOnChanges({ viewDate: {}, events: {} });
+ expect(fixtureWeekView.componentInstance.view.eventRows.length).toBe(1);
+ expect(fixtureWeekView.componentInstance.view.eventRows[0].row[0].event).toBe(
+ fixtureWeekView.componentInstance.events[0]
+ );
+
+ });
+
+ it('should generate the month view', () => {
+ const fixtureMonthView: ComponentFixture<CalendarMonthViewComponent> = TestBed.createComponent(CalendarMonthViewComponent);
+ fixtureMonthView.componentInstance.viewDate = new Date('2017-01-15');
+ fixtureMonthView.componentInstance.events = [
+ {
+ start: new Date('2017-01-15'),
+ end: new Date('2017-01-20'),
+ title: 'Test grid-measure',
+ color: {
+ primary: '',
+ secondary: ''
+ }
+ }
+ ];
+ fixtureMonthView.componentInstance.ngOnChanges({ viewDate: {}, events: {} });
+ expect(fixtureMonthView.componentInstance.events.length).toBe(1);
+ expect(fixtureMonthView.componentInstance.events[0]).toBe(
+ fixtureMonthView.componentInstance.events[0]
+ );
+
+ });
+
+ it('should show correct number of grid-measures for a day', async(() => {
+ mockGridMeasureService.content = JSON.parse(JSON.stringify(GRIDMEASURE[0]));
+
+ fixture.detectChanges();
+
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect(component.events.length).toBe(1);
+ });
+ const calEvent: CalendarEvent = component.events[0];
+ component.removeEvent(calEvent);
+
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect(component.events.length).toBe(0);
+ });
+
+ component.addEvent();
+ component.currDate = new Date();
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect(component.events.length).toBe(1);
+ });
+ }));
+
+ xit('should navigate to grid-measure-detail for in edit-mode', (() => {
+ mockGridMeasureService.content = JSON.parse(JSON.stringify(GRIDMEASURE[0]));
+ component.events = new GridmeasureToEventHelper().createEventsFromGridMeasures(gridmeasures, 'edit');
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ console.log('component.events[0] : ' + component.events[0]);
+ const calEvent: CalendarEvent<GridMeasure> = component.events[0];
+ console.log('calEvent : ' + calEvent);
+
+ component.eventClicked('edit', calEvent);
+
+ expect(routerStub.navigate).toHaveBeenCalledWith(['/gridMeasureDetail/', 1111, 'edit']);
+ });
+ }));
+
+ xit('should check if there are grid measure for actual day and show them', async(() => {
+ component.events.push(<CalendarEvent><GridMeasure>{
+ id: 1,
+ title: 'TITLE',
+ start: new Date('2017-01-12'),
+ end: new Date('2017-01-18')
+ });
+ fixture.detectChanges();
+ component.currDate = new Date('2017-01-16');
+ spyOn((component as any), 'showGridMeasuresIfThere').and.callThrough();
+ (component as any).showGridMeasuresIfThere();
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect((component as any).showGridMeasuresIfThere).toHaveBeenCalled();
+ expect(component.activeDayIsOpen).toBeTruthy();
+ });
+
+ }));
+
+ xit('should check if there are grid measure for actual day(no event actual) and dont show them', async(() => {
+ component.events.push(<CalendarEvent><GridMeasure>{
+ id: 1,
+ title: 'TITLE',
+ start: new Date('2017-01-12'),
+ end: new Date('2017-01-18')
+ });
+ fixture.detectChanges();
+ component.currDate = new Date('2017-01-25');
+ spyOn((component as any), 'showGridMeasuresIfThere').and.callThrough();
+ (component as any).showGridMeasuresIfThere();
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect((component as any).showGridMeasuresIfThere).toHaveBeenCalled();
+ expect(component.activeDayIsOpen).toBeFalsy();
+ });
+
+ }));
+
+ xit('should leave open the gm dialog in month overview if there are events', async(() => {
+ const events: CalendarEvent<GridMeasure>[] = new Array<CalendarEvent>();
+ events.push(<CalendarEvent><GridMeasure>{
+ id: 1,
+ title: 'TITLE',
+ start: new Date('2017-01-12'),
+ end: new Date('2017-01-18')
+ });
+ fixture.detectChanges();
+ component.activeDayIsOpen = true;
+ component.currDate = new Date('2017-01-16');
+ const date = new Date('2017-01-16');
+ spyOn(component, 'dayClicked').and.callThrough();
+ fixture.detectChanges();
+ component.dayClicked({ date, events });
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect(component.dayClicked).toHaveBeenCalled();
+ expect(component.activeDayIsOpen).toBeFalsy();
+ });
+
+ }));
+
+ xit('should close gm dialog if there are no events', async(() => {
+ const events: CalendarEvent<GridMeasure>[] = new Array<CalendarEvent>();
+ fixture.detectChanges();
+ component.activeDayIsOpen = true;
+ component.currDate = new Date('2017-01-16');
+ const date = new Date('2017-01-16');
+ spyOn(component, 'dayClicked').and.callThrough();
+ fixture.detectChanges();
+ component.dayClicked({ date, events });
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect(component.dayClicked).toHaveBeenCalled();
+ expect(component.activeDayIsOpen).toBeFalsy();
+ });
+
+ }));
+
+ xit('should open gm dialog if there are events and not even open', async(() => {
+ const events: CalendarEvent<GridMeasure>[] = new Array<CalendarEvent>();
+ events.push(<CalendarEvent><GridMeasure>{
+ id: 1,
+ title: 'TITLE',
+ start: new Date('2017-01-12'),
+ end: new Date('2017-01-18')
+ });
+ fixture.detectChanges();
+ component.activeDayIsOpen = false;
+ component.currDate = new Date('2017-01-17');
+ const date = new Date('2017-01-17');
+ spyOn(component, 'dayClicked').and.callThrough();
+ fixture.detectChanges();
+ component.dayClicked({ date, events });
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect(component.dayClicked).toHaveBeenCalled();
+ expect(component.activeDayIsOpen).toBeTruthy();
+ });
+
+ }));
+
+ xit('should check the action of an event and navigate there with edit', async(() => {
+ const events: CalendarEvent<GridMeasure>[] = new Array<CalendarEvent>();
+ events.push(<CalendarEvent><GridMeasure>{
+ id: 1,
+ title: 'TITLE',
+ start: new Date('2017-01-12'),
+ end: new Date('2017-01-18')
+ });
+ fixture.detectChanges();
+ component.activeDayIsOpen = false;
+ spyOn(component, 'eventClicked').and.callThrough();
+ fixture.detectChanges();
+ component.eventClicked(Globals.MODE.EDIT, events[0]);
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect(routerStub.navigate).toHaveBeenCalledWith(['/gridMeasureDetail/', events[0].id, Globals.MODE.EDIT]);
+ });
+
+ }));
+
+});
diff --git a/src/app/custom_modules/calendar/calendar.component.ts b/src/app/custom_modules/calendar/calendar.component.ts
new file mode 100644
index 0000000..1cc1957
--- /dev/null
+++ b/src/app/custom_modules/calendar/calendar.component.ts
@@ -0,0 +1,209 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Component, OnInit, ViewChild, TemplateRef } from '@angular/core';
+import { GridMeasureService } from './../../services/grid-measure.service';
+import { GridMeasure } from './../../model/grid-measure';
+
+import {
+ startOfDay,
+ endOfDay,
+ subDays,
+ addDays,
+ endOfMonth,
+ isSameDay,
+ isSameMonth,
+ addHours
+} from 'date-fns';
+import {
+ CalendarEvent,
+ CalendarEventAction,
+ CalendarEventTimesChangedEvent,
+ CalendarDateFormatter
+} from 'angular-calendar';
+import { Subject } from 'rxjs/Subject';
+import { UndoRedoStackComponent } from '../undo-redo-stack/undo-redo-stack.component';
+import { CustomDateFormatter } from './custom-date-formatter-provider';
+import { Router } from '@angular/router';
+import { ModeValidator } from '../helpers/mode-validator';
+import { StatusMainFilterItem, StatusMainFilter } from '../../model/status-main-filter';
+
+const colors: any = {
+ red: {
+ primary: '#ad2121',
+ secondary: '#FAE3E3'
+ },
+ blue: {
+ primary: '#1e90ff',
+ secondary: '#D1E8FF'
+ },
+ yellow: {
+ primary: '#e3bc08',
+ secondary: '#FDF1BA'
+ }
+};
+@Component({
+ selector: 'app-custom-calendar',
+ templateUrl: './calendar.component.html',
+ styleUrls: ['./calendar.component.css'],
+ providers: [
+ {
+ provide: CalendarDateFormatter,
+ useClass: CustomDateFormatter
+ }
+ ]
+})
+export class CustomCalendarComponent implements OnInit {
+
+ @ViewChild('modalContent') modalContent: TemplateRef<any>;
+ weekStartsOn = 1;
+ locale = 'de';
+ undoRedoStack = new UndoRedoStackComponent();
+ eventTypes = colors;
+ eventTypeKeys = ['blue', 'red', 'yellow'];
+
+ activeDayIsOpen = true;
+ refresh: Subject<any> = new Subject();
+
+ modalData: {
+ action: string;
+ event: CalendarEvent;
+ };
+
+ view: any = 'month';
+ currDate: Date = new Date();
+ currentEditEventStart: Date = new Date();
+ currentEditEventEnd: Date = new Date();
+ actions: CalendarEventAction[] = [
+ {
+ label: '<i class="fa fa-fw fa-times"></i>',
+ onClick: ({ event }: { event: CalendarEvent }): void => {
+ this.removeEvent(event);
+ }
+ }
+ ];
+ currentEditEvent: CalendarEvent;
+
+ events = new Array<CalendarEvent>();
+
+ constructor(
+ private gridMeasureService: GridMeasureService,
+ private router: Router,
+ public modeValidator: ModeValidator) { }
+
+ ngOnInit() {
+
+ const calendarStatusMainFilterItem = new StatusMainFilterItem;
+ calendarStatusMainFilterItem.isClosedStatusActive = true;
+ calendarStatusMainFilterItem.isCanceledStatusActive = false;
+ calendarStatusMainFilterItem.onlyUsersGMsDesired = false;
+
+ const calendarStatusMainFilter = new StatusMainFilter;
+ calendarStatusMainFilter.item = calendarStatusMainFilterItem;
+
+ this.gridMeasureService.getGridMeasures(calendarStatusMainFilter)
+ .subscribe(gridMeasures => {
+ const editAction = <CalendarEventAction>{
+ label: '<span class="glyphicon glyphicon-pencil"></span>',
+ onClick: ({ event }: { event: CalendarEvent }): void => {
+ this.eventClicked('edit', event);
+ }
+ };
+ const viewAction = <CalendarEventAction>{
+ label: '<span class="glyphicon glyphicon-eye-open"></span>',
+ onClick: ({ event }: { event: CalendarEvent }): void => {
+ this.eventClicked('view', event);
+ }
+ };
+ gridMeasures.forEach(gridMeasure => {
+ if (!gridMeasure.plannedStarttimeFirstSinglemeasure || !gridMeasure.endtimeGridmeasure) {
+ console.log('No valide date values for ' + gridMeasure.title);
+ console.log('Planned End Time Gridmeasure ' + gridMeasure.endtimeGridmeasure);
+ console.log('Start Time First Sequence: ' + gridMeasure.plannedStarttimeFirstSinglemeasure);
+ } else {
+ this.events.push(<CalendarEvent><GridMeasure>{
+ id: gridMeasure.id,
+ title: gridMeasure.title || 'TITLE NOT DEFINED',
+ start: new Date(gridMeasure.plannedStarttimeFirstSinglemeasure),
+ end: new Date(gridMeasure.endtimeGridmeasure),
+ color: colors.red,
+ draggable: false,
+ actions: [this.modeValidator.isEditModeAllowed(gridMeasure) ? editAction : viewAction],
+ resizable: {
+ beforeStart: false,
+ afterEnd: false
+ },
+ meta: gridMeasure
+ });
+ }
+ });
+
+ this.currDate = new Date();
+ this.showGridMeasuresIfThere();
+ });
+ }
+
+ private showGridMeasuresIfThere() {
+ const numberOfGMsInActualDay = this.events.filter(evt =>
+ (evt.start.getDate() <= this.currDate.getDate() && evt.end.getDate() >= this.currDate.getDate()));
+
+ this.activeDayIsOpen = numberOfGMsInActualDay.length > 0;
+ }
+
+ removeEvent(event: CalendarEvent) {
+ this.events = this.events.filter(iEvent => iEvent !== event);
+ }
+
+ dayClicked({ date, events }: { date: Date; events: CalendarEvent[] }): void {
+ this.currentEditEvent = null;
+ if (isSameMonth(date, this.currDate)) {
+ if (
+ (isSameDay(this.currDate, date) && this.activeDayIsOpen === true) ||
+ events.length === 0
+ ) {
+ this.activeDayIsOpen = false;
+ } else {
+ this.activeDayIsOpen = true;
+ this.currDate = date;
+ }
+ }
+ }
+
+ eventClicked(action: string, event: CalendarEvent<GridMeasure>): void {
+ const isEditModeAllowed = this.modeValidator.isEditModeAllowed(event.meta);
+ this.router.navigate(['/gridMeasureDetail/', event.id, isEditModeAllowed ? 'edit' : 'view']);
+ }
+
+ eventTimesChanged(eventchanged: CalendarEventTimesChangedEvent): void {
+ const event = <CalendarEvent><GridMeasure>eventchanged.event;
+ event.meta.starttimeFirstSequence = eventchanged.newStart;
+ event.meta.endtimeGridmeasure = eventchanged.newEnd;
+
+ event.start = eventchanged.newStart;
+ event.end = eventchanged.newEnd;
+ this.refresh.next();
+ }
+
+ addEvent(): void {
+ this.events.push({
+ title: 'New event',
+ start: startOfDay(new Date()),
+ end: endOfDay(new Date()),
+ color: colors.red,
+ draggable: true,
+ resizable: {
+ beforeStart: true,
+ afterEnd: true
+ }
+ });
+ this.refresh.next();
+ }
+}
diff --git a/src/app/custom_modules/calendar/calendar.module.ts b/src/app/custom_modules/calendar/calendar.module.ts
new file mode 100644
index 0000000..921f9aa
--- /dev/null
+++ b/src/app/custom_modules/calendar/calendar.module.ts
@@ -0,0 +1,29 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { BrowserModule } from '@angular/platform-browser';
+import { FormsModule } from '@angular/forms';
+import { Daterangepicker } from 'ng2-daterangepicker';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+
+@NgModule({
+ declarations: [],
+ imports: [
+ CommonModule,
+ Daterangepicker,
+ FormsModule,
+ BrowserModule,
+ BrowserAnimationsModule
+ ]
+})
+export class CustomCalendarModule { }
diff --git a/src/app/custom_modules/calendar/custom-date-formatter-provider.spec.ts b/src/app/custom_modules/calendar/custom-date-formatter-provider.spec.ts
new file mode 100644
index 0000000..756e633
--- /dev/null
+++ b/src/app/custom_modules/calendar/custom-date-formatter-provider.spec.ts
@@ -0,0 +1,26 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+import { CustomDateFormatter } from './custom-date-formatter-provider';
+
+describe('CustomDateFormatter', () => {
+ const customDateFormatter: CustomDateFormatter = new CustomDateFormatter();
+
+ it('should format date to week view', () => {
+ expect(customDateFormatter.weekViewTitle({date: new Date('12-12-2012'), locale: 'de'})).toBe('Kalenderwoche 50 / 2012');
+ expect(customDateFormatter.weekViewTitle({date: null, locale: 'de'})).toBe(undefined);
+ });
+
+ it('should format date to day view', () => {
+ expect(customDateFormatter.dayViewHour({date: new Date('01-01-2001 16:41'), locale: 'de'})).toBe('16:41');
+ });
+});
diff --git a/src/app/custom_modules/calendar/custom-date-formatter-provider.ts b/src/app/custom_modules/calendar/custom-date-formatter-provider.ts
new file mode 100644
index 0000000..33c5392
--- /dev/null
+++ b/src/app/custom_modules/calendar/custom-date-formatter-provider.ts
@@ -0,0 +1,32 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+******************************************************************************
+ */
+import { CalendarDateFormatter, DateFormatterParams } from 'angular-calendar';
+import { getISOWeek, getISODay } from 'date-fns';
+import { DatePipe } from '@angular/common';
+
+export class CustomDateFormatter extends CalendarDateFormatter {
+ public weekViewTitle({ date, locale }: DateFormatterParams): string {
+ if (!date) {
+ return;
+ }
+ const year: string = new DatePipe(locale).transform(date, 'y', locale);
+ const weekNumber: number = getISOWeek(date);
+ return `Kalenderwoche ${weekNumber} / ${year}`;
+ }
+
+ public dayViewHour({ date, locale }: DateFormatterParams): string {
+ return new Intl.DateTimeFormat('de', {
+ hour: 'numeric',
+ minute: 'numeric'
+ }).format(date);
+ }
+
+}
diff --git a/src/app/custom_modules/helpers/clone-grid-measure-helper.spec.ts b/src/app/custom_modules/helpers/clone-grid-measure-helper.spec.ts
new file mode 100644
index 0000000..09ed8e5
--- /dev/null
+++ b/src/app/custom_modules/helpers/clone-grid-measure-helper.spec.ts
@@ -0,0 +1,96 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { TestBed, async } from '@angular/core/testing';
+import { GridMeasure } from '../../model/grid-measure';
+import { GRIDMEASURE } from '../../test-data/grid-measures';
+import { CloneGridMeasureHelper } from '../helpers/clone-grid-measure-helper';
+import { SingleGridMeasure } from '../../model/single-grid-measure';
+
+describe('CloneGridMeasureHelper', () => {
+ const gm: GridMeasure = JSON.parse(JSON.stringify(GRIDMEASURE[0]));
+ let sgm1: SingleGridMeasure;
+ let sgm2: SingleGridMeasure;
+ let inv_sgm1: SingleGridMeasure;
+ let inv_sgm2: SingleGridMeasure;
+ let dupl_gm: GridMeasure;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ });
+ });
+
+ beforeAll(() => {
+ dupl_gm = CloneGridMeasureHelper.cloneGridMeasure(gm);
+
+ sgm1 = gm.listSingleGridmeasures[0];
+ sgm2 = gm.listSingleGridmeasures[1];
+ inv_sgm1 = CloneGridMeasureHelper.cloneSingleGridMeasure(gm, sgm1);
+ inv_sgm2 = CloneGridMeasureHelper.cloneSingleGridMeasure(gm, sgm2);
+
+
+ });
+
+ it('should create duplicated GMs correctly', async(() => {
+ expect(dupl_gm).toBeTruthy();
+ }));
+
+ it('should alter the titles correctly', async(() => {
+ expect(dupl_gm.title).toBe('Kopie von ' + gm.title);
+ }));
+
+ it('should clear all ids', async(() => {
+ expect(dupl_gm.id).toBeNull();
+ expect(dupl_gm.listSingleGridmeasures[0].id).toBeNull();
+ expect(dupl_gm.listSingleGridmeasures[0].listSteps[0].id).toBeNull();
+ }));
+
+
+ it('should create new SGMs correctly', async(() => {
+ expect(inv_sgm1).toBeTruthy();
+ expect(inv_sgm2).toBeTruthy();
+ }));
+
+ it('should alter the titles correctly', async(() => {
+ expect(inv_sgm1.title).toBe('Rückschaltung von ' + sgm1.title);
+ expect(inv_sgm2.title).toBe('Rückschaltung von ' + sgm2.title);
+ }));
+
+ xit('should reverse the steps correctly', async(() => {
+ expect(inv_sgm1.listSteps.length).toBe(sgm1.listSteps.length);
+
+ for (let i = 0; i < sgm1.listSteps.length; i++) {
+ expect(inv_sgm1.listSteps[inv_sgm1.listSteps.length - 1 - i]).toEqual(sgm1.listSteps[i]);
+ }
+
+ expect(inv_sgm2.listSteps.length).toBe(sgm2.listSteps.length);
+
+ for (let i = 0; i < sgm2.listSteps.length; i++) {
+ expect(inv_sgm2.listSteps[inv_sgm2.listSteps.length - 1 - i]).toEqual(sgm2.listSteps[i]);
+ }
+
+ }));
+
+ it('should set sort order correctly', async(() => {
+ expect(inv_sgm1.sortorder).toBe(gm.listSingleGridmeasures[gm.listSingleGridmeasures.length - 1].sortorder + 1);
+ expect(inv_sgm2.sortorder).toBe(gm.listSingleGridmeasures[gm.listSingleGridmeasures.length - 1].sortorder + 1);
+ }));
+
+ it('should remove certain values', async(() => {
+ expect(inv_sgm1.id).toBeFalsy();
+ expect(inv_sgm1.plannedEndtimeSinglemeasure).toBeFalsy();
+ expect(inv_sgm1.plannedStarttimeSinglemeasure).toBeFalsy();
+ expect(inv_sgm2.id).toBeFalsy();
+ expect(inv_sgm2.plannedEndtimeSinglemeasure).toBeFalsy();
+ expect(inv_sgm2.plannedStarttimeSinglemeasure).toBeFalsy();
+ }));
+
+});
diff --git a/src/app/custom_modules/helpers/clone-grid-measure-helper.ts b/src/app/custom_modules/helpers/clone-grid-measure-helper.ts
new file mode 100644
index 0000000..c3374ea
--- /dev/null
+++ b/src/app/custom_modules/helpers/clone-grid-measure-helper.ts
@@ -0,0 +1,89 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { SingleGridMeasure } from '../../model/single-grid-measure';
+import { GridMeasure } from '../../model/grid-measure';
+import { Globals } from '../../common/globals';
+
+export class CloneGridMeasureHelper {
+
+ static cloneGridMeasure(parentGridMeasure: GridMeasure): GridMeasure {
+
+ // use as deep copy
+ const gm: GridMeasure = JSON.parse(JSON.stringify(parentGridMeasure));
+
+ gm.id = null;
+ gm.statusId = null;
+ gm.descriptiveId = null;
+ gm.title = 'Kopie von ' + parentGridMeasure.title;
+
+ clearIdFromNewSingleGMsAndSteps(gm.listSingleGridmeasures);
+
+ return gm;
+ }
+
+ static cloneSingleGridMeasure(parentGridMeasure: GridMeasure, singleGridMeasureToCopy: SingleGridMeasure): SingleGridMeasure {
+
+ // use as deep copy
+ const singleGM: SingleGridMeasure = JSON.parse(JSON.stringify(singleGridMeasureToCopy));
+
+ // alter values according to inversion
+
+ // new inverted singleGM is ordered after the hitherto last singleGM
+ singleGM.sortorder = parentGridMeasure.listSingleGridmeasures[parentGridMeasure.listSingleGridmeasures.length - 1].sortorder + 1;
+
+ singleGM.title = singleGridMeasureToCopy.title ? 'Rückschaltung von ' + singleGridMeasureToCopy.title : 'Rückschaltung';
+
+ singleGM.id = null;
+ singleGM.plannedEndtimeSinglemeasure = null;
+ singleGM.plannedStarttimeSinglemeasure = null;
+
+ // inverted singleGM has inverted step order
+ if (singleGM.listSteps && singleGM.listSteps.length > 0) {
+ singleGM.listSteps.reverse();
+
+ reverseStepSortOrder(singleGM);
+ clearIdFromNewSteps(singleGM, false);
+ }
+
+ return singleGM;
+ }
+
+}
+
+function clearIdFromNewSingleGMsAndSteps(singleGMs: SingleGridMeasure[]) {
+ singleGMs.forEach(sgm => {
+ sgm.id = null;
+ clearIdFromNewSteps(sgm, true);
+ });
+}
+
+function clearIdFromNewSteps(singleGM: SingleGridMeasure, isDuplicate: boolean) {
+ singleGM.listSteps.forEach(step => {
+ if (isDuplicate) {
+ step.id = null;
+ } else {
+ step.id = Globals.TEMP_ID_TO_SHOW_NEW_STEPS;
+ }
+ });
+}
+
+function reverseStepSortOrder(singleGM: SingleGridMeasure) {
+ for (let i = 0; i < singleGM.listSteps.length / 2; i++) {
+ const tmpSortorder: number = singleGM.listSteps[i].sortorder;
+
+ singleGM.listSteps[i].sortorder = singleGM.listSteps[singleGM.listSteps.length - i - 1].sortorder;
+ singleGM.listSteps[singleGM.listSteps.length - i - 1].sortorder = tmpSortorder;
+
+ singleGM.listSteps[i].id = null;
+
+ }
+}
diff --git a/src/app/custom_modules/helpers/entity-validator.spec.ts b/src/app/custom_modules/helpers/entity-validator.spec.ts
new file mode 100644
index 0000000..09af8b1
--- /dev/null
+++ b/src/app/custom_modules/helpers/entity-validator.spec.ts
@@ -0,0 +1,74 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+import { TestBed } from '@angular/core/testing';
+import { EntityValidator, ValidationFunc } from './entity-validator';
+import { SessionContext } from '../../common/session-context';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+import { MessageService } from 'primeng/api';
+
+class BoolValidator extends ValidationFunc<boolean> {
+ constructor(private result: boolean) {
+ super();
+ }
+
+ public validate(b: boolean, msgService: ToasterMessageService): boolean {
+ if (!this.result) {
+ msgService.showWarn('Test message: ' + b);
+ }
+ return this.result;
+ }
+}
+
+describe('EntityValidator', () => {
+ let toasterMessageService: ToasterMessageService;
+ let sessionContext: SessionContext;
+ let messageService: MessageService;
+
+ beforeEach(() => {
+ messageService = new MessageService;
+ sessionContext = new SessionContext();
+ toasterMessageService = new ToasterMessageService(sessionContext, messageService);
+
+ TestBed.configureTestingModule({
+ });
+ });
+
+ it('should work correctly with stop on first error', (() => {
+ spyOn(toasterMessageService, 'showWarn').and.callThrough();
+ const validationList: ValidationFunc<boolean>[] = [
+ new BoolValidator(true),
+ new BoolValidator(false),
+ new BoolValidator(false)];
+
+ const validator = new EntityValidator<boolean>(toasterMessageService, validationList);
+ validator.validateEntity(true, true);
+
+ expect(toasterMessageService.showWarn).toHaveBeenCalledTimes(1);
+
+ }));
+
+
+ it('should work correctly with dont stop on first error', (() => {
+ spyOn(toasterMessageService, 'showWarn').and.callThrough();
+ const validationList: ValidationFunc<boolean>[] = [
+ new BoolValidator(true),
+ new BoolValidator(false),
+ new BoolValidator(false)];
+
+ const validator = new EntityValidator<boolean>(toasterMessageService, validationList);
+ validator.validateEntity(true, false);
+
+ expect(toasterMessageService.showWarn).toHaveBeenCalledTimes(2);
+
+ }));
+});
diff --git a/src/app/custom_modules/helpers/entity-validator.ts b/src/app/custom_modules/helpers/entity-validator.ts
new file mode 100644
index 0000000..f5811dc
--- /dev/null
+++ b/src/app/custom_modules/helpers/entity-validator.ts
@@ -0,0 +1,32 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { ToasterMessageService } from '../../services/toaster-message.service';
+
+export abstract class ValidationFunc<T> {
+ public abstract validate(item: T, messageService: ToasterMessageService): boolean;
+}
+
+export class EntityValidator<T> {
+ constructor(private messageService: ToasterMessageService,
+ private validationList: ValidationFunc<T>[]) { }
+
+ public validateEntity(entity: T, stopOnFirstError: boolean): boolean {
+ let errorOccured = false;
+ this.validationList.forEach(validator => {
+ if (!(errorOccured && stopOnFirstError)) {
+ errorOccured = !validator.validate(entity, this.messageService);
+ }
+ });
+ return !errorOccured;
+ }
+
+}
diff --git a/src/app/custom_modules/helpers/grid-measure-validator.spec.ts b/src/app/custom_modules/helpers/grid-measure-validator.spec.ts
new file mode 100644
index 0000000..dcb089e
--- /dev/null
+++ b/src/app/custom_modules/helpers/grid-measure-validator.spec.ts
@@ -0,0 +1,133 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { TestBed, async } from '@angular/core/testing';
+import { EntityValidator } from './entity-validator';
+import { GridMeasureValidatorFactory } from './grid-measure-validator';
+import { MessageServiceCustom } from '../../services/message.service';
+import { SessionContext } from '../../common/session-context';
+import { GridMeasure } from '../../model/grid-measure';
+import { GRIDMEASURE } from '../../test-data/grid-measures';
+import { SingleGridMeasure } from '../../model/single-grid-measure';
+import { MessageService } from 'primeng/api';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+
+describe('GridMeasureValidator', () => {
+ let toasterMessageService: ToasterMessageService;
+ let sessionContext: SessionContext;
+ let messageService: MessageService;
+
+ beforeEach(() => {
+ messageService = new MessageService;
+ sessionContext = new SessionContext();
+ toasterMessageService = new ToasterMessageService(sessionContext, messageService);
+
+ TestBed.configureTestingModule({
+ });
+ });
+
+ it('should check PlannedGridMeasureDateValidator correctly', async(() => {
+ spyOn(toasterMessageService, 'showWarn').and.callThrough();
+ const gm1: GridMeasure = JSON.parse(JSON.stringify(GRIDMEASURE[0]));
+ const gm2: GridMeasure = JSON.parse(JSON.stringify(GRIDMEASURE[0]));
+ gm1.plannedStarttimeFirstSinglemeasure = '2016-01-15T11:11:00z';
+ gm1.endtimeGridmeasure = '2017-01-15T11:11:00z';
+ gm2.plannedStarttimeFirstSinglemeasure = '2017-01-15T11:11:00z';
+ gm2.endtimeGridmeasure = '2016-01-15T11:11:00z';
+
+
+ const entityValidator: EntityValidator<GridMeasure> = GridMeasureValidatorFactory.createGM(toasterMessageService);
+
+ entityValidator.validateEntity(gm1, true);
+ expect(toasterMessageService.showWarn).not.toHaveBeenCalled();
+
+ gm1.plannedStarttimeFirstSinglemeasure = gm1.endtimeGridmeasure;
+ entityValidator.validateEntity(gm1, true);
+ expect(toasterMessageService.showWarn).not.toHaveBeenCalled();
+
+ entityValidator.validateEntity(gm2, true);
+ expect(toasterMessageService.showWarn).toHaveBeenCalled();
+
+ gm1.plannedStarttimeFirstSinglemeasure = null;
+ gm1.endtimeGridmeasure = '2017-01-15T11:11:00z';
+ expect(entityValidator.validateEntity(gm1, true)).toBeTruthy();
+
+
+ gm1.plannedStarttimeFirstSinglemeasure = '';
+ gm1.endtimeGridmeasure = '2017-01-15T11:11:00z';
+ expect(entityValidator.validateEntity(gm1, true)).toBeTruthy();
+
+ gm1.plannedStarttimeFirstSinglemeasure = '2017-01-15T11:11:00z';
+ gm1.endtimeGridmeasure = null;
+ expect(entityValidator.validateEntity(gm1, true)).toBeTruthy();
+
+
+ gm1.plannedStarttimeFirstSinglemeasure = '2017-01-15T11:11:00z';
+ gm1.endtimeGridmeasure = '';
+ expect(entityValidator.validateEntity(gm1, true)).toBeTruthy();
+
+ gm1.plannedStarttimeFirstSinglemeasure = '';
+ gm1.endtimeGridmeasure = '';
+ expect(entityValidator.validateEntity(gm1, true)).toBeTruthy();
+
+ }));
+
+ it('should check SingleGridMeasureDateValidator correctly', async(() => {
+ spyOn(toasterMessageService, 'showWarn').and.callThrough();
+ const gm1: GridMeasure = JSON.parse(JSON.stringify(GRIDMEASURE[0]));
+ const gm2: GridMeasure = JSON.parse(JSON.stringify(GRIDMEASURE[0]));
+ gm1.listSingleGridmeasures[0].plannedStarttimeSinglemeasure = '2016-01-15T11:11:00z';
+ gm1.listSingleGridmeasures[0].plannedEndtimeSinglemeasure = '2017-01-15T11:11:00z';
+ gm2.listSingleGridmeasures[0].plannedStarttimeSinglemeasure = '2017-01-15T11:11:00z';
+ gm2.listSingleGridmeasures[0].plannedEndtimeSinglemeasure = '2016-01-15T11:11:00z';
+
+
+ const entityValidator2: EntityValidator<SingleGridMeasure> = GridMeasureValidatorFactory.createsingleGM(toasterMessageService);
+
+ entityValidator2.validateEntity(gm1.listSingleGridmeasures[0], true);
+ expect(toasterMessageService.showWarn).not.toHaveBeenCalled();
+
+ gm1.listSingleGridmeasures[0].plannedStarttimeSinglemeasure = gm1.listSingleGridmeasures[0].plannedEndtimeSinglemeasure;
+ entityValidator2.validateEntity(gm1.listSingleGridmeasures[0], true);
+ expect(toasterMessageService.showWarn).not.toHaveBeenCalled();
+
+ entityValidator2.validateEntity(gm2.listSingleGridmeasures[0], true);
+ expect(toasterMessageService.showWarn).toHaveBeenCalled();
+
+ gm1.listSingleGridmeasures[0].plannedStarttimeSinglemeasure = null;
+ gm1.listSingleGridmeasures[0].plannedEndtimeSinglemeasure = '2017-01-15T11:11:00z';
+ expect(entityValidator2.validateEntity(gm1.listSingleGridmeasures[0], true)).toBeTruthy();
+
+
+ gm1.listSingleGridmeasures[0].plannedStarttimeSinglemeasure = '';
+ gm1.listSingleGridmeasures[0].plannedEndtimeSinglemeasure = '2017-01-15T11:11:00z';
+ expect(entityValidator2.validateEntity(gm1.listSingleGridmeasures[0], true)).toBeTruthy();
+
+ gm1.listSingleGridmeasures[0].plannedStarttimeSinglemeasure = '2017-01-15T11:11:00z';
+ gm1.listSingleGridmeasures[0].plannedEndtimeSinglemeasure = null;
+ expect(entityValidator2.validateEntity(gm1.listSingleGridmeasures[0], true)).toBeTruthy();
+
+
+ gm1.listSingleGridmeasures[0].plannedStarttimeSinglemeasure = '2017-01-15T11:11:00z';
+ gm1.listSingleGridmeasures[0].plannedEndtimeSinglemeasure = '';
+ expect(entityValidator2.validateEntity(gm1.listSingleGridmeasures[0], true)).toBeTruthy();
+
+ gm1.listSingleGridmeasures[0].plannedStarttimeSinglemeasure = '';
+ gm1.listSingleGridmeasures[0].plannedEndtimeSinglemeasure = '';
+ expect(entityValidator2.validateEntity(gm1.listSingleGridmeasures[0], true)).toBeTruthy();
+
+ gm1.listSingleGridmeasures[0].plannedStarttimeSinglemeasure = '2017-01-15T11:11:00z';
+ gm1.listSingleGridmeasures[0].plannedEndtimeSinglemeasure = '2017-01-15T10:11:00z';
+ entityValidator2.validateEntity(gm1.listSingleGridmeasures[0], false);
+ expect(toasterMessageService.showWarn).toHaveBeenCalled();
+ }));
+
+});
diff --git a/src/app/custom_modules/helpers/grid-measure-validator.ts b/src/app/custom_modules/helpers/grid-measure-validator.ts
new file mode 100644
index 0000000..85cc038
--- /dev/null
+++ b/src/app/custom_modules/helpers/grid-measure-validator.ts
@@ -0,0 +1,61 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { EntityValidator, ValidationFunc } from './entity-validator';
+import { GridMeasure } from '../../model/grid-measure';
+import { SingleGridMeasure } from '../../model/single-grid-measure';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+
+class PlannedGridMeasureDateValidator extends ValidationFunc<GridMeasure> {
+ validate(gridMeasure: GridMeasure, messageService: ToasterMessageService): boolean {
+ const startDate = new Date(gridMeasure.plannedStarttimeFirstSinglemeasure || '1990-01-01T00:00:00z');
+ const endDate = new Date(gridMeasure.endtimeGridmeasure || '2199-01-01T00:00:00z');
+ if (startDate.getTime() > endDate.getTime()) {
+ messageService.showWarn('Beginn der ersten geplanten Einzelmaßnahme ' +
+ 'muss vor Ende der Netzmaßnahme liegen');
+ return false;
+ }
+ return true;
+ }
+}
+
+class SingleGridMeasureDateValidator extends ValidationFunc<SingleGridMeasure> {
+ validate(singleGridMeasure: SingleGridMeasure, messageService: ToasterMessageService): boolean {
+ const startDate = new Date(singleGridMeasure.plannedStarttimeSinglemeasure || '1990-01-01T00:00:00z');
+ const endDate = new Date(singleGridMeasure.plannedEndtimeSinglemeasure || '2199-01-01T00:00:00z');
+ if (startDate.getTime() > endDate.getTime()) {
+ messageService.showWarn('Beginn der geplanten Einzelmaßnahme ' +
+ 'muss vor Ende der geplanten Einzelmaßnahme liegen');
+ return false;
+ }
+ return true;
+ }
+}
+
+// Add more Validators here
+export class GridMeasureValidatorFactory {
+ public static createGM(messageService: ToasterMessageService) {
+ return new EntityValidator<GridMeasure>(messageService,
+ [
+ new PlannedGridMeasureDateValidator(),
+ // , new ... -> add more Validators here
+ ]);
+ }
+
+ public static createsingleGM(messageService: ToasterMessageService) {
+ return new EntityValidator<SingleGridMeasure>(messageService,
+ [
+ new SingleGridMeasureDateValidator()
+ // , new ... -> add more Validators here
+ ]);
+ }
+}
+
diff --git a/src/app/custom_modules/helpers/mode-validator.spec.ts b/src/app/custom_modules/helpers/mode-validator.spec.ts
new file mode 100644
index 0000000..abc1136
--- /dev/null
+++ b/src/app/custom_modules/helpers/mode-validator.spec.ts
@@ -0,0 +1,165 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { TestBed, inject } from '@angular/core/testing';
+import { RoleAccessHelperService } from '../../services/jobs/role-access-helper.service';
+import { Router, ActivatedRoute } from '@angular/router';
+import { SessionContext } from '../../common/session-context';
+import { ModeValidator } from './mode-validator';
+import { ActivatedRouteStub, RouterStub } from '../../testing';
+import { GRIDMEASURE } from '../../test-data/grid-measures';
+import { GridMeasure } from '../../model/grid-measure';
+import { Globals } from '../../common/globals';
+import { USERS } from '../../test-data/users';
+import { RoleAccess } from '../../model/role-access';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+
+
+describe('ModeValidator', () => {
+ let sessionContext: SessionContext;
+ let roleAccessHelper: RoleAccessHelperService;
+ let activatedStub: ActivatedRouteStub;
+ let routerStub: RouterStub;
+ const gridmeasures: GridMeasure[] = JSON.parse(JSON.stringify(GRIDMEASURE));
+
+ routerStub = {
+ navigate: jasmine.createSpy('navigate').and.callThrough()
+ };
+
+ beforeEach(() => {
+ sessionContext = new SessionContext();
+ roleAccessHelper = new RoleAccessHelperService();
+ activatedStub = new ActivatedRouteStub();
+ const roleAcess: RoleAccess = {
+ editRoles: [{
+ name: 'planned-policies-measureplanner',
+ gridMeasureStatusIds: [
+ 0,
+ 1
+ ]
+ }, {
+ name: 'planned-policies-superuser',
+ gridMeasureStatusIds: [
+ 0,
+ 1
+ ]
+ }, {
+ name: 'planned-policies-measureapplicant',
+ gridMeasureStatusIds: [
+ 0,
+ 1
+ ]
+ }],
+ controls: [{
+ gridMeasureStatusId: 0,
+ activeButtons: [
+ 'save',
+ 'apply',
+ 'cancel'
+ ],
+ inactiveFields: [
+ 'titeldermassnahme'
+ ]
+ },
+ {
+ gridMeasureStatusId: 1,
+ activeButtons: [
+ 'save',
+ 'cancel',
+ 'forapproval'
+ ],
+ inactiveFields: [
+ 'titeldermassnahme'
+ ]
+ }],
+ stornoSection:
+ {
+ 'stornoRoles': [
+ 'planned-policies-measureapplicant',
+ 'planned-policies-measureplanner',
+ 'planned-policies-measureapprover',
+ 'planned-policies-requester',
+ 'planned-policies-clearance'
+ ]
+ },
+ duplicateSection:
+ {
+ 'duplicateRoles': [
+ 'planned-policies-measureapplicant'
+ ]
+ }
+ };
+ roleAccessHelper.init(roleAcess);
+
+
+ TestBed.configureTestingModule({
+ providers: [ModeValidator,
+ SessionContext,
+ { provide: RoleAccessHelperService, useValue: roleAccessHelper },
+ { provide: ActivatedRoute, useValue: activatedStub },
+ { provide: Router, useValue: routerStub },
+ { provide: ToasterMessageService }]
+ });
+ });
+
+ it('can instatiate service when inject service', inject([ModeValidator], (service: ModeValidator) => {
+ expect(service instanceof ModeValidator);
+ }));
+
+ it('should handle grid measure detail view mode', inject([ModeValidator], (modeValidator: ModeValidator) => {
+ modeValidator.handleGridMeasureMode(gridmeasures[0], Globals.MODE.VIEW);
+ expect(routerStub.navigate).toHaveBeenCalledWith(['/gridMeasureDetail/', gridmeasures[0].id, Globals.MODE.VIEW]);
+ }));
+
+ it('should handle grid measure detail edit mode', inject([ModeValidator], (modeValidator: ModeValidator) => {
+ modeValidator.handleGridMeasureMode(gridmeasures[0], Globals.MODE.EDIT);
+ expect(routerStub.navigate).toHaveBeenCalledWith(['/gridMeasureDetail/', gridmeasures[0].id, Globals.MODE.EDIT]);
+ }));
+
+ it('should handle grid measure detail cancel mode', inject([ModeValidator], (modeValidator: ModeValidator) => {
+ modeValidator.handleGridMeasureMode(gridmeasures[0], Globals.MODE.CANCEL);
+ expect(routerStub.navigate).toHaveBeenCalledWith(['/gridMeasureDetail/', gridmeasures[0].id, '#', Globals.MODE.CANCEL]);
+ }));
+
+ it('should handle grid measure detail wrong mode', inject([ModeValidator], (modeValidator: ModeValidator) => {
+ modeValidator.handleGridMeasureMode(gridmeasures[0], 'wrongmode');
+ expect(routerStub.navigate).toHaveBeenCalledWith(['/overview']);
+ }));
+
+ it('should check if edit mode is allowed for superuser', inject([ModeValidator], (modeValidator: ModeValidator) => {
+ sessionContext.setCurrUser(USERS[1]);
+ modeValidator.isEditModeAllowed(gridmeasures[0]);
+ expect(modeValidator.isEditModeAllowed).toBeTruthy();
+
+ }));
+
+ it('should check if edit mode is allowed for planner and status new', inject([ModeValidator], (modeValidator: ModeValidator) => {
+ sessionContext.setCurrUser(USERS[0]);
+ modeValidator.isEditModeAllowed(gridmeasures[0]);
+ expect(modeValidator.isEditModeAllowed).toBeTruthy();
+
+ }));
+
+ it('should check if email edit mode allowed for role and status new', inject([ModeValidator], (modeValidator: ModeValidator) => {
+ sessionContext.setCurrUser(USERS[0]);
+ modeValidator.isEmailEditModeAllowed(gridmeasures[0]);
+ expect(modeValidator.isEmailEditModeAllowed).toBeTruthy();
+
+ }));
+
+ it('should check if cancel mode allowed for role and status', inject([ModeValidator], (modeValidator: ModeValidator) => {
+ sessionContext.setCurrUser(USERS[0]);
+ modeValidator.isCancelAllowed(gridmeasures[0]);
+ expect(modeValidator.isCancelAllowed).toBeTruthy();
+
+ }));
+
+});
diff --git a/src/app/custom_modules/helpers/mode-validator.ts b/src/app/custom_modules/helpers/mode-validator.ts
new file mode 100644
index 0000000..161a825
--- /dev/null
+++ b/src/app/custom_modules/helpers/mode-validator.ts
@@ -0,0 +1,84 @@
+
+
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+******************************************************************************
+ */
+import { GridMeasure } from '../../model/grid-measure';
+import { RoleAccessHelperService } from '../../services/jobs/role-access-helper.service';
+import { Router } from '@angular/router';
+import { SessionContext } from '../../common/session-context';
+import { Globals } from '../../common/globals';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class ModeValidator {
+
+ constructor(public sessionContext: SessionContext,
+ public roleAccessHelper: RoleAccessHelperService,
+ public router: Router) { }
+
+ handleGridMeasureMode(gridmeasure: GridMeasure, mode: string) {
+ switch (mode) {
+ case Globals.MODE.VIEW:
+ this.router.navigate(['/gridMeasureDetail/', gridmeasure.id, Globals.MODE.VIEW]);
+ break;
+ case Globals.MODE.EDIT:
+ this.router.navigate(['/gridMeasureDetail/', gridmeasure.id, Globals.MODE.EDIT]);
+ break;
+ case Globals.MODE.CANCEL:
+ // this.messageService.clearBannerLocalEvent$.emit(true);
+ this.sessionContext.setGridMeasureDetail(gridmeasure);
+ this.sessionContext.setCancelStage(true);
+ this.router.navigate(['/gridMeasureDetail/', gridmeasure.id, '#', Globals.MODE.CANCEL]);
+ break;
+ default:
+ this.router.navigate(['/overview']);
+ break;
+ }
+ }
+
+ isEditModeAllowed(gridmeasure: GridMeasure): boolean {
+ const user = this.sessionContext.getCurrUser();
+ return (user.roles.includes(Globals.OAUTH2CONF_SUPERUSER_ROLE) ||
+ this.roleAccessHelper.editPossibleForRoles(user.roles, gridmeasure.statusId))
+ && gridmeasure.statusId !== Globals.STATUS.CLOSED;
+ }
+
+ isEmailEditModeAllowed(gridmeasure: GridMeasure): boolean {
+ const user = this.sessionContext.getCurrUser();
+ return this.isEditModeAllowed(gridmeasure) &&
+ (user.roles.includes(Globals.OAUTH2CONF_SUPERUSER_ROLE) ||
+ user.roles.includes(Globals.OAUTH2CONF_MEASUREAPPLICANT_ROLE) ||
+ user.roles.includes(Globals.OAUTH2CONF_MEASUREPLANNER_ROLE));
+ }
+
+ isCancelAllowed(gridmeasure: GridMeasure): boolean {
+ const currRoles = this.sessionContext.getCurrUser().roles;
+ const stornoSection = this.roleAccessHelper.getRoleAccessDefinitions().stornoSection;
+ let cancelAllowedForRole = false;
+ if (!stornoSection) {
+ return;
+ }
+ stornoSection.stornoRoles.forEach(allowedRole => {
+ if (currRoles.includes(allowedRole)) {
+ cancelAllowedForRole = true;
+ return;
+ }
+ });
+ const status = gridmeasure.statusId;
+ let cancelAllowedForStatus = false;
+ if (status === Globals.STATUS.NEW || status === Globals.STATUS.APPLIED || status === Globals.STATUS.APPROVED
+ || status === Globals.STATUS.FORAPPROVAL || status === Globals.STATUS.REQUESTED) {
+ cancelAllowedForStatus = true;
+ }
+ return cancelAllowedForRole && cancelAllowedForStatus;
+ }
+
+}
diff --git a/src/app/custom_modules/routing/app-routing.module.spec.ts b/src/app/custom_modules/routing/app-routing.module.spec.ts
new file mode 100644
index 0000000..cc36d6d
--- /dev/null
+++ b/src/app/custom_modules/routing/app-routing.module.spec.ts
@@ -0,0 +1,27 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+******************************************************************************
+ */
+import { TestBed } from '@angular/core/testing';
+import { AppRoutingModule } from './app-routing.module';
+
+describe('Router: App', () => {
+
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [
+ AppRoutingModule
+ ]
+ });
+ });
+
+
+});
+
diff --git a/src/app/custom_modules/routing/app-routing.module.ts b/src/app/custom_modules/routing/app-routing.module.ts
new file mode 100644
index 0000000..89e13f2
--- /dev/null
+++ b/src/app/custom_modules/routing/app-routing.module.ts
@@ -0,0 +1,74 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { OverviewComponent } from '../../pages/overview/overview.component';
+import { LogoutPageComponent } from '../../pages/logout/logout.component';
+import { LoggedoutPageComponent } from '../../pages/loggedout/loggedout.component';
+import { GridMeasureDetailComponent } from '../../pages/grid-measure-detail/grid-measure-detail.component';
+import { AuthGuard } from '../../services/auth-guard.service';
+import { GridConfigModifierComponent } from '../../pages/grid-config-modifier/grid-config-modifier.component';
+import { CancelGridMeasureComponent } from '../../pages/cancel-grid-measure/cancel-grid-measure.component';
+
+const ROUTES: Routes = [
+ {
+ path: '',
+ redirectTo: '/overview',
+ pathMatch: 'full'
+ },
+ {
+ path: 'overview',
+ component: OverviewComponent,
+ canActivate: [AuthGuard]
+ },
+ {
+ path: 'logout',
+ component: LogoutPageComponent,
+ canActivate: [AuthGuard]
+ },
+ {
+ path: 'loggedout',
+ component: LoggedoutPageComponent
+ },
+ {
+ path: 'gridMeasureDetail',
+ component: GridMeasureDetailComponent,
+ canActivate: [AuthGuard]
+ },
+ {
+ path: 'gridMeasureDetail/:id/:mode',
+ component: GridMeasureDetailComponent
+ },
+ {
+ path: 'modifyGridConfig',
+ component: GridConfigModifierComponent,
+ canActivate: [AuthGuard]
+ },
+ {
+ path: 'gridMeasureDetail/:id/#/cancel',
+ component: CancelGridMeasureComponent,
+ canActivate: [AuthGuard]
+ },
+ {
+ path: '**',
+ redirectTo: '/overview'
+ }
+
+];
+
+@NgModule({
+ imports: [
+ RouterModule.forRoot(ROUTES, { useHash: true })
+ ],
+ exports: [RouterModule]
+})
+export class AppRoutingModule { }
diff --git a/src/app/custom_modules/undo-redo-stack/commands/base-command.ts b/src/app/custom_modules/undo-redo-stack/commands/base-command.ts
new file mode 100644
index 0000000..18dc6d8
--- /dev/null
+++ b/src/app/custom_modules/undo-redo-stack/commands/base-command.ts
@@ -0,0 +1,22 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Command } from './command';
+
+export abstract class BaseCommand implements Command {
+ abstract undo();
+ abstract redo();
+
+ mergePossible( newerCommand: Command ): boolean {
+ return false;
+ }
+ abstract merge( newerCommand: Command): Command;
+}
diff --git a/src/app/custom_modules/undo-redo-stack/commands/command.ts b/src/app/custom_modules/undo-redo-stack/commands/command.ts
new file mode 100644
index 0000000..8898d03
--- /dev/null
+++ b/src/app/custom_modules/undo-redo-stack/commands/command.ts
@@ -0,0 +1,17 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+export interface Command {
+ undo();
+ redo();
+ mergePossible(newerCommand: Command): boolean;
+ merge(newerCommand: Command): Command;
+}
diff --git a/src/app/custom_modules/undo-redo-stack/commands/simple-field-command.spec.ts b/src/app/custom_modules/undo-redo-stack/commands/simple-field-command.spec.ts
new file mode 100644
index 0000000..4f6ef0c
--- /dev/null
+++ b/src/app/custom_modules/undo-redo-stack/commands/simple-field-command.spec.ts
@@ -0,0 +1,116 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { TestBed, inject } from '@angular/core/testing';
+import { SimpleFieldCommand } from './simple-field-command';
+import { BaseCommand } from './base-command';
+import { Command } from './command';
+
+
+class TestModel {
+ public stringField = 'STRINga';
+ public numField = 776655;
+ public boolField = true;
+
+}
+class TestCommand extends BaseCommand {
+ undo() {
+ }
+ redo() {
+ }
+ merge(newerCommand: Command): Command {
+ return null;
+ }
+}
+
+describe('SimpleFieldCommand', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ });
+ });
+
+ it('should work with strings', (() => {
+ const model = new TestModel();
+
+ const cmdString1 = new SimpleFieldCommand(model, 'stringField', 'stringy', 'STRINg');
+ const cmdString2 = new SimpleFieldCommand(model, 'stringField', 'STRINg', 'STRINga');
+
+ expect(cmdString1.mergePossible(cmdString2)).toBe(true);
+ cmdString1.merge(cmdString2);
+
+ cmdString1.undo();
+
+ expect(model.stringField).toBe( 'stringy' );
+
+ cmdString1.redo();
+
+ expect(model.stringField).toBe( 'STRINga' );
+
+
+
+ }));
+ it('should work with nums', (() => {
+ const model = new TestModel();
+
+
+ const cmdNum1 = new SimpleFieldCommand(model, 'numField', 112233, 776644 );
+ const cmdNum2 = new SimpleFieldCommand(model, 'numField', 776644, 776655 );
+
+ expect(cmdNum1.mergePossible(cmdNum2)).toBe(true);
+ cmdNum1.merge(cmdNum2);
+
+ cmdNum1.undo();
+
+ expect(model.numField).toBe( 112233 );
+
+ cmdNum1.redo();
+
+ expect(model.numField).toBe( 776655 );
+
+ }));
+
+ it('should work with bool', (() => {
+ const model = new TestModel();
+
+
+ const cmdNum1 = new SimpleFieldCommand(model, 'boolField', true, false );
+ const cmdNum2 = new SimpleFieldCommand(model, 'boolField', false, true );
+
+ expect(cmdNum1.mergePossible(cmdNum2)).toBe(true);
+ cmdNum1.merge(cmdNum2);
+
+ cmdNum1.undo();
+
+ expect(model.boolField).toBe( true );
+
+ cmdNum1.redo();
+
+ expect(model.boolField).toBe( true );
+
+ }));
+
+ it('should reject invalid commands for merge', (() => {
+ const model = new TestModel();
+ const model2 = new TestModel();
+
+ const cmdString1 = new SimpleFieldCommand(model, 'stringField', 'bruno', 'haferkamp');
+ const cmdString2 = new SimpleFieldCommand(model2, 'stringField', 'claudio', 'haysterkamp');
+ const cmdNum3 = new SimpleFieldCommand(model2, 'numField', 123, 345);
+
+ expect(cmdString1.mergePossible(cmdString2)).toBe(false);
+ expect(cmdString1.mergePossible(cmdNum3)).toBe(false);
+ expect(cmdString1.mergePossible(new TestCommand())).toBe(false);
+
+ expect( function() { cmdString1.merge(new TestCommand); }).toThrowError();
+ }));
+});
+
+
diff --git a/src/app/custom_modules/undo-redo-stack/commands/simple-field-command.ts b/src/app/custom_modules/undo-redo-stack/commands/simple-field-command.ts
new file mode 100644
index 0000000..b1fc24a
--- /dev/null
+++ b/src/app/custom_modules/undo-redo-stack/commands/simple-field-command.ts
@@ -0,0 +1,48 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Command } from './command';
+import { BaseCommand } from './base-command';
+
+export class SimpleFieldCommand extends BaseCommand {
+ constructor(
+ private model: any,
+ private field: string,
+ private oldval: any,
+ private newval: any) {
+ super();
+ }
+ public undo() {
+ this.model[this.field] = this.oldval;
+ }
+
+ public redo() {
+ this.model[this.field] = this.newval;
+ }
+
+ public mergePossible( newerCommand: Command ): boolean {
+ if ( newerCommand instanceof SimpleFieldCommand ) {
+ const newCmd: SimpleFieldCommand = newerCommand;
+ return this.model === newCmd.model && this.field === newCmd.field;
+ }
+ return false;
+ }
+ public merge( newerCommand: Command): Command {
+ if ( newerCommand instanceof SimpleFieldCommand ) {
+ const newCmd: SimpleFieldCommand = newerCommand;
+ this.newval = newCmd.newval;
+
+ return this;
+ }
+ throw new TypeError( 'Invalid merge call!');
+ }
+
+}
diff --git a/src/app/custom_modules/undo-redo-stack/undo-redo-stack.component.ts b/src/app/custom_modules/undo-redo-stack/undo-redo-stack.component.ts
new file mode 100644
index 0000000..babe042
--- /dev/null
+++ b/src/app/custom_modules/undo-redo-stack/undo-redo-stack.component.ts
@@ -0,0 +1,62 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Component } from '@angular/core';
+import { Command } from './commands/command';
+
+export class UndoRedoStackComponent {
+ private stack: Command[] = [];
+ private pointer = -1;
+ private limit = -1;
+
+ constructor(isMergeable: boolean = true) {
+ this.clear();
+ }
+
+ public push(cmd: Command) {
+ if (this.pointer >= 0 && this.stack[this.pointer].mergePossible(cmd)) {
+ this.stack[this.pointer].merge(cmd);
+ } else {
+ this.pointer++;
+ this.limit = this.pointer;
+ this.stack[this.pointer] = cmd;
+ }
+ }
+
+ public undo() {
+ if (this.isUndoPossible()) {
+ this.stack[this.pointer].undo();
+ this.pointer--;
+ }
+ }
+
+ public redo() {
+ if (this.isRedoPossible()) {
+ this.pointer++;
+ this.stack[this.pointer].redo();
+ }
+ }
+
+ public isUndoPossible() {
+ return this.pointer >= 0;
+ }
+
+ public isRedoPossible() {
+ return this.pointer < this.limit;
+ }
+
+ public clear() {
+ this.stack = [];
+ this.pointer = -1;
+ this.limit = -1;
+ }
+
+}
diff --git a/src/app/custom_modules/undo-redo-stack/undo-redo-stack.module.ts b/src/app/custom_modules/undo-redo-stack/undo-redo-stack.module.ts
new file mode 100644
index 0000000..25d8fe0
--- /dev/null
+++ b/src/app/custom_modules/undo-redo-stack/undo-redo-stack.module.ts
@@ -0,0 +1,21 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+@NgModule({
+ imports: [
+ CommonModule
+ ],
+ declarations: []
+})
+export class UndoRedoStackModule { }
diff --git a/src/app/custom_modules/undo-redo-stack/undo-redo-stack.spec.ts b/src/app/custom_modules/undo-redo-stack/undo-redo-stack.spec.ts
new file mode 100644
index 0000000..a23f3a8
--- /dev/null
+++ b/src/app/custom_modules/undo-redo-stack/undo-redo-stack.spec.ts
@@ -0,0 +1,110 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { TestBed, inject } from '@angular/core/testing';
+import { BaseCommand } from './commands/base-command';
+import { Command } from './commands/command';
+import { UndoRedoStackComponent } from './undo-redo-stack.component';
+
+class TestCommand extends BaseCommand {
+ undoCalled = 0;
+ redoCalled = 0;
+ mergePoss = false;
+ mergeCalled = 0;
+
+ undo() {
+ this.undoCalled += 1;
+ }
+ redo() {
+ this.redoCalled += 1;
+ }
+ mergePossible(newerCommand: Command): boolean {
+ super.mergePossible(newerCommand); // for test coverage!
+ return this.mergePoss;
+ }
+ merge(newerCommand: Command) {
+ if (this.mergePossible(null)) {
+ this.mergeCalled += 1;
+ }
+ return this;
+ }
+}
+
+describe('UndoRedoStack', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ });
+ });
+
+ it('should be createable', (() => {
+ const stack = new UndoRedoStackComponent();
+ expect(stack).not.toBeNull();
+ expect(stack.isRedoPossible()).toBeFalsy();
+ expect(stack.isUndoPossible()).toBeFalsy();
+ }));
+
+ it('behave correctly', (() => {
+ const stack = new UndoRedoStackComponent();
+ stack.undo(); // does nothing when stack is empty
+ stack.redo(); // does nothing when stack is empty
+
+ const testCommand1 = new TestCommand();
+ const testCommand2 = new TestCommand();
+ const testCommand3 = new TestCommand();
+ const testCommand4 = new TestCommand();
+ const testCommand5 = new TestCommand();
+
+
+ stack.push(testCommand1);
+ stack.push(testCommand2);
+ stack.push(testCommand3);
+
+ testCommand3.mergePoss = true;
+ stack.push(testCommand4);
+
+ testCommand3.mergePoss = false;
+ stack.push(testCommand5);
+
+ stack.undo();
+ stack.undo();
+
+ stack.redo();
+ stack.redo();
+ stack.redo();
+
+ stack.undo();
+ stack.undo();
+ stack.undo();
+ stack.undo();
+ stack.undo();
+ stack.undo();
+
+ expect(testCommand1.undoCalled).toBe(1);
+ expect(testCommand2.undoCalled).toBe(1);
+ expect(testCommand3.undoCalled).toBe(2);
+ expect(testCommand4.undoCalled).toBe(0);
+ expect(testCommand5.undoCalled).toBe(2);
+
+ expect(testCommand1.redoCalled).toBe(0);
+ expect(testCommand2.redoCalled).toBe(0);
+ expect(testCommand3.redoCalled).toBe(1);
+ expect(testCommand4.redoCalled).toBe(0);
+ expect(testCommand5.redoCalled).toBe(1);
+
+ const testCommand6 = new TestCommand();
+ stack.redo();
+ stack.push(testCommand6);
+
+ const stackUncovered: any = stack;
+ expect(stackUncovered.pointer).toBe(1);
+ expect(stackUncovered.limit).toBe(1);
+ }));
+});
diff --git a/src/app/lists/common-components/abstract-list/abstract-list.component.css b/src/app/lists/common-components/abstract-list/abstract-list.component.css
new file mode 100644
index 0000000..5e32c2a
--- /dev/null
+++ b/src/app/lists/common-components/abstract-list/abstract-list.component.css
@@ -0,0 +1,57 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+table {
+ width: 100%;
+}
+
+td {
+ vertical-align: middle;
+ padding: 0px 0px 0px 0px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.input-group {
+ width: 20%;
+}
+
+th {
+ padding: 0pc 2px 0px 2px;
+}
+
+.grid-measure-tab-moduser-header,
+.grid-measure-tab-moduser {
+ width: 1%;
+ text-align: left;
+}
+
+.grid-measure-tab-moduser {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.grid-measure-tab-status-header,
+.grid-measure-tab-status {
+ width: 7%;
+ min-width: 15%;
+ max-width: 15%;
+ text-align: center;
+}
+
+.breakHeaderCol {
+ word-wrap: break-word;
+ /* All browsers since IE 5.5+ */
+ overflow-wrap: break-word;
+}
diff --git a/src/app/lists/common-components/abstract-list/abstract-list.component.html b/src/app/lists/common-components/abstract-list/abstract-list.component.html
new file mode 100644
index 0000000..ef2876a
--- /dev/null
+++ b/src/app/lists/common-components/abstract-list/abstract-list.component.html
@@ -0,0 +1,15 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+-->
+
+<p>
+ abstract-list works!
+</p>
diff --git a/src/app/lists/common-components/abstract-list/abstract-list.component.spec.ts b/src/app/lists/common-components/abstract-list/abstract-list.component.spec.ts
new file mode 100644
index 0000000..5451810
--- /dev/null
+++ b/src/app/lists/common-components/abstract-list/abstract-list.component.spec.ts
@@ -0,0 +1,340 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { async, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing';
+import { AbstractMockObservableService } from '../../../testing/abstract-mock-observable.service';
+import { AbstractListComponent } from './abstract-list.component';
+import { FormsModule } from '@angular/forms';
+import { StringToDatePipe } from '../../../common-components/pipes/string-to-date.pipe';
+import { FormattedDatePipe } from '../../../common-components/pipes/formatted-date.pipe';
+import { FormattedTimestampPipe } from '../../../common-components/pipes/formatted-timestamp.pipe';
+import { SessionContext } from '../../../common/session-context';
+import { DaterangepickerConfig } from 'ng2-daterangepicker';
+import { MockComponent } from '../../../testing/mock.component';
+import { GridMeasureService } from '../../../services/grid-measure.service';
+import { ReminderService } from '../../../services/reminder.service';
+import { LockService } from '../../../services/lock.service';
+import { Router } from '@angular/router';
+import { UserSettingsService } from '../../../services/user-settings.service';
+import { UserSettings } from '../../../model/user-settings';
+import { USERS } from '../../../test-data/users';
+import { RoleAccessHelperService } from '../../../services/jobs/role-access-helper.service';
+import { ModeValidator } from '../../../custom_modules/helpers/mode-validator';
+import { OnInit, Component } from '../../../../../node_modules/@angular/core';
+import { Globals } from './../../../common/globals';
+import { GridOptions } from 'ag-grid/dist/lib/entities/gridOptions';
+import { ToasterMessageService } from '../../../services/toaster-message.service';
+import { MessageService } from 'primeng/api';
+import { MessageServiceCustom } from '../../../services/message.service';
+
+export class AbstractListMocker {
+ public static getComponentMocks() {
+ return [
+ MockComponent({
+ selector: 'app-abstract-list',
+ inputs: ['withCheckboxes', 'withEditButtons', 'isCollapsible', 'stayCollapsedGridMeasures', 'gridId', 'enforceShowReadOnly']
+ })
+ ];
+ }
+}
+
+class MockUserSettingService extends AbstractMockObservableService {
+ savedUserSettings: UserSettings;
+ getUserSettings(gridId: string) {
+ return this;
+ }
+ setUserSettings(userSettings: UserSettings) {
+ this.savedUserSettings = userSettings;
+ return this;
+ }
+}
+
+@Component({
+ selector: 'app-abstract-list',
+ templateUrl: './abstract-list.component.html',
+ styleUrls: ['./abstract-list.component.css']
+})
+class MockConcreteListComponent extends AbstractListComponent implements OnInit {
+ Globals = Globals;
+ listItems: any;
+ currentDate = new Date();
+
+ initAgGrid() {
+ const localGridOptions = <GridOptions>{
+ columnDefs: [
+ {
+ headerName: 'Test1',
+ field: 'test1',
+ colId: 'test1',
+ width: 58
+ },
+ {
+ headerName: 'Test2',
+ field: 'test1',
+ colId: 'test1',
+ width: 58
+ }]
+ };
+ this.gridOptions = Object.assign(this.globalGridOptions, localGridOptions);
+
+ }
+
+ retrieveData() { }
+
+ changeAllSelection() { }
+}
+
+describe('AbstractListComponent', () => {
+ let component: MockConcreteListComponent;
+ let fixture: ComponentFixture<MockConcreteListComponent>;
+
+ class MockService extends AbstractMockObservableService {
+ getGridMeasures() {
+ return this;
+ }
+ }
+
+ class MockReminderService extends AbstractMockObservableService {
+ getCurrentReminders() {
+ return this;
+ }
+ }
+
+ let sessionContext;
+ let mockService;
+ let mockReminderService;
+ let mockUserSettingService;
+ let roleAccessHelper: RoleAccessHelperService;
+ let toasterMessageService: ToasterMessageService;
+ let messageService: MessageService;
+ beforeEach(async(() => {
+ sessionContext = new SessionContext();
+ mockService = new MockService();
+ mockReminderService = new MockReminderService();
+ mockUserSettingService = new MockUserSettingService();
+ roleAccessHelper = new RoleAccessHelperService();
+ messageService = new MessageService;
+ toasterMessageService = new ToasterMessageService(sessionContext, messageService);
+ TestBed.configureTestingModule({
+ imports: [FormsModule],
+ declarations: [
+ // AbstractListComponent,
+ MockConcreteListComponent,
+ StringToDatePipe,
+ FormattedDatePipe,
+ FormattedTimestampPipe,
+ MockComponent({ selector: 'input', inputs: ['options', 'gridId'] })
+ ],
+ providers: [
+ ModeValidator,
+ { provide: UserSettingsService, userValue: mockService },
+ { provide: GridMeasureService, useValue: mockService },
+ { provide: ReminderService, useValue: mockReminderService },
+ { provide: LockService, useValue: mockService },
+ { provide: SessionContext, useClass: SessionContext },
+ { provide: DaterangepickerConfig, useClass: DaterangepickerConfig },
+ { provide: UserSettingsService, useValue: mockUserSettingService },
+ { provide: Router },
+ { provide: RoleAccessHelperService, useValue: roleAccessHelper },
+ { provide: ToasterMessageService, useValue: toasterMessageService },
+ MessageServiceCustom
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MockConcreteListComponent);
+ component = fixture.componentInstance;
+ });
+
+
+
+ it('should return a new ListHelperTool', () => {
+ expect(component.getListHelperTool()).toBeTruthy();
+ });
+
+ it('should init correctly error in user settings service', () => {
+ spyOn(console, 'log').and.callThrough();
+ spyOn(component, 'retrieveData').and.callThrough();
+ mockUserSettingService.content = {};
+ mockUserSettingService.error = 'Error in UserSettingsService';
+ component.ngOnInit();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(console.log).toHaveBeenCalled();
+ expect(component.retrieveData).toHaveBeenCalled();
+ });
+ });
+
+
+ it('should init correctly with empty user settings', () => {
+ mockUserSettingService.content = {};
+ component.ngOnInit();
+ });
+
+ it('should init correctly with non empty user settings', async(() => {
+ mockUserSettingService.content = { userName: '', settingType: '', value: { sorting: 'abc', filter: 'xyz' } };
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ expect(component.userSettings.value.sorting).toBe('abc');
+ expect(component.userSettings.value.filter).toBe('xyz');
+
+ });
+ }));
+
+
+ it('should store new user settings correctly', fakeAsync(() => {
+ const abstractComp: any = component; // used to access privates
+ component.user = USERS[1];
+ component.gridId = 'Gridd';
+ component.sortingState = {
+ column: 'shorty',
+ counter: 1,
+ defaultState: true,
+ isDesc: true
+ };
+ component.filteringSearchText = {
+ branchId: 'filty',
+ title: 'fix',
+ statusId: 'foxy'
+ };
+ mockUserSettingService.savedUserSettings = {};
+
+ abstractComp.saveSettings();
+
+ tick();
+
+ expect(mockUserSettingService.savedUserSettings).toBeTruthy();
+ expect(mockUserSettingService.savedUserSettings.username).toBe(USERS[1].username);
+
+ mockUserSettingService.savedUserSettings.username = 'Brodtkamp';
+
+ abstractComp.settingsIsDirty = true;
+ abstractComp.saveSettings();
+
+ tick();
+ fixture.detectChanges();
+ tick();
+
+ expect(mockUserSettingService.savedUserSettings.username).toBe('Brodtkamp');
+
+ }));
+
+ it('should store new user settings correctly', async(() => {
+ const abstractComp: any = component; // used to access privates
+ component.user = USERS[1];
+ component.gridId = 'Gridd';
+ component.sortingState = {
+ column: 'shorty',
+ counter: 1,
+ defaultState: true,
+ isDesc: true
+ };
+ component.filteringSearchText = {
+ branchId: 'filty',
+ title: 'fix',
+ statusId: 'foxy'
+ };
+ mockUserSettingService.savedUserSettings = {};
+
+ abstractComp.saveSettings();
+
+ fixture.whenStable().then(() => {
+
+ expect(mockUserSettingService.savedUserSettings).toBeTruthy();
+ expect(mockUserSettingService.savedUserSettings.username).toBe(USERS[1].username);
+
+ mockUserSettingService.savedUserSettings.username = 'Brodtkamp';
+
+ abstractComp.settingsIsDirty = true;
+ abstractComp.saveSettings();
+
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+
+ expect(mockUserSettingService.savedUserSettings.username).toBe('Brodtkamp');
+
+ });
+ });
+
+ }));
+
+ xit('should work with onGridReadyCorrectly -> empty', () => {
+ const abstractComp: any = component; // used to access privates
+ const param = {
+ api: {},
+ columnApi: {
+ getAllColumns: () => [],
+ autoSizeColumns: (allColIds) => { }
+ }
+ };
+
+ const mockState = new ParamsMockState();
+ abstractComp.onGridReady(getParamsMock(mockState));
+
+ expect(mockState.setColumnStateCalled).toBeFalsy();
+ expect(mockState.setFilterCalled).toBeFalsy();
+ expect(mockState.onFilterChangedCalled).toBeFalsy();
+ expect(mockState.setSortModelCalled).toBeFalsy();
+ expect(mockState.getAllColumnsCalled).toBeTruthy();
+ expect(mockState.autoSizeColumnsCalled).toBeTruthy();
+ });
+
+ xit('should work with onGridReadyCorrectly -> complete', () => {
+ const abstractComp: any = component; // used to access privates
+ abstractComp.columnState = {};
+ abstractComp.filteringSearchText = 'holla';
+ abstractComp.sortingState = {};
+
+ const mockState = new ParamsMockState();
+ abstractComp.onGridReady(getParamsMock(mockState));
+
+ expect(mockState.setColumnStateCalled).toBeTruthy();
+ expect(mockState.setFilterCalled).toBeTruthy();
+ expect(mockState.onFilterChangedCalled).toBeTruthy();
+ expect(mockState.setSortModelCalled).toBeTruthy();
+ expect(mockState.getAllColumnsCalled).toBeTruthy();
+ expect(mockState.autoSizeColumnsCalled).toBeTruthy();
+ });
+
+ class ParamsMockState {
+ setFilterCalled = false;
+ onFilterChangedCalled = false;
+ setSortModelCalled = false;
+ setColumnStateCalled = false;
+ getAllColumnsCalled = false;
+ autoSizeColumnsCalled = false;
+
+ }
+
+ function getParamsMock(mockState: ParamsMockState) {
+ const param = {
+ api: {
+
+ setFilterModel: (filertext) => { mockState.setFilterCalled = true; },
+ onFilterChanged: () => { mockState.onFilterChangedCalled = true; },
+ setSortModel: () => { mockState.setSortModelCalled = true; }
+ },
+ columnApi: {
+ setColumnState: (state) => { mockState.setColumnStateCalled = true; },
+ getAllColumns: () => { mockState.getAllColumnsCalled = true; return []; },
+ autoSizeColumns: (allColIds) => { mockState.autoSizeColumnsCalled = true; },
+
+ }
+ };
+ return param;
+ }
+
+});
+
diff --git a/src/app/lists/common-components/abstract-list/abstract-list.component.ts b/src/app/lists/common-components/abstract-list/abstract-list.component.ts
new file mode 100644
index 0000000..69faf6b
--- /dev/null
+++ b/src/app/lists/common-components/abstract-list/abstract-list.component.ts
@@ -0,0 +1,279 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import {
+ Component, OnDestroy, OnInit,
+ Input, ChangeDetectorRef
+} from '@angular/core';
+
+
+import { GridMeasureService } from './../../../services/grid-measure.service';
+import { ReminderService } from './../../../services/reminder.service';
+import { DaterangepickerConfig } from 'ng2-daterangepicker';
+import { Router } from '@angular/router';
+import { Globals } from '../../../common/globals';
+import { User } from '../../../model/user';
+import { SessionContext } from '../../../common/session-context';
+import { ListHelperTool } from '../../../common/list-helper-tool';
+import { GridMeasure } from '../../../model/grid-measure';
+import { SettingType, UserSettings, SettingValue } from '../../../model/user-settings';
+import { UserSettingsService } from '../../../services/user-settings.service';
+import { RoleAccess } from '../../../model/role-access';
+import { RoleAccessHelperService } from '../../../services/jobs/role-access-helper.service';
+import { ModeValidator } from '../../../custom_modules/helpers/mode-validator';
+import { GridOptions } from 'ag-grid/dist/lib/entities/gridOptions';
+import { StatusMainFilter } from '../../../model/status-main-filter';
+import { RowDragEndEvent } from '../../../../../node_modules/ag-grid';
+import { ToasterMessageService } from '../../../services/toaster-message.service';
+import { MessageServiceCustom } from '../../../services/message.service';
+
+@Component({
+ selector: 'app-abstract-list',
+ templateUrl: './abstract-list.component.html',
+ styleUrls: ['./abstract-list.component.css']
+})
+
+export class AbstractListComponent implements OnInit, OnDestroy {
+
+
+ userSettings: UserSettings;
+ @Input() withCheckboxes = false;
+ @Input() withDatePicker = true;
+ @Input() withEditButtons = false;
+ @Input() isCollapsible = true;
+ @Input() stayCollapsedGridMeasures = false;
+ @Input() gridId: string;
+
+ showSpinner = true;
+ globals = Globals;
+ settingsIsDirty = false;
+ selectAll = false;
+ currentDate = new Date();
+ endDate: Date;
+ startDate: Date;
+ minDate: Date;
+ maxDate: Date;
+ user: User = null;
+ sortingState: any;
+ columnState: any;
+ subscription: any;
+ filteringSearchText: any;
+ statusMainFilter = new StatusMainFilter();
+ gridmeasures: GridMeasure[];
+ roleAccessDefinition: RoleAccess;
+ gridApi: any;
+ gridColumnApi: any;
+ protected globalGridOptions: any = <GridOptions>{
+ context: {
+ componentParent: this
+ },
+ pagination: true,
+ paginationPageSize: 15,
+ enableSorting: true,
+ enableColResize: true,
+ floatingFilter: true,
+ rowHeight: 32,
+ localeText: {
+ equals: 'ist gleich',
+ contains: 'enthält',
+ startsWith: 'beginnt mit',
+ endsWith: 'endet mit',
+ notContains: 'enthält nicht',
+ notEqual: 'ist ungleich',
+ noRowsToShow: 'Keine Daten vorhanden',
+ greaterThan: 'ist großer als',
+ lessThan: 'ist kleiner als',
+ inRange: 'ist zwischen',
+ clearFilter: 'Filter zurücksetzen'
+ },
+ onSortChanged: (model) => {
+ const currentState = JSON.stringify(model.api.getSortModel());
+ this.sessionContext.setSortingState(currentState);
+ const oldState = JSON.stringify(this.sortingState);
+ this.settingsIsDirty = this.sessionContext.getFilterDirtyState() || currentState !== oldState;
+ this.sessionContext.setFilterDirtyState(this.settingsIsDirty);
+ this.sortingState = model.api.getSortModel();
+ },
+ onFilterChanged: (model) => {
+ const currentState = JSON.stringify(model.api.getFilterModel());
+ this.sessionContext.setFilteringSearchText(currentState);
+ const oldState = JSON.stringify(this.filteringSearchText);
+ this.settingsIsDirty = this.sessionContext.getFilterDirtyState() || currentState !== oldState;
+ this.sessionContext.setFilterDirtyState(this.settingsIsDirty);
+ this.filteringSearchText = model.api.getFilterModel();
+
+ },
+ onColumnMoved: (model) => {
+ const currentState = JSON.stringify(model.columnApi.getColumnState());
+ this.sessionContext.setColumnState(currentState);
+ const oldState = JSON.stringify(this.columnState);
+ this.settingsIsDirty = this.sessionContext.getFilterDirtyState() || currentState !== oldState;
+ this.sessionContext.setFilterDirtyState(this.settingsIsDirty);
+ this.columnState = model.columnApi.getColumnState();
+ },
+ onGridReady: (params) => {
+ // this.onGridReady(params);
+
+ this.gridApi = params.api;
+ this.gridColumnApi = params.columnApi;
+ this.initAgGridStructure();
+
+ },
+ onRowDragEnd: (event: RowDragEndEvent) => {
+ this.onRowDragEnd(event);
+ }
+ };
+
+ protected gridOptions: GridOptions;
+ constructor(
+ private ref: ChangeDetectorRef,
+ protected userSettingsService: UserSettingsService,
+ protected router: Router,
+ protected gridMeasureService: GridMeasureService,
+ protected reminderService: ReminderService,
+ protected sessionContext: SessionContext,
+ protected daterangepickerConfig: DaterangepickerConfig,
+ protected roleAccessHelper: RoleAccessHelperService,
+ protected modeValidator: ModeValidator,
+ protected toasterMessageService: ToasterMessageService,
+ protected messageServiceCustom: MessageServiceCustom) {
+
+ this.gridOptions = <GridOptions>{};
+ }
+
+
+ ngOnDestroy() {
+ }
+
+ ngOnInit() {
+ this.stayCollapsedGridMeasures = this.sessionContext.getCollapseState(Globals.GRID_MEASURE_COLLAPSABLE);
+ this.settingsIsDirty = this.sessionContext.getFilterDirtyState();
+ this.init();
+ }
+
+ protected initAgGrid() { }
+
+ protected initAgGridStructure() {
+ const allColumnIds = [];
+
+ if (this.columnState) {
+ this.gridColumnApi.setColumnState(this.sessionContext.getColumnState() || this.columnState);
+ }
+
+ if (this.filteringSearchText) {
+ this.gridApi.setFilterModel(this.sessionContext.getFilteringSearchText() || this.filteringSearchText);
+ this.gridApi.onFilterChanged();
+ }
+
+ if (this.sortingState) {
+ this.gridApi.setSortModel(this.sessionContext.getSortingState() || this.sortingState);
+ }
+
+ this.gridColumnApi.getAllColumns().forEach(function (column) {
+ allColumnIds.push(column.colId);
+ });
+ this.gridColumnApi.autoSizeColumns(allColumnIds);
+ }
+ protected init() {
+ this.user = this.sessionContext.getCurrUser();
+
+ // console.log('ROLE: ' + this.user.role);
+ this.roleAccessDefinition = this.roleAccessHelper.getRoleAccessDefinitions();
+ this.userSettingsService.getUserSettings(this.gridId)
+ .subscribe(res => {
+ if (res.value) {
+ this.userSettings = res;
+ this.sortingState = this.sessionContext.getSortingState() || res.value[SettingType.Sorting] || this.sortingState;
+ this.filteringSearchText = this.sessionContext.getFilteringSearchText()
+ || res.value[SettingType.Filter] || this.filteringSearchText;
+ this.columnState = this.sessionContext.getColumnState() || res.value[SettingType.ColumnState] || this.columnState;
+ this.statusMainFilter = this.sessionContext.getStatusMainFilter() || res.value[SettingType.StatusFilter] || this.statusMainFilter;
+ }
+ this.retrieveData();
+ this.initAgGrid();
+ },
+ error => {
+ console.log(error);
+ this.retrieveData();
+ });
+ // Daterangepicker Config
+ this.daterangepickerConfig.settings = {
+ timePicker: true,
+ timePicker24Hour: true,
+ timePickerSeconds: false,
+ timePickerIncrement: 1,
+ useCurrent: true,
+ locale: {
+ format: 'DD.MM.YYYY HH:mm',
+ applyLabel: 'Übernehmen',
+ cancelLabel: 'Abbrechen',
+ daysOfWeek: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'],
+ monthNames: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli',
+ 'August', 'September', 'Oktober', 'November', 'Dezember'],
+ firstDay: 1
+ }
+ };
+ }
+ public saveSettings() {
+
+ if (!this.userSettings) {
+ this.userSettings = <UserSettings>{
+ username: this.user.username,
+ settingType: this.gridId,
+ value: <SettingValue>{
+ [SettingType.Sorting]: this.sortingState,
+ [SettingType.Filter]: this.filteringSearchText,
+ [SettingType.ColumnState]: this.columnState,
+ [SettingType.StatusFilter]: this.statusMainFilter
+ }
+ };
+ } else {
+ this.userSettings = {
+ ...this.userSettings, value: <SettingValue>{
+ [SettingType.Sorting]: this.sortingState,
+ [SettingType.Filter]: this.filteringSearchText,
+ [SettingType.ColumnState]: this.columnState,
+ [SettingType.StatusFilter]: this.statusMainFilter
+ }
+ };
+ }
+
+ this.userSettingsService.setUserSettings(this.userSettings).subscribe(gms => {
+ this.settingsIsDirty = false;
+ this.sessionContext.setFilterDirtyState(this.settingsIsDirty);
+ }, error => {
+ console.log(error);
+ });
+
+ }
+
+
+
+ protected onRowDragEnd(e) {
+ console.log('onRowDragEnd', e);
+ }
+
+ protected selectionChanged(): void {
+
+ }
+ protected onItemChanged(item: any): void {
+ }
+ protected retrieveData() { }
+
+ onItemAdded(item: any): void {
+ }
+
+ getListHelperTool() {
+ return new ListHelperTool();
+ }
+ protected changeAllSelection() { }
+
+}
diff --git a/src/app/lists/email-distribution-list/email-distribution-ag-grid-configuration.spec.ts b/src/app/lists/email-distribution-list/email-distribution-ag-grid-configuration.spec.ts
new file mode 100644
index 0000000..1bd9cf9
--- /dev/null
+++ b/src/app/lists/email-distribution-list/email-distribution-ag-grid-configuration.spec.ts
@@ -0,0 +1,62 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+
+import { TestBed } from '@angular/core/testing';
+import { EmailDistributionListAgGridConfiguration } from './email-distribution-ag-grid-configuration';
+import { SessionContext } from '../../common/session-context';
+
+describe('EmailDistributionListAgGridConfiguration', () => {
+
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ });
+ });
+
+ it('should provide the correct functions', (() => {
+ const configArray: any = EmailDistributionListAgGridConfiguration.createColumnDefs();
+
+ const idSegment = configArray[0];
+ expect(idSegment.field).toBe('id');
+
+ const emailAddressSegment = configArray[1];
+ expect(emailAddressSegment.field).toBe('emailAddress');
+
+ }));
+
+ it('should provide the correct modeCellRenderer', (() => {
+ const configArray: any = (EmailDistributionListAgGridConfiguration as any).modeCellRenderer(
+ {
+ context:
+ {
+ componentParent:
+ {
+ sessionContext: {
+ isLocked: function (data: any) { return true; }
+ },
+ modeValidator:
+ {
+ isEditModeAllowed: function (data: any) { return true; },
+ isEmailEditModeAllowed: function (data: any) { return true; }
+ },
+ deleteEmailAddress: function (data: any) { return true; }
+ }
+ },
+ params: {},
+ data: { id: 1, emailAddress: 'test@test.de', delete: false }
+ }
+ );
+
+ }));
+
+});
diff --git a/src/app/lists/email-distribution-list/email-distribution-ag-grid-configuration.ts b/src/app/lists/email-distribution-list/email-distribution-ag-grid-configuration.ts
new file mode 100644
index 0000000..037adf5
--- /dev/null
+++ b/src/app/lists/email-distribution-list/email-distribution-ag-grid-configuration.ts
@@ -0,0 +1,66 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+export class EmailDistributionListAgGridConfiguration {
+
+ static createColumnDefs() {
+ return [
+ {
+ headerName: '',
+ field: 'id',
+ cellRenderer: 'agGroupCellRenderer',
+ cellRendererParams: { innerRenderer: EmailDistributionListAgGridConfiguration.modeCellRenderer },
+ suppressFilter: true,
+ cellClass: 'grid-measure-mode',
+ colId: 'modeButton',
+ minWidth: 58
+ },
+ {
+ headerName: 'Verteilerliste',
+ field: 'emailAddress',
+ headerClass: 'grid-measures-header',
+ cellClass: 'grid-measure-tab-emailaddress',
+ suppressFilter: true,
+ colId: 'emailAddress',
+ width: 300
+ }
+ ];
+ }
+
+ private static modeCellRenderer(params) {
+ const isEditMode =
+ params.context.componentParent.modeValidator.isEmailEditModeAllowed(params.context.componentParent.gridMeasureDetail);
+ const span = document.createElement('span');
+ const modeHtmlTemplate = (isEditMode && !params.data.delete && !params.data.preconfigured) &&
+ !params.context.componentParent.sessionContext.isLocked() &&
+ !params.context.componentParent.sessionContext.isReadOnlyForStatus(params.context.componentParent.gridMeasureDetail) ?
+ '<span style="cursor: default;">' +
+ '<button id="modeButton" class="btn btn-danger btn-sm" >' +
+ '<span class="glyphicon glyphicon-trash"></span>' +
+ '</button>' +
+ '</span>' :
+ '<span style="cursor: not-allowed;">' +
+ '<button id="modeButton" class="btn btn-default btn-sm" disabled>' +
+ '<span class="glyphicon glyphicon-trash"></span>' +
+ '</button></span>';
+ span.innerHTML = '<span style="cursor: default;">' +
+ modeHtmlTemplate +
+ '</span>';
+ const eButton = span.querySelector('#modeButton');
+ eButton.addEventListener('click', function () {
+ params.context.componentParent.deleteEmailAddress(params.data);
+ });
+ return span;
+
+ }
+
+}
diff --git a/src/app/lists/email-distribution-list/email-distribution-list.component.css b/src/app/lists/email-distribution-list/email-distribution-list.component.css
new file mode 100644
index 0000000..c78b871
--- /dev/null
+++ b/src/app/lists/email-distribution-list/email-distribution-list.component.css
@@ -0,0 +1,28 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+.email-distribution-entry-container {
+ padding-left: 0;
+}
+
+.email-distribution-list-grid-container {
+ padding-right: 0;
+}
+
+@media (max-width: 991px) {
+ .email-distribution-entry-container {
+ padding: 0;
+ }
+ .email-distribution-list-grid-container {
+ padding: 30px 0 0 0;
+ }
+}
\ No newline at end of file
diff --git a/src/app/lists/email-distribution-list/email-distribution-list.component.html b/src/app/lists/email-distribution-list/email-distribution-list.component.html
new file mode 100644
index 0000000..6171d3a
--- /dev/null
+++ b/src/app/lists/email-distribution-list/email-distribution-list.component.html
@@ -0,0 +1,17 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+-->
+<div class="col-md-8 email-distribution-list-grid-container">
+ <ag-grid-angular *ngIf="!showSpinner" style="height: 280px; margin-top: 33px;" class="ag-theme-balham" [gridOptions]="gridOptions"
+ [rowData]="gridMeasureDetail.listEmailDistribution">
+ </ag-grid-angular>
+ <app-loading-spinner *ngIf="showSpinner"></app-loading-spinner>
+</div>
\ No newline at end of file
diff --git a/src/app/lists/email-distribution-list/email-distribution-list.component.spec.ts b/src/app/lists/email-distribution-list/email-distribution-list.component.spec.ts
new file mode 100644
index 0000000..44bad2b
--- /dev/null
+++ b/src/app/lists/email-distribution-list/email-distribution-list.component.spec.ts
@@ -0,0 +1,168 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { ComponentFixture, TestBed, async } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { SimpleChange } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { AbstractMockObservableService } from '../../testing/abstract-mock-observable.service';
+import { MockComponent } from '../../testing/mock.component';
+import { ActivatedRouteStub } from '../../testing/router-stubs';
+import { Globals } from './../../common/globals';
+
+import { SessionContext } from './../../common/session-context';
+import { GridMeasureService } from '../../services/grid-measure.service';
+import { USERS } from '../../test-data/users';
+import { EmailDistributionEntry } from '../../model/email-distribution-entry';
+import { GRIDMEASURE } from '../../test-data/grid-measures';
+import { BaseDataService } from '../../services/base-data.service';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+import { MessageService } from 'primeng/api';
+import { EmailDistributionEntryComponent } from '../../pages/email-distribution-entry/email-distribution-entry.component';
+
+class FakeRouter {
+ navigate(commands: any[]) {
+ return commands[0];
+ }
+}
+
+describe('EmailDistributionEntryComponent', () => {
+ let component: EmailDistributionEntryComponent;
+ let fixture: ComponentFixture<EmailDistributionEntryComponent>;
+ let activatedStub: ActivatedRouteStub;
+ let sessionContext: SessionContext;
+
+ let mockGridmeasureService;
+ let toasterMessageService: ToasterMessageService;
+ let mockBaseDataService;
+ let messageService: MessageService;
+
+ class MockGridmeasureService extends AbstractMockObservableService {
+ storeGridMeasure() {
+ return this;
+ }
+ getGridMeasure(id: number) {
+ return this;
+ }
+ }
+
+ class MockBaseDataService extends AbstractMockObservableService {
+ getEmailAddressesFromGridmeasures() {
+ return this;
+ }
+ }
+
+ beforeEach(async(() => {
+ messageService = new MessageService;
+ activatedStub = new ActivatedRouteStub();
+ sessionContext = new SessionContext();
+ toasterMessageService = new ToasterMessageService(sessionContext, messageService);
+ mockGridmeasureService = new MockGridmeasureService();
+ mockBaseDataService = new MockBaseDataService();
+
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule
+ ],
+ declarations: [
+ EmailDistributionEntryComponent,
+ MockComponent({ selector: 'input', inputs: ['options'] })
+ ],
+ providers: [
+ { provide: ActivatedRoute, useValue: activatedStub },
+ { provide: SessionContext, useValue: sessionContext },
+ { provide: ToasterMessageService, useValue: toasterMessageService },
+ { provide: BaseDataService, useValue: mockBaseDataService },
+ { provide: GridMeasureService, useValue: mockGridmeasureService }
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(EmailDistributionEntryComponent);
+ component = fixture.componentInstance;
+ sessionContext.setCurrUser(USERS[0]);
+ sessionContext.setAllUsers(USERS);
+ component.isReadOnlyForm = false;
+
+ component.isCollapsible = true;
+ component.gridMeasureDetail = GRIDMEASURE[0];
+ activatedStub.testParams = { id: 555, mode: Globals.MODE.EDIT };
+ component.ngOnInit();
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should set form in readonly mode', () => {
+ component.readOnlyForm = false;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ isReadOnlyForm: new SimpleChange(component.readOnlyForm, true, true)
+ });
+ fixture.detectChanges();
+ expect(component.readOnlyForm).toBeTruthy();
+ });
+
+ it('should convert xmlstring to json correctly', () => {
+ const xmlstring = `<root>
+ <child><textNode>First & Child</textNode></child>
+ <child><textNode>Second Child</textNode></child>
+ <testAttrs attr1='attr1Value'/>
+ </root>`;
+ const jsonstring =
+ `{"root":{"child":[{"textNode":"First & Child"},{"textNode":"Second Child"}],"testAttrs":{"_attr1":"attr1Value"}}}`;
+ expect(JSON.stringify(component.convertXmlToJsonObj(xmlstring))).toBe(jsonstring);
+ });
+
+ it('should add email-distribution-entry correctly for empty emailDistributionList', () => {
+ component.gridMeasureDetail.listEmailDistribution = [];
+ component.emailDistributionEntry = new EmailDistributionEntry();
+ component.emailDistributionEntry.id = 3;
+ component.emailDistributionEntry.emailAddress = 'test-me-new@test.de';
+ component.emailDistributionEntry.delete = false;
+ component.emailDistributionEntry._isValide = true;
+ component.processAddEmailDistributionEntry();
+ fixture.detectChanges();
+ expect(component.gridMeasureDetail.listEmailDistribution.length).toBe(1);
+ });
+
+ it('should add email-distribution-entry correctly', () => {
+ component.emailDistributionEntry = new EmailDistributionEntry();
+ component.emailDistributionEntry.id = 3;
+ component.emailDistributionEntry.emailAddress = 'test-me-new1@test.de';
+ component.emailDistributionEntry.delete = false;
+ component.emailDistributionEntry._isValide = true;
+ const numberOflistEmailDistribution = component.gridMeasureDetail.listEmailDistribution.length;
+ component.processAddEmailDistributionEntry();
+ fixture.detectChanges();
+ expect(component.gridMeasureDetail.listEmailDistribution.length).toBe(numberOflistEmailDistribution + 1);
+ });
+
+ it('should emit warning message for empty email-distribution-entry', async(() => {
+ spyOn(toasterMessageService, 'showWarn').and.callThrough();
+
+ component.emailDistributionEntry.id = 3;
+ component.emailDistributionEntry.emailAddress = '';
+ component.emailDistributionEntry.delete = false;
+ component.emailDistributionEntry._isValide = false;
+
+ component.processAddEmailDistributionEntry();
+
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect(toasterMessageService.showWarn).toHaveBeenCalled();
+ });
+ }));
+
+});
diff --git a/src/app/lists/email-distribution-list/email-distribution-list.component.ts b/src/app/lists/email-distribution-list/email-distribution-list.component.ts
new file mode 100644
index 0000000..36c57b7
--- /dev/null
+++ b/src/app/lists/email-distribution-list/email-distribution-list.component.ts
@@ -0,0 +1,106 @@
+/*
+******************************************************************************
+* Copyright 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Component, Input, Output, EventEmitter } from '@angular/core';
+import { AbstractListComponent } from '../common-components/abstract-list/abstract-list.component';
+import { Globals } from './../../common/globals';
+import { GridOptions } from 'ag-grid/dist/lib/entities/gridOptions';
+import { GridMeasure } from '../../model/grid-measure';
+import { EmailDistributionEntry } from '../../model/email-distribution-entry';
+import { EmailDistributionListAgGridConfiguration } from './email-distribution-ag-grid-configuration';
+
+@Component({
+ selector: 'app-email-distribution-list',
+ templateUrl: './email-distribution-list.component.html',
+ styleUrls: ['./email-distribution-list.component.css', '../common-components/abstract-list/abstract-list.component.css']
+})
+
+export class EmailDistributionListComponent extends AbstractListComponent {
+
+ @Input() gridMeasureDetail: GridMeasure = new GridMeasure();
+ @Output() gridMeasureChanged = new EventEmitter<GridMeasure>();
+
+ Globals = Globals;
+ listEmailDistribution: any;
+ currentDate = new Date();
+ isStatusCollapsed = true;
+
+ initAgGrid() {
+ const localGridOptions = <GridOptions>{
+ columnDefs: EmailDistributionListAgGridConfiguration.createColumnDefs(),
+ pagination: false
+ };
+ this.gridOptions = Object.assign(this.globalGridOptions, localGridOptions);
+
+ }
+
+ retrieveData() {
+
+ if (!this.gridMeasureDetail.listEmailDistribution || this.gridMeasureDetail.listEmailDistribution.length === 0) {
+ this.gridMeasureDetail.listEmailDistribution = new Array<EmailDistributionEntry>();
+
+ let idCount = 1;
+ // preconfigured emailaddresses from mailtemplates
+ let preconfiguredEmailAddressStringArray = [];
+ preconfiguredEmailAddressStringArray = this.sessionContext.getEmailAddressesFromTemplates();
+ if (preconfiguredEmailAddressStringArray && preconfiguredEmailAddressStringArray.length > 0) {
+ preconfiguredEmailAddressStringArray.forEach(email => {
+ const ede = new EmailDistributionEntry();
+ ede.emailAddress = email.trim();
+ ede.preconfigured = true;
+ ede.delete = false;
+ ede._isValide = true;
+ ede.id = idCount;
+ this.gridMeasureDetail.listEmailDistribution = [...this.gridMeasureDetail.listEmailDistribution, ede];
+ idCount++;
+ });
+ }
+
+ if (this.gridMeasureDetail.emailAddresses) {
+
+ const emailAddressStringArray = this.gridMeasureDetail.emailAddresses.split(';');
+ emailAddressStringArray.forEach(email => {
+ const ede = new EmailDistributionEntry();
+ ede.emailAddress = email.trim();
+ ede.preconfigured = false;
+ ede.delete = false;
+ ede._isValide = true;
+ ede.id = idCount;
+ this.gridMeasureDetail.listEmailDistribution = [...this.gridMeasureDetail.listEmailDistribution, ede];
+ idCount++;
+ });
+ }
+
+ }
+
+ this.listEmailDistribution = this.gridMeasureDetail.listEmailDistribution;
+ this.showSpinner = false;
+
+ }
+
+
+ deleteEmailAddress(emailDistributionEntry: EmailDistributionEntry): void {
+
+ emailDistributionEntry.delete = true;
+ if (!this.gridMeasureDetail.listEmailDistributionDeleted) {
+ this.gridMeasureDetail.listEmailDistributionDeleted = new Array<EmailDistributionEntry>();
+ }
+
+ if (emailDistributionEntry.id !== Globals.TEMP_ID_TO_SHOW_NEW_EMAILDISTRIBUTIONENTRYS) {
+ this.gridMeasureDetail.listEmailDistributionDeleted.push(emailDistributionEntry);
+ }
+
+ this.gridMeasureDetail.listEmailDistribution = this.gridMeasureDetail.listEmailDistribution.filter(s => !s.delete);
+ this.listEmailDistribution = this.gridMeasureDetail.listEmailDistribution;
+ this.gridMeasureChanged.emit(this.gridMeasureDetail);
+ }
+}
+
diff --git a/src/app/lists/grid-measures/grid-measures-ag-grid-configuration.spec.ts b/src/app/lists/grid-measures/grid-measures-ag-grid-configuration.spec.ts
new file mode 100644
index 0000000..5881202
--- /dev/null
+++ b/src/app/lists/grid-measures/grid-measures-ag-grid-configuration.spec.ts
@@ -0,0 +1,83 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+
+import { TestBed, inject } from '@angular/core/testing';
+import { GridMeasuresAgGridConfiguration } from './grid-measures-ag-grid-configuration';
+import { SessionContext } from '../../common/session-context';
+import { BRANCHES } from '../../test-data/branches';
+import { STATUSES } from '../../test-data/statuses';
+
+describe('GridMeasureAgGridConfiguration', () => {
+ let sessionContext: SessionContext;
+
+ beforeEach(() => {
+ sessionContext = new SessionContext();
+
+ TestBed.configureTestingModule({
+ });
+ });
+
+ it('should provide the correct functions', (() => {
+ const configArray: any = GridMeasuresAgGridConfiguration.createColumnDefs(sessionContext);
+ const plannedStarttimeSegment = configArray[1];
+ expect(plannedStarttimeSegment.field).toBe('plannedStarttimeFirstSinglemeasure');
+ expect(plannedStarttimeSegment.valueFormatter({ data: { plannedStarttimeFirstSinglemeasure: null } })).toBe('');
+
+ sessionContext.setCurrentReminders([111]);
+ sessionContext.setExpiredReminders([222]);
+ expect((plannedStarttimeSegment.cellStyle({ data: { id: 222 } }) as any).color).toBe('tomato');
+
+ sessionContext.setCurrentReminders([111]);
+ sessionContext.setExpiredReminders([]);
+ expect((plannedStarttimeSegment.cellStyle({ data: { id: 111 } }) as any).color).toBe('#f79e60');
+
+ const branchIdSegment = configArray[3];
+ expect(branchIdSegment.field).toBe('branchId');
+ sessionContext.setBranches(BRANCHES);
+ expect(branchIdSegment.valueGetter({ data: { branchId: 999 } })).toBeFalsy();
+ expect(branchIdSegment.valueGetter({ data: { branchId: 4 } })).toBe('W');
+
+ expect(branchIdSegment.cellStyle({ data: { branchId: 999 } })).toBeFalsy();
+ expect((branchIdSegment.cellStyle({ data: { branchId: 4 } }) as any).backgroundColor).toBe('');
+
+ const statusIdSegment = configArray[7];
+ expect(statusIdSegment.field).toBe('statusId');
+ sessionContext.setStatuses(STATUSES);
+ expect(statusIdSegment.valueGetter({ data: { statusId: 999 } })).toBeFalsy();
+ expect(statusIdSegment.valueGetter({ data: { statusId: 3 } })).toBe('beendet');
+ }));
+
+ it('should provide the correct modeCellRenderer', (() => {
+ const configArray: any = (GridMeasuresAgGridConfiguration as any).modeCellRenderer(
+ {
+ context:
+ {
+ componentParent:
+ {
+ modeValidator:
+ {
+ isEditModeAllowed: function (data: any) { return true; },
+ isCancelAllowed: function (data: any) { return true; }
+ }
+ }
+ },
+ params: {}
+ }
+ );
+
+
+
+ }));
+
+
+});
diff --git a/src/app/lists/grid-measures/grid-measures-ag-grid-configuration.ts b/src/app/lists/grid-measures/grid-measures-ag-grid-configuration.ts
new file mode 100644
index 0000000..ac7be03
--- /dev/null
+++ b/src/app/lists/grid-measures/grid-measures-ag-grid-configuration.ts
@@ -0,0 +1,226 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { FormattedTimestampPipe } from '../../common-components/pipes/formatted-timestamp.pipe';
+import { SessionContext } from '../../common/session-context';
+import { Globals } from '../../common/globals';
+import * as moment from 'moment';
+
+export class GridMeasuresAgGridConfiguration {
+
+ static createColumnDefs(sessionContext: SessionContext) {
+ const datePipe = new FormattedTimestampPipe();
+ return [
+ {
+ headerName: '',
+ field: 'statusId',
+ cellRenderer: 'agGroupCellRenderer',
+ cellRendererParams: { innerRenderer: GridMeasuresAgGridConfiguration.modeCellRenderer },
+ suppressFilter: true,
+ cellClass: 'grid-measure-mode',
+ colId: 'modeButton',
+ minWidth: 100
+ },
+ {
+ headerName: 'Beginnt am',
+ field: 'plannedStarttimeFirstSinglemeasure',
+ valueFormatter: function (params) {
+ if (!params || !params.data) {
+ return;
+ }
+ return datePipe.transform(params.data.plannedStarttimeFirstSinglemeasure, 'DD.MM.YYYY HH:mm');
+
+ },
+ headerClass: 'grid-measures-header',
+ cellClass: 'grid-measure-tab-date-col',
+ colId: 'plannedStarttimeFirstSinglemeasure',
+ cellStyle: function (params) {
+ if (!params || !params.data) {
+ return;
+ }
+ const shortterm = sessionContext.isShorttermNotification(params.data.id);
+ const overdue = sessionContext.isOverdueNotification(params.data.id);
+ if (overdue) {
+ return { color: 'tomato', 'font-weight': 'bold' };
+ } else if (shortterm) {
+ return { color: '#f79e60', 'font-weight': 'bold' };
+ } else {
+ return null;
+ }
+ },
+ minWidth: 180,
+ filter: 'agDateColumnFilter',
+ filterParams: {
+ inRangeInclusive: true,
+ clearButton: true,
+ // provide comparator function
+ comparator: function (filterLocalDateAtMidnight, cellValue) {
+
+ // In the example application, dates are stored as dd/mm/yyyy
+ // We create a Date object for comparison against the filter date
+ if (cellValue) {
+ const cellDate = new Date(cellValue);
+ cellDate.setHours(0);
+ cellDate.setMinutes(0);
+
+ // Now that both parameters are Date objects, we can compare
+ if (cellDate < filterLocalDateAtMidnight) {
+ return -1;
+ } else if (cellDate > filterLocalDateAtMidnight) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+
+ }
+ }
+ },
+ {
+ headerName: 'Nummer (ID)',
+ field: 'descriptiveId',
+ headerClass: 'grid-measures-header',
+ cellClass: 'grid-measure-tab-id',
+ colId: 'descriptiveId',
+ minWidth: 100
+ },
+ {
+ headerName: 'Sparte',
+ field: 'branchId',
+ filter: 'agTextColumnFilter',
+ valueGetter: function (params) {
+
+ if (!params || !params.data) {
+ return;
+ }
+ const branch = sessionContext.getBranchById(params.data.branchId);
+ if (branch) {
+ return branch.name;
+ } else {
+ return '';
+ }
+ },
+ cellStyle: function (params) {
+ if (!params || !params.data) {
+ return;
+ }
+ const branch = sessionContext.getBranchById(params.data.branchId);
+ if (branch) {
+ return {
+ backgroundColor: branch.colorCode
+ };
+ } else {
+ return '';
+ }
+ },
+ headerClass: 'grid-measures-header grid-measure-tab-branche',
+ cellClass: 'grid-measure-tab-branche',
+ colId: 'branchId',
+ minWidth: 110
+ },
+ {
+ headerName: 'Titel der Maßnahme',
+ filter: 'agTextColumnFilter',
+ field: 'title',
+ headerClass: 'grid-measures-header',
+ cellClass: 'grid-measure-tab-title',
+ colId: 'title',
+ minWidth: 530,
+ maxWidth: 700
+ },
+ {
+ headerName: 'Name des Erstellers',
+ field: 'createUser',
+ headerClass: 'grid-measures-header',
+ cellClass: 'grid-measure-tab-createUser',
+ colId: 'createUser',
+ minWidth: 295
+ },
+ {
+ headerName: 'Betroffenes Objekt / Betriebsmittel',
+ field: 'affectedResource',
+ headerClass: 'grid-measures-header',
+ cellClass: 'grid-measure-tab-affectedResource',
+ colId: 'affectedResource',
+ minWidth: 300
+ },
+ {
+ headerName: 'Status',
+ field: 'statusId',
+ valueGetter: function (params) {
+ if (!params || !params.data) {
+ return;
+ }
+ const status = sessionContext.getStatusById(params.data.statusId);
+ if (status) {
+ return status.name;
+ } else {
+ return '';
+ }
+ },
+ headerClass: 'grid-measures-header grid-measure-tab-status',
+ cellClass: 'grid-measure-tab-status',
+ filter: 'agTextColumnFilter',
+ colId: 'statusId',
+ minWidth: 171
+ }
+ ];
+ }
+
+
+
+ private static modeCellRenderer(params) {
+ const viewBtnStyle = 'style="width: 32px;height: 28px;"';
+ const editBtnStyle = 'style="width: 32px;height: 28px;"';
+ const cancelBtnDisabledStyle = 'style="background-color:white;color:grey;width: 32px;height: 28px;"';
+ const cancelBtnStyle = 'style="background-color:#f79e60;width: 32px;height: 28px;"';
+ if (!params || !params.data) {
+ return;
+ }
+ const isEditMode = params.context.componentParent.modeValidator.isEditModeAllowed(params.data);
+ const isCancelAllowed = params.context.componentParent.modeValidator.isCancelAllowed(params.data);
+ const span = document.createElement('span');
+ const modeHtmlTemplate = isEditMode ? '<button id="modeButton" class="btn btn-primary btn-sm" ' + editBtnStyle + ' >' +
+ '<span class="glyphicon glyphicon-pencil"></span>' +
+ '</button>' :
+ '<button id="modeButton" class="btn btn-default btn-sm"' + viewBtnStyle + ' >' +
+ '<span class="glyphicon glyphicon-eye-open"></span>' +
+ '</button>';
+ const cancelHtmlTemplate = isCancelAllowed ?
+ '<button id="cancelButton" class="btn btn-warning btn-sm"' + cancelBtnStyle + '>' +
+ '<span class="glyphicon glyphicon-remove"></span>' +
+ '</button>' :
+ '<button id="cancelButton" class="btn btn-warning btn-sm" disabled="disabled"' + cancelBtnDisabledStyle + '>' +
+ '<span class="glyphicon glyphicon-remove"></span>' +
+ '</button>';
+ span.innerHTML = '<span style="cursor: default;">' +
+ modeHtmlTemplate + ' ' + cancelHtmlTemplate +
+ '</span>';
+ const eButton = span.querySelector('#modeButton');
+ eButton.addEventListener('click', function () {
+ const mode = isEditMode ? Globals.MODE.EDIT : Globals.MODE.VIEW;
+ params.context.componentParent.modeValidator.handleGridMeasureMode(params.data, mode);
+ });
+
+ const cancelButton = span.querySelector('#cancelButton');
+ cancelButton.addEventListener('click', function () {
+ const mode = Globals.MODE.CANCEL;
+ let gridMeasure: any;
+ params.context.componentParent.gridMeasureService.getGridMeasure(params.data.id).subscribe(async (gm) => {
+ gridMeasure = gm;
+ params.context.componentParent.modeValidator.handleGridMeasureMode(gridMeasure, mode);
+ });
+ });
+ return span;
+ }
+
+
+}
diff --git a/src/app/lists/grid-measures/grid-measures.component.css b/src/app/lists/grid-measures/grid-measures.component.css
new file mode 100644
index 0000000..5047cef
--- /dev/null
+++ b/src/app/lists/grid-measures/grid-measures.component.css
@@ -0,0 +1,107 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+.ag-theme-balham,
+ag-header {
+ font-size: 14px;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+}
+
+.dropdown-item {
+ margin-left: 10px;
+}
+
+.filter-options {
+ width: 100%;
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ border-right: solid 1px #ddd;
+}
+
+.filter-options label {
+ margin: 0 10px 0 0;
+ font-weight: 400;
+}
+
+.save-filter {
+ display: flex;
+ align-items: center;
+ min-width: 152px;
+}
+
+.save-filter button {
+ margin: 0 10px;
+}
+
+.save-filter button .fa {
+ margin-right: 10px;
+}
+
+.switch {
+ position: relative;
+ display: inline-block;
+ width: 36px;
+ height: 18px;
+}
+
+.switch input {
+ display: none;
+}
+
+.slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #ccc;
+ -webkit-transition: .4s;
+ transition: .4s;
+}
+
+.slider:before {
+ position: absolute;
+ content: "";
+ height: 14px;
+ width: 14px;
+ left: 3px;
+ bottom: 2px;
+ background-color: white;
+ -webkit-transition: .4s;
+ transition: .4s;
+}
+
+input:checked+.slider {
+ background-color: #337ab7;
+}
+
+input:focus+.slider {
+ box-shadow: 0 0 1px #337ab7;
+}
+
+input:checked+.slider:before {
+ -webkit-transform: translateX(16px);
+ -ms-transform: translateX(16px);
+ transform: translateX(16px);
+}
+
+/* Rounded sliders */
+
+.slider.round {
+ border-radius: 34px;
+}
+
+.slider.round:before {
+ border-radius: 50%;
+}
diff --git a/src/app/lists/grid-measures/grid-measures.component.html b/src/app/lists/grid-measures/grid-measures.component.html
new file mode 100644
index 0000000..d90e11a
--- /dev/null
+++ b/src/app/lists/grid-measures/grid-measures.component.html
@@ -0,0 +1,55 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+-->
+
+<div class="panel panel-default" style="width:100%;">
+ <div class="panel-heading" style="display: inline-flex; width: 100%;padding: 0px 0px 0px 0px">
+ <h4 class="panel-title" style="width: 100%;padding: 10px 15px">
+ <a *ngIf="isCollapsible" data-toggle="collapse" href="#collapse4" (click)="sessionContext.setCollapseState(!stayCollapsedGridMeasures, Globals.GRID_MEASURE_COLLAPSABLE)">Aktuelle
+ Netzmaßnahmen
+ </a>
+ <div *ngIf="!isCollapsible">Aktuelle Netzmaßnahmen</div>
+ </h4>
+ <div class="filter-options">
+ <label class="switch">
+ <input type="checkbox" [(ngModel)]="statusMainFilter.item.onlyUsersGMsDesired" (change)="setDirty();retrieveData()">
+ <span class="slider round"></span>
+ </label>
+ <label>Meine Vorgänge</label>
+ <label class="switch">
+ <input type="checkbox" [disabled]="statusMainFilter.item.onlyUsersGMsDesired" [(ngModel)]="statusMainFilter.item.isClosedStatusActive "
+ (change)="setDirty();retrieveData() ">
+ <span class="slider round " [style.cursor]="statusMainFilter.item.onlyUsersGMsDesired ? 'not-allowed' : 'pointer'"></span>
+ </label>
+ <label>Geschlossene</label>
+ <label class="switch ">
+ <input type="checkbox" [disabled]="statusMainFilter.item.onlyUsersGMsDesired" [(ngModel)]="statusMainFilter.item.isCanceledStatusActive "
+ (change)="setDirty();retrieveData() ">
+ <span class="slider round " [style.cursor]="statusMainFilter.item.onlyUsersGMsDesired ? 'not-allowed' : 'pointer'"></span>
+ </label>
+ <label>Stornierte</label>
+ </div>
+ <div class="save-filter ">
+ <button [disabled]="!settingsIsDirty" type="button " class="btn btn-default btn-sm " (click)="saveSettings()
+ " title="Einstellungen speichern ">
+ <i class="fa fa-floppy-o fa-lg " aria-hidden="true "></i>Filter Speichern
+ </button>
+ </div>
+ </div>
+ <div id="collapse4" style="width:100%; " class="panel-collapse collapse in " [ngClass]="{ 'in': !isCollapsible || !stayCollapsedGridMeasures} ">
+ <div id="grid-wrapper " class="panel-body " style="width:100%; ">
+ <ag-grid-angular *ngIf="!showSpinner " [style.height.px]="calcGridHeight()" class="ag-theme-balham" [gridOptions]="gridOptions"
+ [rowData]="gridmeasures">
+ </ag-grid-angular>
+ <app-loading-spinner *ngIf="showSpinner "></app-loading-spinner>
+ </div>
+ </div>
+</div>
\ No newline at end of file
diff --git a/src/app/lists/grid-measures/grid-measures.component.spec.ts b/src/app/lists/grid-measures/grid-measures.component.spec.ts
new file mode 100644
index 0000000..f6b9f08
--- /dev/null
+++ b/src/app/lists/grid-measures/grid-measures.component.spec.ts
@@ -0,0 +1,357 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { ComponentFixture, TestBed, async, fakeAsync, tick } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { Router } from '@angular/router';
+import { DaterangepickerConfig } from 'ng2-daterangepicker';
+import { FormattedDatePipe } from '../../common-components/pipes/formatted-date.pipe';
+import { FormattedTimestampPipe } from '../../common-components/pipes/formatted-timestamp.pipe';
+import { StringToDatePipe } from '../../common-components/pipes/string-to-date.pipe';
+import { Lock } from '../../model/lock';
+import { GridMeasureService } from '../../services/grid-measure.service';
+import { ReminderService } from '../../services/reminder.service';
+import { LockService } from '../../services/lock.service';
+import { MessageServiceCustom } from '../../services/message.service';
+import { UserSettingsService } from '../../services/user-settings.service';
+import { GRIDMEASURE } from '../../test-data/grid-measures';
+import { AbstractMockObservableService } from '../../testing/abstract-mock-observable.service';
+import { MockComponent } from '../../testing/mock.component';
+import { UniquePipe } from './../../common-components/pipes/unique.pipe';
+import { SessionContext } from './../../common/session-context';
+import { UserSettings } from './../../model/user-settings';
+import { USERS } from './../../test-data/users';
+import { GridMeasuresComponent } from './grid-measures.component';
+import { RoleAccessHelperService } from '../../services/jobs/role-access-helper.service';
+import { ModeValidator } from '../../custom_modules/helpers/mode-validator';
+import { StatusMainFilter } from '../../model/status-main-filter';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+import { MessageService } from 'primeng/api';
+
+class FakeRouter {
+ navigate(commands: any[]) {
+ return commands[0];
+ }
+}
+
+describe('GridMeasuresComponent', () => {
+ let component: GridMeasuresComponent;
+ let fixture: ComponentFixture<GridMeasuresComponent>;
+ let routerStub: FakeRouter;
+ let router: Router;
+ let sessionContext: SessionContext;
+
+ routerStub = {
+ navigate: jasmine.createSpy('navigate').and.callThrough()
+ };
+
+ class MockService extends AbstractMockObservableService {
+ getGridMeasures() {
+ return this;
+ }
+ }
+
+ class MockReminderService extends AbstractMockObservableService {
+ getCurrentReminders() {
+ return this;
+ }
+ getExpiredReminders() {
+ return this;
+ }
+ }
+
+ class MockUserSettingService extends AbstractMockObservableService {
+ savedUserSettings: UserSettings;
+ getUserSettings(gridId: string) {
+ return this;
+ }
+ setUserSettings(userSettings: UserSettings) {
+ this.savedUserSettings = userSettings;
+ return this;
+ }
+ }
+ class MockLockService extends AbstractMockObservableService {
+ checkLock(key: number, info: string) {
+ const lock = new Lock();
+ lock.key = key;
+ lock.username = 'otto';
+ lock.info = info;
+ return lock;
+ }
+ storeLock(lock: Lock) {
+ return lock;
+ }
+
+ deleteLock(key: number, info: string) {
+ return key;
+ }
+ }
+
+ let mockGridMeasureService;
+ let mockReminderService;
+ let mockUserSettingService;
+ let mockLockService: MockLockService;
+ let roleAccessHelper: RoleAccessHelperService;
+ let toasterMessageService: ToasterMessageService;
+ let messageService: MessageService;
+ beforeEach(async(() => {
+ router = new FakeRouter() as any as Router;
+ sessionContext = new SessionContext();
+ messageService = new MessageService;
+ mockGridMeasureService = new MockService();
+ mockReminderService = new MockReminderService();
+ mockUserSettingService = new MockUserSettingService();
+ mockLockService = new MockLockService();
+ roleAccessHelper = new RoleAccessHelperService();
+ toasterMessageService = new ToasterMessageService(sessionContext, messageService);
+
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule
+ ],
+ declarations: [
+ GridMeasuresComponent,
+ StringToDatePipe,
+ FormattedDatePipe,
+ FormattedTimestampPipe,
+ UniquePipe,
+ MockComponent({ selector: 'input', inputs: ['options'] }),
+ MockComponent({ selector: 'app-loading-spinner' }),
+ MockComponent({ selector: 'ag-grid-angular ', inputs: ['gridOptions', 'rowData'] })
+ ],
+ providers: [
+ ModeValidator,
+ { provide: SessionContext, useValue: sessionContext },
+ MessageServiceCustom,
+ { provide: UserSettingsService, useValue: mockUserSettingService },
+ { provide: Router, useValue: routerStub },
+ { provide: GridMeasureService, useValue: mockGridMeasureService },
+ { provide: ReminderService, useValue: mockReminderService },
+ { provide: LockService, useValue: mockLockService },
+ { provide: DaterangepickerConfig, useClass: DaterangepickerConfig },
+ { provide: RoleAccessHelperService, useValue: roleAccessHelper },
+ { provide: ToasterMessageService, useValue: toasterMessageService }
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ const filter = new StatusMainFilter();
+ filter.item.isClosedStatusActive = false;
+ filter.item.isCanceledStatusActive = false;
+ filter.item.onlyUsersGMsDesired = false;
+ sessionContext.setStatusMainFilter(filter);
+ fixture = TestBed.createComponent(GridMeasuresComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should call init', async(() => {
+ spyOn((component as any), 'init').and.callThrough();
+ spyOn(component, 'initAgGrid').and.callThrough();
+ spyOn(component, 'retrieveData').and.callThrough();
+ sessionContext.setCurrUser(USERS[1]);
+ sessionContext.setUserAuthenticated(true);
+ mockGridMeasureService.content = JSON.parse(JSON.stringify(GRIDMEASURE));
+ mockReminderService.content = [];
+ mockUserSettingService.content = {};
+ const abstractComp: any = component; // used to access privates
+ component.user = USERS[1];
+ component.gridId = 'Gridd';
+ component.sortingState = {
+ column: 'shorty',
+ counter: 1,
+ defaultState: true,
+ isDesc: true
+ };
+ component.filteringSearchText = {
+ branchId: 'filty',
+ title: 'fix',
+ statusId: 'foxy'
+ };
+ mockUserSettingService.savedUserSettings = {};
+
+ abstractComp.saveSettings();
+ fixture.detectChanges();
+ component.ngOnInit();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect((component as any).init).toHaveBeenCalled();
+ expect(component.initAgGrid).toHaveBeenCalled();
+ expect(component.retrieveData).toHaveBeenCalled();
+ });
+ }));
+
+ it('should retrieveData (gridmeasures) on init', async(() => {
+ mockGridMeasureService.content = JSON.parse(JSON.stringify(GRIDMEASURE));
+ mockReminderService.content = [];
+ const abstractComp: any = component; // used to access privates
+ component.user = USERS[1];
+ component.gridId = 'Gridd';
+ component.sortingState = {
+ column: 'shorty',
+ counter: 1,
+ defaultState: true,
+ isDesc: true
+ };
+ component.filteringSearchText = {
+ branchId: 'filty',
+ title: 'fix',
+ statusId: 'foxy'
+ };
+ mockUserSettingService.savedUserSettings = {};
+
+ abstractComp.saveSettings();
+ component.retrieveData();
+ fixture.detectChanges();
+
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect(component.gridmeasures.length).toBe(4);
+ });
+
+
+
+ }));
+
+ it('should retrieveData (gridmeasures) on init by user', async(() => {
+ mockGridMeasureService.content = JSON.parse(JSON.stringify(GRIDMEASURE));
+ mockReminderService.content = [];
+ const abstractComp: any = component; // used to access privates
+ component.user = USERS[1];
+ component.gridId = 'Gridd';
+ component.sortingState = {
+ column: 'shorty',
+ counter: 1,
+ defaultState: true,
+ isDesc: true
+ };
+ component.filteringSearchText = {
+ branchId: 'filty',
+ title: 'fix',
+ statusId: 'foxy'
+ };
+ component.statusMainFilter.item.onlyUsersGMsDesired = true;
+ mockUserSettingService.savedUserSettings = {};
+
+ abstractComp.saveSettings();
+ component.retrieveData();
+ fixture.detectChanges();
+
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect(component.gridmeasures.length).toBe(4);
+ expect(component.statusMainFilter.item.isCanceledStatusActive).toBeFalsy();
+ expect(component.statusMainFilter.item.isClosedStatusActive).toBeFalsy();
+ });
+ }));
+
+ it('should raise an message on error in retrieveData (gridmeasures)', fakeAsync(() => {
+ spyOn((component as any).toasterMessageService, 'showError');
+ sessionContext.setUserAuthenticated(true);
+ mockGridMeasureService.error = 'Error in GridmeasureService';
+ const abstractComp: any = component; // used to access privates
+ component.user = USERS[1];
+ component.gridId = 'Gridd';
+ component.sortingState = {
+ column: 'shorty',
+ counter: 1,
+ defaultState: true,
+ isDesc: true
+ };
+ component.filteringSearchText = {
+ branchId: 'filty',
+ title: 'fix',
+ statusId: 'foxy'
+ };
+ mockUserSettingService.savedUserSettings = {};
+
+ abstractComp.saveSettings();
+ component.retrieveData();
+ fixture.detectChanges();
+ tick();
+ expect((component as any).toasterMessageService.showError).toHaveBeenCalled();
+
+ }));
+
+ it('should raise an message on error in retrieveData (reminders)', fakeAsync(() => {
+ spyOn((component as any).toasterMessageService, 'showError');
+ sessionContext.setUserAuthenticated(true);
+ mockGridMeasureService.content = JSON.parse(JSON.stringify(GRIDMEASURE));
+ mockReminderService.error = 'Error in ReminderService';
+ const abstractComp: any = component; // used to access privates
+ component.user = USERS[1];
+ component.gridId = 'Gridd';
+ component.sortingState = {
+ column: 'shorty',
+ counter: 1,
+ defaultState: true,
+ isDesc: true
+ };
+ component.filteringSearchText = {
+ branchId: 'filty',
+ title: 'fix',
+ statusId: 'foxy'
+ };
+ mockUserSettingService.savedUserSettings = {};
+
+ abstractComp.saveSettings();
+ component.retrieveData();
+ fixture.detectChanges();
+ tick();
+ expect((component as any).toasterMessageService.showError).toHaveBeenCalled();
+
+ }));
+
+ it('should call changeAllSelection and set selected false', async(() => {
+ component.selectAll = false;
+ component.gridmeasures = JSON.parse(JSON.stringify(GRIDMEASURE));
+ fixture.detectChanges();
+ spyOn(component, 'changeAllSelection').and.callThrough();
+
+ fixture.detectChanges();
+
+ component.changeAllSelection();
+ expect(component.changeAllSelection).toHaveBeenCalled();
+ expect(component.gridmeasures[0].selected).toBeFalsy();
+ }));
+
+ it('should call changeAllSelection and set selected true', async(() => {
+ component.selectAll = true;
+ component.gridmeasures = JSON.parse(JSON.stringify(GRIDMEASURE));
+ fixture.detectChanges();
+ spyOn(component, 'changeAllSelection').and.callThrough();
+
+ fixture.detectChanges();
+
+ component.changeAllSelection();
+ expect(component.changeAllSelection).toHaveBeenCalled();
+ expect(component.gridmeasures[0].selected).toBeTruthy();
+ }));
+
+ it('should react on filter click and set filter save to dirty', async(() => {
+ spyOn(component, 'setDirty').and.callThrough();
+ component.setDirty();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.setDirty).toHaveBeenCalledWith();
+ expect(component.settingsIsDirty).toBeTruthy();
+
+ });
+
+ }));
+
+});
diff --git a/src/app/lists/grid-measures/grid-measures.component.ts b/src/app/lists/grid-measures/grid-measures.component.ts
new file mode 100644
index 0000000..eb2a771
--- /dev/null
+++ b/src/app/lists/grid-measures/grid-measures.component.ts
@@ -0,0 +1,106 @@
+/*
+******************************************************************************
+* Copyright 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Component } from '@angular/core';
+import { AbstractListComponent } from '../common-components/abstract-list/abstract-list.component';
+import { ErrorType } from '../../common/enums';
+import { Globals } from './../../common/globals';
+import { OnInit } from '@angular/core';
+import { Observable } from '../../../../node_modules/rxjs/Rx';
+import { GridOptions } from 'ag-grid/dist/lib/entities/gridOptions';
+import { GridMeasuresAgGridConfiguration } from './grid-measures-ag-grid-configuration';
+
+@Component({
+ selector: 'app-grid-measures',
+ templateUrl: './grid-measures.component.html',
+ styleUrls: ['./grid-measures.component.css', '../common-components/abstract-list/abstract-list.component.css'],
+})
+
+export class GridMeasuresComponent extends AbstractListComponent implements OnInit {
+ Globals = Globals;
+ gridmeasures: any;
+ currentDate = new Date();
+
+ isCancelClosedFilterButtons: boolean;
+ windowHeight: number = window.innerHeight;
+
+ ngOnInit() {
+ super.ngOnInit();
+ if (this.sessionContext.getStatusMainFilter() !== null) {
+ this.statusMainFilter = this.sessionContext.getStatusMainFilter();
+ }
+
+ Observable.fromEvent(window, 'resize').debounceTime(100).subscribe(() => this.windowHeight = window.innerHeight);
+ }
+
+ initAgGrid() {
+ const localGridOptions = <GridOptions>{
+ columnDefs: GridMeasuresAgGridConfiguration.createColumnDefs(this.sessionContext)
+ };
+ this.gridOptions = Object.assign(this.globalGridOptions, localGridOptions);
+ }
+
+ async retrieveData() {
+
+ if (this.statusMainFilter.item.onlyUsersGMsDesired === true) {
+ this.statusMainFilter.item.isCanceledStatusActive = false;
+ this.statusMainFilter.item.isClosedStatusActive = false;
+ }
+
+ await this.reminderService.getCurrentReminders().subscribe(currentrems => {
+ this.sessionContext.setCurrentReminders(currentrems);
+ }, error => {
+ console.log(error);
+ this.toasterMessageService.showError(ErrorType.retrieve, error);
+ });
+
+ await this.reminderService.getExpiredReminders().subscribe(expiredrems => {
+ this.sessionContext.setExpiredReminders(expiredrems);
+ }, error => {
+ console.log(error);
+ this.toasterMessageService.showError(ErrorType.retrieve, error);
+ });
+
+ this.gridMeasureService.getGridMeasures(this.statusMainFilter).subscribe(gms => {
+ this.gridmeasures = gms;
+ this.showSpinner = false;
+ }, error => {
+ console.log(error);
+ this.toasterMessageService.showError(ErrorType.retrieve, this.gridId, error);
+ });
+
+ this.sessionContext.setStatusMainFilter(this.statusMainFilter);
+ }
+
+ setDirty() {
+ this.settingsIsDirty = true;
+ this.sessionContext.setFilterDirtyState(true);
+ }
+
+
+ calcGridHeight(): number {
+ const gridOffset = document.getElementsByTagName('ag-grid-angular')[0].getBoundingClientRect().top;
+ const appOffset = 130;
+
+ return this.windowHeight - appOffset - gridOffset;
+ }
+ changeAllSelection(): void {
+ if (this.selectAll) {
+ for (const info of this.gridmeasures) {
+ info.selected = true;
+ }
+ } else {
+ for (const info of this.gridmeasures) {
+ info.selected = false;
+ }
+ }
+ }
+}
diff --git a/src/app/lists/status-changes/status-changes-ag-grid-configuration.spec.ts b/src/app/lists/status-changes/status-changes-ag-grid-configuration.spec.ts
new file mode 100644
index 0000000..aaea02a
--- /dev/null
+++ b/src/app/lists/status-changes/status-changes-ag-grid-configuration.spec.ts
@@ -0,0 +1,45 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+
+import { TestBed, inject } from '@angular/core/testing';
+import { StatusChangesAgGridConfiguration } from './status-changes-ag-grid-configuration';
+import { SessionContext } from '../../common/session-context';
+import { STATUSES } from '../../test-data/statuses';
+import * as moment from 'moment';
+
+describe('StatusChangesAgGridConfiguration', () => {
+ let sessionContext: SessionContext;
+
+ beforeEach(() => {
+ sessionContext = new SessionContext();
+
+ TestBed.configureTestingModule({
+ });
+ });
+
+ it('should provide the correct functions', (() => {
+ const configArray: any = StatusChangesAgGridConfiguration.createColumnDefs( sessionContext );
+
+ const statusIdSegment = configArray[0];
+ expect(statusIdSegment.field).toBe('statusId');
+ sessionContext.setStatuses(STATUSES);
+ expect(statusIdSegment.valueGetter({data: { statusId: 999 }})).toBeFalsy();
+ expect(statusIdSegment.valueGetter({data: { statusId: 3 }})).toBe('beendet');
+
+ const modDateSegment = configArray[1];
+ expect(modDateSegment.field).toBe('modDate');
+ expect(modDateSegment.valueFormatter({data: {modDate: null}})).toBe('');
+
+ }));
+
+});
diff --git a/src/app/lists/status-changes/status-changes-ag-grid-configuration.ts b/src/app/lists/status-changes/status-changes-ag-grid-configuration.ts
new file mode 100644
index 0000000..8b075b5
--- /dev/null
+++ b/src/app/lists/status-changes/status-changes-ag-grid-configuration.ts
@@ -0,0 +1,76 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { FormattedTimestampPipe } from '../../common-components/pipes/formatted-timestamp.pipe';
+import { SessionContext } from '../../common/session-context';
+
+export class StatusChangesAgGridConfiguration {
+
+ static createColumnDefs(sessionContext: SessionContext) {
+ const datePipe = new FormattedTimestampPipe();
+ return [
+ {
+ headerName: 'Status (neu)',
+ field: 'statusId',
+ valueGetter: function (params) {
+ const status = sessionContext.getStatusById(params.data.statusId);
+ if (status) {
+ return status.name;
+ } else {
+ return '';
+ }
+ },
+ headerClass: 'grid-measures-header grid-measure-tab-status',
+ cellClass: 'grid-measure-tab-status',
+ filter: 'agTextColumnFilter',
+ colId: 'statusId',
+ minWidth: 171
+ },
+ {
+ headerName: 'Geändert am',
+ field: 'modDate',
+ valueFormatter: function (params) {
+ return datePipe.transform(params.data.modDate, 'DD.MM.YYYY HH:mm');
+
+ },
+ headerClass: 'grid-measures-header',
+ cellClass: 'grid-measure-tab-date-col',
+ suppressFilter: true,
+ colId: 'modDate'
+ },
+ {
+ headerName: 'Bearbeitet von',
+ field: 'modUser',
+ valueFormatter: function (params) {
+ return sessionContext.getUserMap().findAndRenderUser(params.data.modUser);
+ },
+ headerClass: 'grid-measures-header',
+ cellClass: 'grid-measure-tab-moduser',
+ suppressFilter: true,
+ colId: 'modUser',
+ width: 300
+ }
+ /* ,
+ {
+ headerName: 'Bemerkung',
+ filter: 'agTextColumnFilter',
+ field: 'remark',
+ headerClass: 'grid-measures-header',
+ cellClass: 'grid-measure-tab-remark',
+ colId: 'remark',
+ minWidth: 530
+ }
+ */
+ ];
+ }
+
+
+}
diff --git a/src/app/lists/status-changes/status-changes.component.css b/src/app/lists/status-changes/status-changes.component.css
new file mode 100644
index 0000000..1998c70
--- /dev/null
+++ b/src/app/lists/status-changes/status-changes.component.css
@@ -0,0 +1,11 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
\ No newline at end of file
diff --git a/src/app/lists/status-changes/status-changes.component.html b/src/app/lists/status-changes/status-changes.component.html
new file mode 100644
index 0000000..c5d6c07
--- /dev/null
+++ b/src/app/lists/status-changes/status-changes.component.html
@@ -0,0 +1,29 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+-->
+
+<div class="panel panel-default" style="width:100%;">
+ <div class="panel-heading" style="display: inline-flex; width: 100%;padding: 0px 0px 0px 0px">
+ <h4 class="panel-title" style="width: 100%;padding: 10px 15px">
+ <a *ngIf="isStatusCollapsed" data-toggle="collapse" href="#collapse5">Statuswechsel</a>
+ <div *ngIf="!isStatusCollapsed">Statuswechsel</div>
+ </h4>
+ </div>
+ <div id="collapse5" style="width:100%;" class="panel-collapse collapse in" [ngClass]="{'in': !isStatusCollapsed }">
+ <div id="grid-wrapper" class="panel-body" style="width:100%;">
+
+ <ag-grid-angular *ngIf="!showSpinner" style="height: 600px;" class="ag-theme-balham" [gridOptions]="gridOptions" [rowData]="statuschanges">
+
+ </ag-grid-angular>
+ <app-loading-spinner *ngIf="showSpinner"></app-loading-spinner>
+ </div>
+ </div>
+</div>
\ No newline at end of file
diff --git a/src/app/lists/status-changes/status-changes.component.spec.ts b/src/app/lists/status-changes/status-changes.component.spec.ts
new file mode 100644
index 0000000..732f3ba
--- /dev/null
+++ b/src/app/lists/status-changes/status-changes.component.spec.ts
@@ -0,0 +1,304 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { ComponentFixture, TestBed, async, fakeAsync, tick } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { Router } from '@angular/router';
+import { DaterangepickerConfig } from 'ng2-daterangepicker';
+import { FormattedDatePipe } from '../../common-components/pipes/formatted-date.pipe';
+import { FormattedTimestampPipe } from '../../common-components/pipes/formatted-timestamp.pipe';
+import { StringToDatePipe } from '../../common-components/pipes/string-to-date.pipe';
+import { BannerMessage } from '../../common/banner-message';
+import { BannerMessageStatusEn } from '../../common/enums';
+import { Lock } from '../../model/lock';
+import { GridMeasureService } from '../../services/grid-measure.service';
+import { ReminderService } from '../../services/reminder.service';
+import { LockService } from '../../services/lock.service';
+import { MessageServiceCustom } from '../../services/message.service';
+import { UserSettingsService } from '../../services/user-settings.service';
+import { GRIDMEASURE } from '../../test-data/grid-measures';
+import { AbstractMockObservableService } from '../../testing/abstract-mock-observable.service';
+import { MockComponent } from '../../testing/mock.component';
+import { AbstractListComponent } from '../common-components/abstract-list/abstract-list.component';
+import { UniquePipe } from './../../common-components/pipes/unique.pipe';
+import { SessionContext } from './../../common/session-context';
+import { UserSettings } from './../../model/user-settings';
+import { USERS } from './../../test-data/users';
+import { StatusChangesComponent } from './status-changes.component';
+import { RoleAccessHelperService } from '../../services/jobs/role-access-helper.service';
+import { ModeValidator } from '../../custom_modules/helpers/mode-validator';
+import { STATUSCHANGES } from '../../test-data/status-changes';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+import { MessageService } from 'primeng/api';
+
+class FakeRouter {
+ navigate(commands: any[]) {
+ return commands[0];
+ }
+}
+
+describe('StatusChangesComponent', () => {
+ let component: StatusChangesComponent;
+ let fixture: ComponentFixture<StatusChangesComponent>;
+ let routerStub: FakeRouter;
+ let router: Router;
+ let sessionContext: SessionContext;
+
+ routerStub = {
+ navigate: jasmine.createSpy('navigate').and.callThrough()
+ };
+
+ class MockService extends AbstractMockObservableService {
+ getGridMeasures() {
+ return this;
+ }
+
+ getHistoricalStatusChanges(id: number) {
+ return this;
+ }
+ }
+
+ class MockReminderService extends AbstractMockObservableService {
+ getCurrentReminders() {
+ return this;
+ }
+ }
+
+ class MockUserSettingService extends AbstractMockObservableService {
+ savedUserSettings: UserSettings;
+ getUserSettings(gridId: string) {
+ return this;
+ }
+ setUserSettings(userSettings: UserSettings) {
+ this.savedUserSettings = userSettings;
+ return this;
+ }
+ }
+ class MockLockService extends AbstractMockObservableService {
+ checkLock(key: number, info: string) {
+ const lock = new Lock();
+ lock.key = key;
+ lock.username = 'otto';
+ lock.info = info;
+ return lock;
+ }
+ storeLock(lock: Lock) {
+ return lock;
+ }
+
+ deleteLock(key: number, info: string) {
+ return key;
+ }
+ }
+
+ let mockGridMeasureService;
+ let mockReminderService;
+ let mockUserSettingService;
+ let mockLockService: MockLockService;
+ let roleAccessHelper: RoleAccessHelperService;
+ let toasterMessageService: ToasterMessageService;
+ let messageService: MessageService;
+ beforeEach(async(() => {
+ router = new FakeRouter() as any as Router;
+ sessionContext = new SessionContext();
+ messageService = new MessageService;
+ mockGridMeasureService = new MockService();
+ mockReminderService = new MockReminderService();
+ mockUserSettingService = new MockUserSettingService();
+ mockLockService = new MockLockService();
+ roleAccessHelper = new RoleAccessHelperService();
+ toasterMessageService = new ToasterMessageService(sessionContext, messageService);
+
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule
+ ],
+ declarations: [
+ StatusChangesComponent,
+ StringToDatePipe,
+ AbstractListComponent,
+ FormattedDatePipe,
+ FormattedTimestampPipe,
+ UniquePipe,
+ MockComponent({ selector: 'input', inputs: ['options'] }),
+ MockComponent({ selector: 'app-loading-spinner' }),
+ MockComponent({ selector: 'ag-grid-angular ', inputs: ['gridOptions', 'rowData'] })
+ ],
+ providers: [
+ ModeValidator,
+ { provide: SessionContext, useValue: sessionContext },
+ MessageServiceCustom,
+ { provide: UserSettingsService, useValue: mockUserSettingService },
+ { provide: Router, useValue: routerStub },
+ { provide: GridMeasureService, useValue: mockGridMeasureService },
+ { provide: ReminderService, useValue: mockReminderService },
+ { provide: LockService, useValue: mockLockService },
+ { provide: DaterangepickerConfig, useClass: DaterangepickerConfig },
+ { provide: RoleAccessHelperService, useValue: roleAccessHelper },
+ { provide: ToasterMessageService, useValue: toasterMessageService }
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(StatusChangesComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should call init', async(() => {
+ spyOn((component as any), 'init').and.callThrough();
+ spyOn(component, 'initAgGrid').and.callThrough();
+ spyOn(component, 'retrieveData').and.callThrough();
+ sessionContext.setCurrUser(USERS[1]);
+ sessionContext.setUserAuthenticated(true);
+ mockGridMeasureService.content = JSON.parse(JSON.stringify(STATUSCHANGES));
+ mockReminderService.content = [];
+ mockUserSettingService.content = {};
+ const abstractComp: any = component; // used to access privates
+ component.user = USERS[1];
+ component.gridId = 'statusChanges';
+ component.sortingState = {
+ column: 'shorty',
+ counter: 1,
+ defaultState: true,
+ isDesc: true
+ };
+ component.filteringSearchText = {
+ branchId: 'filty',
+ title: 'fix',
+ statusId: 'foxy'
+ };
+ mockUserSettingService.savedUserSettings = {};
+
+ abstractComp.saveSettings();
+ fixture.detectChanges();
+ component.ngOnInit();
+ fixture.detectChanges();
+ component.initAgGrid();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect((component as any).init).toHaveBeenCalled();
+ expect(component.initAgGrid).toHaveBeenCalled();
+ expect(component.retrieveData).toHaveBeenCalled();
+ });
+ }));
+
+ it('should retrieveData (statuschanges) on init', async(() => {
+ mockGridMeasureService.content = JSON.parse(JSON.stringify(STATUSCHANGES));
+ mockReminderService.content = [];
+ const abstractComp: any = component; // used to access privates
+ component.user = USERS[1];
+ component.gridId = 'statusChanges';
+ component.sortingState = {
+ column: 'shorty',
+ counter: 1,
+ defaultState: true,
+ isDesc: true
+ };
+ component.filteringSearchText = {
+ branchId: 'filty',
+ title: 'fix',
+ statusId: 'foxy'
+ };
+ mockUserSettingService.savedUserSettings = {};
+
+ abstractComp.saveSettings();
+ component.retrieveData();
+ fixture.detectChanges();
+
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect(component.statuschanges.length).toBe(2);
+ });
+
+
+
+ }));
+
+ it('should raise an message on error in retrieveData (statuschanges)', fakeAsync(() => {
+ spyOn(toasterMessageService, 'showError').and.callThrough();
+ sessionContext.setUserAuthenticated(true);
+ mockGridMeasureService.error = 'Error in GridmeasureService';
+ const abstractComp: any = component; // used to access privates
+ component.user = USERS[1];
+ component.gridId = 'statusChanges';
+ component.sortingState = {
+ column: 'shorty',
+ counter: 1,
+ defaultState: true,
+ isDesc: true
+ };
+ component.filteringSearchText = {
+ branchId: 'filty',
+ title: 'fix',
+ statusId: 'foxy'
+ };
+ mockUserSettingService.savedUserSettings = {};
+
+ abstractComp.saveSettings();
+ component.retrieveData();
+ fixture.detectChanges();
+ tick();
+ expect(toasterMessageService.showError).toHaveBeenCalled();
+
+ }));
+
+ it('should set statuschanges as empty array in retrieveData for unknown gridId', fakeAsync(() => {
+
+ sessionContext.setUserAuthenticated(true);
+ const abstractComp: any = component; // used to access privates
+ component.user = USERS[1];
+ component.gridId = null;
+
+ mockUserSettingService.savedUserSettings = {};
+ component.showSpinner = true;
+ abstractComp.saveSettings();
+ component.retrieveData();
+ fixture.detectChanges();
+ tick();
+ expect(component.statuschanges.length).toBe(0);
+ expect(component.showSpinner).toBeFalsy();
+ }));
+
+ it('should call changeAllSelection and set selected false', async(() => {
+ component.selectAll = false;
+ component.statuschanges = JSON.parse(JSON.stringify(STATUSCHANGES));
+ fixture.detectChanges();
+ spyOn(component, 'changeAllSelection').and.callThrough();
+
+ fixture.detectChanges();
+
+ component.changeAllSelection();
+ expect(component.changeAllSelection).toHaveBeenCalled();
+ expect(component.statuschanges[0].selected).toBeFalsy();
+ }));
+
+ it('should call changeAllSelection and set selected true', async(() => {
+ component.selectAll = true;
+ component.statuschanges = JSON.parse(JSON.stringify(STATUSCHANGES));
+ fixture.detectChanges();
+ spyOn(component, 'changeAllSelection').and.callThrough();
+
+ fixture.detectChanges();
+
+ component.changeAllSelection();
+ expect(component.changeAllSelection).toHaveBeenCalled();
+ expect(component.statuschanges[0].selected).toBeTruthy();
+ }));
+
+
+});
diff --git a/src/app/lists/status-changes/status-changes.component.ts b/src/app/lists/status-changes/status-changes.component.ts
new file mode 100644
index 0000000..4471d50
--- /dev/null
+++ b/src/app/lists/status-changes/status-changes.component.ts
@@ -0,0 +1,71 @@
+/*
+******************************************************************************
+* Copyright 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Component } from '@angular/core';
+import { AbstractListComponent } from '../common-components/abstract-list/abstract-list.component';
+import { ErrorType } from '../../common/enums';
+import { Globals } from './../../common/globals';
+import { GridOptions } from 'ag-grid/dist/lib/entities/gridOptions';
+import { StatusChangesAgGridConfiguration } from './status-changes-ag-grid-configuration';
+
+@Component({
+ selector: 'app-status-changes',
+ templateUrl: './status-changes.component.html',
+ styleUrls: ['./status-changes.component.css', '../common-components/abstract-list/abstract-list.component.css']
+})
+
+export class StatusChangesComponent extends AbstractListComponent {
+
+ Globals = Globals;
+ statuschanges: any;
+ currentDate = new Date();
+ isStatusCollapsed = true;
+
+ initAgGrid() {
+ const localGridOptions = <GridOptions>{
+ columnDefs: StatusChangesAgGridConfiguration.createColumnDefs(this.sessionContext),
+ pagination: false
+ };
+ this.gridOptions = Object.assign(this.globalGridOptions, localGridOptions);
+
+ }
+
+ retrieveData() {
+
+ if (this.gridId) {
+ this.gridMeasureService.getHistoricalStatusChanges(parseInt(this.gridId, 10)).subscribe(statchanges => {
+ this.statuschanges = statchanges;
+ this.showSpinner = false;
+ }, error => {
+ console.log(error);
+ this.toasterMessageService.showError(ErrorType.retrieve, this.gridId, error);
+ });
+ } else {
+ this.statuschanges = [];
+ this.showSpinner = false;
+ }
+ }
+
+ changeAllSelection(): void {
+ if (this.selectAll) {
+ for (const info of this.statuschanges) {
+ info.selected = true;
+ }
+ } else {
+ for (const info of this.statuschanges) {
+ info.selected = false;
+ }
+ }
+ }
+
+
+}
+
diff --git a/src/app/lists/steps/steps-ag-grid-configuration.spec.ts b/src/app/lists/steps/steps-ag-grid-configuration.spec.ts
new file mode 100644
index 0000000..392bb44
--- /dev/null
+++ b/src/app/lists/steps/steps-ag-grid-configuration.spec.ts
@@ -0,0 +1,61 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+
+import { TestBed } from '@angular/core/testing';
+import { StepsAgGridConfiguration } from './steps-ag-grid-configuration';
+
+describe('StepsAgGridConfiguration', () => {
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ });
+ });
+
+ it('should provide the correct functions', (() => {
+ const configArray: any = StepsAgGridConfiguration.createColumnDefs();
+
+ const idSegment = configArray[1];
+ expect(idSegment.field).toBe('sortorder');
+
+ const switchingObjectSegment = configArray[2];
+ expect(switchingObjectSegment.field).toBe('switchingObject');
+
+ const sortOrderSegment = configArray[4];
+ expect(sortOrderSegment.field).toBe('targetState');
+ }));
+
+ it('should provide the correct modeCellRenderer', (() => {
+ const configArray: any = (StepsAgGridConfiguration as any).modeCellRenderer(
+ {
+ context:
+ {
+ componentParent:
+ {
+ sessionContext: {
+ isLocked: function (data: any) { return true; }
+ },
+ modeValidator:
+ {
+ isEditModeAllowed: function (data: any) { return true; }
+ },
+ deleteStep: function (data: any) { return true; }
+ }
+ },
+ params: {},
+ data: { id: 1, sortorder: 1, switchingObject: 'Schalter 1', targetState: 'aus', singleGridmeasureId: 3, delete: false }
+ }
+ );
+
+ }));
+
+});
diff --git a/src/app/lists/steps/steps-ag-grid-configuration.ts b/src/app/lists/steps/steps-ag-grid-configuration.ts
new file mode 100644
index 0000000..5a453c2
--- /dev/null
+++ b/src/app/lists/steps/steps-ag-grid-configuration.ts
@@ -0,0 +1,126 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+export class StepsAgGridConfiguration {
+
+
+ static createColumnDefs() {
+ return [
+ {
+ headerName: '',
+ field: 'id',
+ cellRenderer: 'agGroupCellRenderer',
+ cellRendererParams: { innerRenderer: StepsAgGridConfiguration.modeCellRenderer },
+ suppressFilter: true,
+ cellClass: 'grid-measure-mode',
+ colId: 'modeButton',
+ minWidth: 58
+ },
+ {
+ headerName: 'Nr',
+ field: 'sortorder',
+ headerClass: 'grid-measures-header',
+ cellClass: 'grid-measure-tab-sortorder',
+ suppressFilter: true,
+ colId: 'sortorder',
+ width: 80,
+ rowDrag: true
+ },
+ {
+ headerName: 'Objekt der Schaltung',
+ field: 'switchingObject',
+ headerClass: 'grid-measures-header',
+ cellClass: 'grid-measure-tab-switchingObject',
+ suppressFilter: true,
+ colId: 'switchingObject',
+ width: 220,
+ editable: true
+ },
+ {
+ headerName: 'Ist-Zustand',
+ field: 'presentState',
+ headerClass: 'grid-measures-header',
+ cellClass: 'grid-measure-tab-presentState',
+ suppressFilter: true,
+ colId: 'presentState',
+ width: 180,
+ editable: true
+ },
+ {
+ headerName: 'Soll-Zustand',
+ field: 'targetState',
+ headerClass: 'grid-measures-header',
+ cellClass: 'grid-measure-tab-targetState',
+ suppressFilter: true,
+ colId: 'targetState',
+ width: 180,
+ editable: true
+ },
+ {
+ headerName: 'Typ',
+ field: 'type',
+ headerClass: 'grid-measures-header',
+ cellClass: 'grid-measure-tab-type',
+ suppressFilter: true,
+ colId: 'type',
+ width: 100,
+ editable: true
+ },
+ {
+ headerName: 'Ist-Zeit',
+ field: 'presentTime',
+ headerClass: 'grid-measures-header',
+ cellClass: 'grid-measure-tab-presentTime',
+ suppressFilter: true,
+ colId: 'presentTime',
+ width: 180,
+ editable: true
+ },
+ {
+ headerName: 'Ausführender',
+ field: 'operator',
+ headerClass: 'grid-measures-header',
+ cellClass: 'grid-measure-tab-operator',
+ suppressFilter: true,
+ colId: 'operator',
+ width: 180,
+ editable: true
+ }
+ ];
+ }
+
+ private static modeCellRenderer(params) {
+ const isEditMode = params.context.componentParent.modeValidator.isEditModeAllowed(params.context.componentParent.gridMeasureDetail);
+ const span = document.createElement('span');
+ const modeHtmlTemplate = (isEditMode && !params.data.delete) &&
+ !params.context.componentParent.sessionContext.isLocked() &&
+ !params.context.componentParent.sessionContext.isReadOnlyForStatus(params.context.componentParent.gridMeasureDetail) ?
+ '<span style="cursor: default;">' +
+ '<button id="modeButton" class="btn btn-danger btn-sm" >' +
+ '<span class="glyphicon glyphicon-trash"></span>' +
+ '</button>' +
+ '</span>' :
+ '<span style="cursor: not-allowed;">' +
+ '<button id="modeButton" class="btn btn-default btn-sm" disabled>' +
+ '<span class="glyphicon glyphicon-trash"></span>' +
+ '</button></span>';
+ span.innerHTML = '<span style="cursor: default;">' +
+ modeHtmlTemplate +
+ '</span>';
+ const eButton = span.querySelector('#modeButton');
+ eButton.addEventListener('click', function () {
+ params.context.componentParent.deleteStep(params.data);
+ });
+ return span;
+ }
+
+}
diff --git a/src/app/lists/steps/steps.component.css b/src/app/lists/steps/steps.component.css
new file mode 100644
index 0000000..1998c70
--- /dev/null
+++ b/src/app/lists/steps/steps.component.css
@@ -0,0 +1,11 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
\ No newline at end of file
diff --git a/src/app/lists/steps/steps.component.html b/src/app/lists/steps/steps.component.html
new file mode 100644
index 0000000..0b2566e
--- /dev/null
+++ b/src/app/lists/steps/steps.component.html
@@ -0,0 +1,15 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+-->
+<label>Schrittsequenzen</label>
+<ag-grid-angular *ngIf="!showSpinner" style="height: 400px;" class="ag-theme-balham" [gridOptions]="gridOptions" [rowData]="singleGridMeasure.listSteps">
+</ag-grid-angular>
+<app-loading-spinner *ngIf="showSpinner"></app-loading-spinner>
\ No newline at end of file
diff --git a/src/app/lists/steps/steps.component.spec.ts b/src/app/lists/steps/steps.component.spec.ts
new file mode 100644
index 0000000..0cfc1fd
--- /dev/null
+++ b/src/app/lists/steps/steps.component.spec.ts
@@ -0,0 +1,344 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { ComponentFixture, TestBed, async, fakeAsync, tick } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { Router } from '@angular/router';
+import { DaterangepickerConfig } from 'ng2-daterangepicker';
+import { FormattedDatePipe } from '../../common-components/pipes/formatted-date.pipe';
+import { FormattedTimestampPipe } from '../../common-components/pipes/formatted-timestamp.pipe';
+import { StringToDatePipe } from '../../common-components/pipes/string-to-date.pipe';
+import { BannerMessage } from '../../common/banner-message';
+import { BannerMessageStatusEn } from '../../common/enums';
+import { Lock } from '../../model/lock';
+import { GridMeasureService } from '../../services/grid-measure.service';
+import { ReminderService } from '../../services/reminder.service';
+import { LockService } from '../../services/lock.service';
+import { MessageServiceCustom } from '../../services/message.service';
+import { UserSettingsService } from '../../services/user-settings.service';
+import { GRIDMEASURE } from '../../test-data/grid-measures';
+import { AbstractMockObservableService } from '../../testing/abstract-mock-observable.service';
+import { MockComponent } from '../../testing/mock.component';
+import { AbstractListComponent } from '../common-components/abstract-list/abstract-list.component';
+import { UniquePipe } from './../../common-components/pipes/unique.pipe';
+import { SessionContext } from './../../common/session-context';
+import { UserSettings } from './../../model/user-settings';
+import { USERS } from './../../test-data/users';
+import { StepsComponent } from './steps.component';
+import { RoleAccessHelperService } from '../../services/jobs/role-access-helper.service';
+import { ModeValidator } from '../../custom_modules/helpers/mode-validator';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+
+
+class FakeRouter {
+ navigate(commands: any[]) {
+ return commands[0];
+ }
+}
+
+describe('StepsComponent', () => {
+ let component: StepsComponent;
+ let fixture: ComponentFixture<StepsComponent>;
+ let routerStub: FakeRouter;
+ let router: Router;
+ let sessionContext: SessionContext;
+
+ routerStub = {
+ navigate: jasmine.createSpy('navigate').and.callThrough()
+ };
+
+ class MockService extends AbstractMockObservableService {
+ getGridMeasures() {
+ return this;
+ }
+ }
+
+ class MockReminderService extends AbstractMockObservableService {
+ getCurrentReminders() {
+ return this;
+ }
+ }
+
+ class MockUserSettingService extends AbstractMockObservableService {
+ savedUserSettings: UserSettings;
+ getUserSettings(gridId: string) {
+ return this;
+ }
+ setUserSettings(userSettings: UserSettings) {
+ this.savedUserSettings = userSettings;
+ return this;
+ }
+ }
+ class MockLockService extends AbstractMockObservableService {
+ checkLock(key: number, info: string) {
+ const lock = new Lock();
+ lock.key = key;
+ lock.username = 'otto';
+ lock.info = info;
+ return lock;
+ }
+ storeLock(lock: Lock) {
+ return lock;
+ }
+
+ deleteLock(key: number, info: string) {
+ return key;
+ }
+ }
+
+ let mockGridMeasureService;
+ let mockReminderService;
+ let mockUserSettingService;
+ let mockLockService: MockLockService;
+ let roleAccessHelper: RoleAccessHelperService;
+
+ beforeEach(async(() => {
+ router = new FakeRouter() as any as Router;
+ sessionContext = new SessionContext();
+
+ mockGridMeasureService = new MockService();
+ mockReminderService = new MockReminderService();
+ mockUserSettingService = new MockUserSettingService();
+ mockLockService = new MockLockService();
+ roleAccessHelper = new RoleAccessHelperService();
+
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule
+ ],
+ declarations: [
+ StepsComponent,
+ StringToDatePipe,
+ AbstractListComponent,
+ FormattedDatePipe,
+ FormattedTimestampPipe,
+ UniquePipe,
+ MockComponent({ selector: 'input', inputs: ['options', 'gridId'] }),
+ MockComponent({ selector: 'app-loading-spinner' }),
+ MockComponent({ selector: 'ag-grid-angular ', inputs: ['gridOptions', 'rowData'] })
+ ],
+ providers: [
+ ModeValidator,
+ { provide: SessionContext, useValue: sessionContext },
+ MessageServiceCustom,
+ { provide: UserSettingsService, useValue: mockUserSettingService },
+ { provide: Router, useValue: routerStub },
+ { provide: GridMeasureService, useValue: mockGridMeasureService },
+ { provide: ReminderService, useValue: mockReminderService },
+ { provide: LockService, useValue: mockLockService },
+ { provide: DaterangepickerConfig, useClass: DaterangepickerConfig },
+ { provide: RoleAccessHelperService, useValue: roleAccessHelper },
+ { provide: ToasterMessageService }
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(StepsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ xit('should call init', async(() => {
+ const abstractComp: any = component; // used to access privates
+ spyOn((component as any), 'init').and.callThrough();
+ spyOn(component, 'initAgGrid').and.callThrough();
+ spyOn(component, 'retrieveData').and.callThrough();
+ spyOn(abstractComp, 'initAgGridStructure').and.callThrough();
+ sessionContext.setCurrUser(USERS[2]);
+ sessionContext.setUserAuthenticated(true);
+ mockGridMeasureService.content = JSON.parse(JSON.stringify(GRIDMEASURE[0].listSingleGridmeasures[0].listSteps));
+ mockReminderService.content = [];
+ mockUserSettingService.content = {};
+ component.user = USERS[1];
+ component.gridId = 'steps';
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(GRIDMEASURE[0]));
+ component.singleGridMeasure = GRIDMEASURE[0].listSingleGridmeasures[0];
+
+ component.sortingState = {
+ column: 'shorty',
+ counter: 1,
+ defaultState: true,
+ isDesc: true
+ };
+
+ component.filteringSearchText = {
+ branchId: 'filty',
+ title: 'fix',
+ statusId: 'foxy'
+ };
+
+ mockUserSettingService.savedUserSettings = {};
+
+ abstractComp.saveSettings();
+ fixture.detectChanges();
+ component.ngOnInit();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+
+ expect((component as any).init).toHaveBeenCalled();
+ expect(component.initAgGrid).toHaveBeenCalled();
+ expect(component.retrieveData).toHaveBeenCalled();
+ expect((component as any).gridOptions).toBeTruthy();
+
+ // (component as any).onGridReady({type: 'gridReady', api: GridApi, columnApi: ColumnApi});
+
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect((component as any).gridOptions).toBeTruthy();
+ // expect((component as any).gridApi).toBeTruthy();
+ // expect(abstractComp.initAgGridStructure).toHaveBeenCalled();
+ });
+ });
+ }));
+
+ it('should retrieveData (steps) on init', async(() => {
+ mockGridMeasureService.content = JSON.parse(JSON.stringify(GRIDMEASURE[0].listSingleGridmeasures[0].listSteps));
+ mockReminderService.content = [];
+ const abstractComp: any = component; // used to access privates
+ component.user = USERS[1];
+ component.gridId = 'steps';
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(GRIDMEASURE[0]));
+ component.singleGridMeasure = GRIDMEASURE[0].listSingleGridmeasures[0];
+ component.sortingState = {
+ column: 'shorty',
+ counter: 1,
+ defaultState: true,
+ isDesc: true
+ };
+ component.filteringSearchText = {
+ branchId: 'filty',
+ title: 'fix',
+ statusId: 'foxy'
+ };
+ mockUserSettingService.savedUserSettings = {};
+
+ abstractComp.saveSettings();
+ component.retrieveData();
+ fixture.detectChanges();
+
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect(component.singleGridMeasure.listSteps.length).toBe(3);
+ });
+ }));
+
+ xit('should raise an message on error in retrieveData (gridmeasures)', fakeAsync(() => {
+ let msgRisen = false;
+ sessionContext.setUserAuthenticated(true);
+ mockGridMeasureService.error = 'Error in GridmeasureService';
+ const abstractComp: any = component; // used to access privates
+ component.user = USERS[1];
+ component.gridId = 'steps';
+ component.gridMeasureDetail = GRIDMEASURE[0];
+ component.singleGridMeasure = GRIDMEASURE[0].listSingleGridmeasures[0];
+ component.sortingState = {
+ column: 'shorty',
+ counter: 1,
+ defaultState: true,
+ isDesc: true
+ };
+ component.filteringSearchText = {
+ branchId: 'filty',
+ title: 'fix',
+ statusId: 'foxy'
+ };
+ mockUserSettingService.savedUserSettings = {};
+
+ (component as any).messageService.errorOccured$.subscribe((msg: BannerMessage) => {
+ expect(msg.status).toBe(BannerMessageStatusEn.error);
+ msgRisen = true;
+ });
+
+ abstractComp.saveSettings();
+ component.retrieveData();
+ fixture.detectChanges();
+ tick();
+ expect(msgRisen).toBeTruthy('Error message was risen');
+
+ }));
+
+
+ it('should add step to deleted list', async(() => {
+ mockGridMeasureService.content = JSON.parse(JSON.stringify(GRIDMEASURE[0].listSingleGridmeasures[0].listSteps));
+ mockReminderService.content = [];
+ const abstractComp: any = component; // used to access privates
+ component.user = USERS[1];
+ component.gridId = 'steps';
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(GRIDMEASURE[0]));
+ component.singleGridMeasure = GRIDMEASURE[0].listSingleGridmeasures[0];
+ component.sortingState = {
+ column: 'shorty',
+ counter: 1,
+ defaultState: true,
+ isDesc: true
+ };
+ component.filteringSearchText = {
+ branchId: 'filty',
+ title: 'fix',
+ statusId: 'foxy'
+ };
+ mockUserSettingService.savedUserSettings = {};
+
+ abstractComp.saveSettings();
+ component.retrieveData();
+ fixture.detectChanges();
+
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect(component.singleGridMeasure.listSteps.length).toBe(3);
+
+ component.deleteStep(component.singleGridMeasure.listSteps[1]);
+ fixture.detectChanges();
+
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect(component.singleGridMeasure.listSteps.length).toBe(2);
+ expect(component.singleGridMeasure.listSteps.filter(s => s.delete === true).length).toBe(0);
+ expect(component.singleGridMeasure.listStepsDeleted.length).toBe(1);
+ });
+ });
+ }));
+
+ it('should call changeAllSelection and set selected false', async(() => {
+ component.selectAll = false;
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(GRIDMEASURE[0]));
+ component.singleGridMeasure = GRIDMEASURE[0].listSingleGridmeasures[0];
+ fixture.detectChanges();
+ spyOn(component, 'changeAllSelection').and.callThrough();
+
+ fixture.detectChanges();
+
+ component.changeAllSelection();
+ expect(component.changeAllSelection).toHaveBeenCalled();
+ expect(component.singleGridMeasure.listSteps[0].selected).toBeFalsy();
+ }));
+
+ it('should call changeAllSelection and set selected true', async(() => {
+ component.selectAll = true;
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(GRIDMEASURE[0]));
+ component.singleGridMeasure = GRIDMEASURE[0].listSingleGridmeasures[0];
+ fixture.detectChanges();
+ spyOn(component, 'changeAllSelection').and.callThrough();
+
+ fixture.detectChanges();
+
+ component.changeAllSelection();
+ expect(component.changeAllSelection).toHaveBeenCalled();
+ expect(component.singleGridMeasure.listSteps[0].selected).toBeTruthy();
+ }));
+
+});
diff --git a/src/app/lists/steps/steps.component.ts b/src/app/lists/steps/steps.component.ts
new file mode 100644
index 0000000..537192b
--- /dev/null
+++ b/src/app/lists/steps/steps.component.ts
@@ -0,0 +1,105 @@
+import { OnChanges, SimpleChanges } from '@angular/core';
+/*
+******************************************************************************
+* Copyright 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Component, OnInit, Input } from '@angular/core';
+import { AbstractListComponent } from '../common-components/abstract-list/abstract-list.component';
+import { Globals } from './../../common/globals';
+import { GridOptions } from 'ag-grid/dist/lib/entities/gridOptions';
+import { GridMeasure } from '../../model/grid-measure';
+import { StepsAgGridConfiguration } from './steps-ag-grid-configuration';
+import { SingleGridMeasure } from '../../model/single-grid-measure';
+import { Step } from '../../model/step';
+import { RowDragEvent } from '../../../../node_modules/ag-grid';
+
+@Component({
+ selector: 'app-steps',
+ templateUrl: './steps.component.html',
+ styleUrls: ['./steps.component.css', '../common-components/abstract-list/abstract-list.component.css'],
+})
+
+export class StepsComponent extends AbstractListComponent {
+
+ @Input() gridMeasureDetail: GridMeasure = new GridMeasure();
+ @Input() singleGridMeasure: SingleGridMeasure = new SingleGridMeasure();
+
+ Globals = Globals;
+ currentDate = new Date();
+ isStatusCollapsed = true;
+
+
+ initAgGrid() {
+
+ const localGridOptions = <GridOptions>{
+ rowDragManaged: !this.sessionContext.isReadOnlyForStatus(this.gridMeasureDetail),
+ animateRows: true,
+ stopEditingWhenGridLosesFocus: true,
+ columnDefs: StepsAgGridConfiguration.createColumnDefs(),
+ onRowDragEnd: (ev: RowDragEvent) => {
+ this.setNewSortOrderAfterDrag();
+ },
+ pagination: false
+ };
+ this.gridOptions = Object.assign(this.globalGridOptions, localGridOptions);
+
+ }
+
+ retrieveData() {
+ this.showSpinner = false;
+ if (!this.singleGridMeasure.listSteps) {
+ this.singleGridMeasure.listSteps = new Array<Step>();
+ }
+ }
+
+ changeAllSelection(): void {
+ if (this.selectAll) {
+ for (const info of this.singleGridMeasure.listSteps) {
+ info.selected = true;
+ }
+ } else {
+ for (const info of this.singleGridMeasure.listSteps) {
+ info.selected = false;
+ }
+ }
+ }
+
+ setNewSortOrderAfterDrag(): void {
+ const listStepsTmp = new Array<Step>();
+ const modelData: any[] = this.gridApi.getModel().rowsToDisplay;
+ let newSortOrder = 1;
+ for (const stepNode of modelData) {
+ const step: Step = stepNode.data;
+ step.sortorder = newSortOrder;
+ listStepsTmp.push(step);
+ newSortOrder++;
+ }
+ this.singleGridMeasure.listSteps = listStepsTmp;
+
+ }
+
+ deleteStep(step: Step): void {
+
+ step.delete = true;
+ if (!this.singleGridMeasure.listStepsDeleted) {
+ this.singleGridMeasure.listStepsDeleted = new Array<Step>();
+ }
+
+ if (step.id !== Globals.TEMP_ID_TO_SHOW_NEW_STEPS) {
+ this.singleGridMeasure.listStepsDeleted.push(step);
+ }
+
+ this.singleGridMeasure.listSteps = this.singleGridMeasure.listSteps.filter(s => !s.delete);
+ for (let i = 0; i < this.singleGridMeasure.listSteps.length; i++) {
+ this.singleGridMeasure.listSteps[i].sortorder = i + 1;
+ }
+ }
+}
+
diff --git a/src/app/model/TreeModelImpl.ts b/src/app/model/TreeModelImpl.ts
new file mode 100644
index 0000000..92a864f
--- /dev/null
+++ b/src/app/model/TreeModelImpl.ts
@@ -0,0 +1,19 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { TreeModel } from 'ng2-tree';
+
+export class TreeModelImpl implements TreeModel {
+ value: string;
+ id?: string | number;
+ children?: TreeModel[];
+ [additionalData: string]: any;
+}
diff --git a/src/app/model/backend-settings.ts b/src/app/model/backend-settings.ts
new file mode 100644
index 0000000..b575f7f
--- /dev/null
+++ b/src/app/model/backend-settings.ts
@@ -0,0 +1,21 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+export class BackendSettings {
+ reminderPeriod: number;
+ appointmentRepetition: AppointmentRepetition[];
+}
+
+export class AppointmentRepetition {
+ id: number;
+ name: string;
+}
+
diff --git a/src/app/model/branch-level.ts b/src/app/model/branch-level.ts
new file mode 100644
index 0000000..b564810
--- /dev/null
+++ b/src/app/model/branch-level.ts
@@ -0,0 +1,18 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+export class BranchLevel {
+ id?: number;
+ name?: string;
+ description?: string;
+ branchId?: number;
+
+}
diff --git a/src/app/model/branch.ts b/src/app/model/branch.ts
new file mode 100644
index 0000000..817887d
--- /dev/null
+++ b/src/app/model/branch.ts
@@ -0,0 +1,17 @@
+/*
+******************************************************************************
+* Copyright 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+export class Branch {
+ id: number;
+ name: string;
+ description: string;
+ colorCode: string;
+}
diff --git a/src/app/model/cost-center.ts b/src/app/model/cost-center.ts
new file mode 100644
index 0000000..1cb6ff5
--- /dev/null
+++ b/src/app/model/cost-center.ts
@@ -0,0 +1,16 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+export class CostCenter {
+ id?: number;
+ name?: string;
+ description?: string;
+}
diff --git a/src/app/model/document.ts b/src/app/model/document.ts
new file mode 100644
index 0000000..e6e4378
--- /dev/null
+++ b/src/app/model/document.ts
@@ -0,0 +1,16 @@
+/*
+******************************************************************************
+* Copyright 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+export class Document {
+ id: number;
+ documentName: string;
+ data: string;
+}
diff --git a/src/app/model/email-distribution-entry.ts b/src/app/model/email-distribution-entry.ts
new file mode 100644
index 0000000..1e98d8d
--- /dev/null
+++ b/src/app/model/email-distribution-entry.ts
@@ -0,0 +1,19 @@
+/*
+******************************************************************************
+* Copyright 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+export class EmailDistributionEntry {
+ id?: number;
+ emailAddress?: string;
+ preconfigured?: boolean;
+ delete?: boolean;
+ _isValide ?= true;
+}
diff --git a/src/app/model/grid-config.ts b/src/app/model/grid-config.ts
new file mode 100644
index 0000000..8cf036d
--- /dev/null
+++ b/src/app/model/grid-config.ts
@@ -0,0 +1,18 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+export class GridConfig {
+ skipForApproval: boolean;
+ endAfterApproved: boolean;
+ skipRequesting: boolean;
+ endAfterReleased: boolean;
+ skipInWork: boolean;
+}
diff --git a/src/app/model/grid-measure.ts b/src/app/model/grid-measure.ts
new file mode 100644
index 0000000..4723b07
--- /dev/null
+++ b/src/app/model/grid-measure.ts
@@ -0,0 +1,56 @@
+/*
+******************************************************************************
+* Copyright 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { SingleGridMeasure } from './single-grid-measure';
+import { EmailDistributionEntry } from './email-distribution-entry';
+
+export class GridMeasure {
+ id?: number;
+ descriptiveId?: string;
+ title?: string;
+ affectedResource?: string;
+ remark?: string;
+ createUser?: string;
+ createUserDepartment?: string;
+ createDate?: Date;
+ modUser?: string;
+ modUserDepartment?: string;
+ modDate?: Date;
+ statusId?: number;
+ switchingObject?: string;
+ costCenter?: string;
+ // responsibleOnSiteName?: string; // TODO: gehört hier nicht rein, ist ein Attribut der SGM
+ // responsibleOnSiteDepartment?: string; // TODO: gehört hier nicht rein, ist ein Attribut der SGM
+ approvalBy?: string;
+ areaOfSwitching?: string;
+ appointmentRepetition?: string;
+ appointmentStartdate?: string;
+ appointmentNumberOf?: number;
+ plannedStarttimeFirstSequence?: string;
+ plannedStarttimeFirstSinglemeasure?: string;
+ plannedEndtimeLastSinglemeasure?: string;
+ plannedEndtimeGridmeasure?: string;
+ starttimeFirstSequence?: string;
+ starttimeFirstSinglemeasure?: string;
+ endtimeLastSinglemeasure?: string;
+ endtimeGridmeasure?: string;
+ timeOfReallocation?: string;
+ description?: string;
+ branchId?: number;
+ branchLevelId?: number;
+ listSingleGridmeasures?: Array<SingleGridMeasure>;
+ emailAddresses?: string;
+ listEmailDistribution?: Array<EmailDistributionEntry>;
+ listEmailDistributionDeleted?: Array<EmailDistributionEntry>;
+ selected?: boolean;
+ _isValide ?= true;
+ _isHeaderValide ?= true;
+}
diff --git a/src/app/model/jwt-payload.ts b/src/app/model/jwt-payload.ts
new file mode 100644
index 0000000..9f009b2
--- /dev/null
+++ b/src/app/model/jwt-payload.ts
@@ -0,0 +1,16 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+/* Model for the contents of the accessToken, decoded and parsed. It is not completed,
+only the properties actually used are defined. To be completed if needed. */
+export class JwtPayload {
+ name: string;
+}
diff --git a/src/app/model/lock.ts b/src/app/model/lock.ts
new file mode 100644
index 0000000..2e92ecc
--- /dev/null
+++ b/src/app/model/lock.ts
@@ -0,0 +1,18 @@
+/*
+******************************************************************************
+* Copyright 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+export class Lock {
+ id: number;
+ key: number;
+ username: string;
+ info: string;
+
+}
diff --git a/src/app/model/power-system-resource.ts b/src/app/model/power-system-resource.ts
new file mode 100644
index 0000000..0fcdee1
--- /dev/null
+++ b/src/app/model/power-system-resource.ts
@@ -0,0 +1,17 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+export class PowerSystemResource {
+ cimId: string;
+ cimName?: string;
+ cimDescription?: string;
+
+}
diff --git a/src/app/model/role-access.ts b/src/app/model/role-access.ts
new file mode 100644
index 0000000..ba6d801
--- /dev/null
+++ b/src/app/model/role-access.ts
@@ -0,0 +1,32 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+export class RoleAccess {
+ editRoles: Array<EditRoleItems>;
+ controls: Array<Controls>;
+ stornoSection: StornoSection;
+ duplicateSection: DuplicateSection;
+}
+export class EditRoleItems {
+ name: string;
+ gridMeasureStatusIds: Array<number>;
+}
+export class Controls {
+ gridMeasureStatusId: number;
+ activeButtons: Array<string>;
+ inactiveFields: Array<string>;
+}
+export class StornoSection {
+ stornoRoles: Array<string>;
+}
+export class DuplicateSection {
+ duplicateRoles: Array<string>;
+}
diff --git a/src/app/model/single-grid-measure.ts b/src/app/model/single-grid-measure.ts
new file mode 100644
index 0000000..3d77915
--- /dev/null
+++ b/src/app/model/single-grid-measure.ts
@@ -0,0 +1,32 @@
+/*
+******************************************************************************
+* Copyright 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { PowerSystemResource } from './power-system-resource';
+import { Step } from './step';
+
+export class SingleGridMeasure {
+ id?: number;
+ sortorder?: number;
+ title?: string;
+ switchingObject?: string;
+ powerSystemResource?: PowerSystemResource;
+ plannedStarttimeSinglemeasure?: string;
+ plannedEndtimeSinglemeasure?: string;
+ description?: string;
+ gridmeasureId?: number;
+ _isValide ?= true;
+ delete?: boolean;
+ listSteps?: Array<Step>;
+ listStepsDeleted?: Array<Step>;
+ responsibleOnSiteName?: string;
+ responsibleOnSiteDepartment?: string;
+ networkControl?: string;
+}
diff --git a/src/app/model/status-change.ts b/src/app/model/status-change.ts
new file mode 100644
index 0000000..15bdce5
--- /dev/null
+++ b/src/app/model/status-change.ts
@@ -0,0 +1,19 @@
+/*
+******************************************************************************
+* Copyright 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+export class StatusChange {
+ statusId?: number;
+ modDate?: string;
+ modUser?: string;
+ remark?: string;
+ selected?: boolean;
+}
diff --git a/src/app/model/status-main-filter.ts b/src/app/model/status-main-filter.ts
new file mode 100644
index 0000000..aa15064
--- /dev/null
+++ b/src/app/model/status-main-filter.ts
@@ -0,0 +1,19 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+export class StatusMainFilterItem {
+ isClosedStatusActive = false;
+ isCanceledStatusActive = false;
+ onlyUsersGMsDesired = false;
+}
+export class StatusMainFilter {
+ item = new StatusMainFilterItem();
+}
diff --git a/src/app/model/status.ts b/src/app/model/status.ts
new file mode 100644
index 0000000..1261581
--- /dev/null
+++ b/src/app/model/status.ts
@@ -0,0 +1,15 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+export class Status {
+ id: number;
+ name: string;
+}
diff --git a/src/app/model/step.ts b/src/app/model/step.ts
new file mode 100644
index 0000000..bd8c2a5
--- /dev/null
+++ b/src/app/model/step.ts
@@ -0,0 +1,31 @@
+/*
+******************************************************************************
+* Copyright 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+export class Step {
+ id?: number;
+ sortorder?: number;
+ switchingObject?: string;
+ targetState?: string;
+ presentState?: string;
+ presentTime?: string;
+ type?: string;
+ singleGridmeasureId?: number;
+ operator?: string;
+ delete?: boolean;
+ createDate?: Date;
+ createUser?: string;
+ modUser?: string;
+ modDate?: Date;
+ selected?: boolean;
+ _isValide ?= true;
+
+}
diff --git a/src/app/model/territory.ts b/src/app/model/territory.ts
new file mode 100644
index 0000000..3e765a6
--- /dev/null
+++ b/src/app/model/territory.ts
@@ -0,0 +1,16 @@
+/*
+******************************************************************************
+* Copyright 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+export class Territory {
+ id: number;
+ name: string;
+ description: string;
+}
diff --git a/src/app/model/user-department.ts b/src/app/model/user-department.ts
new file mode 100644
index 0000000..6bc40f1
--- /dev/null
+++ b/src/app/model/user-department.ts
@@ -0,0 +1,15 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+export class UserDepartment {
+ id: number;
+ name: string;
+}
diff --git a/src/app/model/user-settings.ts b/src/app/model/user-settings.ts
new file mode 100644
index 0000000..a3c04cb
--- /dev/null
+++ b/src/app/model/user-settings.ts
@@ -0,0 +1,28 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+export class UserSettings {
+ username: string;
+ settingType: SettingType;
+ value: SettingValue;
+}
+
+export class SettingValue {
+ [Identifier: string]: any;
+}
+
+export class SettingType {
+ public static ColumnState = 'columnState';
+ public static Sorting = 'sorting';
+ public static Filter = 'filter';
+ public static StatusFilter = 'statusFilter';
+}
diff --git a/src/app/model/user.ts b/src/app/model/user.ts
new file mode 100644
index 0000000..a1ce997
--- /dev/null
+++ b/src/app/model/user.ts
@@ -0,0 +1,21 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+export class User {
+ id: string;
+ itemName: string;
+ username: string;
+ password: string;
+ name: string;
+ roles?: Array<string>;
+ firstName: string;
+ lastName: string;
+}
diff --git a/src/app/model/version-info.ts b/src/app/model/version-info.ts
new file mode 100644
index 0000000..8b9f277
--- /dev/null
+++ b/src/app/model/version-info.ts
@@ -0,0 +1,16 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+export class VersionInfo {
+ public backendVersion?: string;
+ public dbVersion?: string;
+ public frontendVersion?: string;
+}
diff --git a/src/app/pages/cancel-grid-measure/cancel-grid-measure.component.css b/src/app/pages/cancel-grid-measure/cancel-grid-measure.component.css
new file mode 100644
index 0000000..cf6778b
--- /dev/null
+++ b/src/app/pages/cancel-grid-measure/cancel-grid-measure.component.css
@@ -0,0 +1,47 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+div.panel-default {
+ margin: 1px 1px;
+}
+
+#cancelContainer {
+ display: flex;
+ justify-content: center;
+ margin: 4%;
+}
+
+#cancelReasonText {
+ resize: none;
+ width: 100%;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
+ transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
+}
+
+#backBtn {
+ margin-right: 50px;
+}
+
+#titleContainer {
+ margin-bottom: 3%;
+ padding: 0;
+}
+
+#reasonContainer {
+ margin-bottom: 3%;
+ padding: 0;
+}
+
+#buttonContainer {
+ padding: 0;
+}
diff --git a/src/app/pages/cancel-grid-measure/cancel-grid-measure.component.html b/src/app/pages/cancel-grid-measure/cancel-grid-measure.component.html
new file mode 100644
index 0000000..49a4b48
--- /dev/null
+++ b/src/app/pages/cancel-grid-measure/cancel-grid-measure.component.html
@@ -0,0 +1,50 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+-->
+<div class="grid-measure-body ">
+ <div class="maincontent">
+ <div class="panel panel-default">
+
+ <div class="panel-heading">
+ <h4 class="panel-title">
+ <a data-toggle="collapse" href="#collapse4">Storno Bestätigung</a>
+ </h4>
+ </div>
+
+ <form>
+ <fieldset class="form-group">
+ <div id="cancelContainer">
+ <div>
+
+ <div id="titleContainer" class="col-md-12">
+ <label>Titel der Maßnahme</label>
+ <input disabled="disabled" maxlength="256" type="text" name="title" id="title" class="form-control" [(ngModel)]="gridMeasureDetail.title"
+ />
+ </div>
+
+ <div id="reasonContainer" class="col-md-12">
+ <label>Bitte geben Sie eine Begründung ein:</label>
+ <textarea id="cancelReasonText" maxlength="1024" rows="5" [required]="false" name="cancelReason" [(ngModel)]="cancelReasonText"></textarea>
+ </div>
+
+ <div id="buttonContainer" class="col-md-12">
+ <button id="submitCancelBtn" (click)="doCancel()" type="button" class="btn btn-success pull-right">Bestätigen</button>
+
+ <button id="backBtn" (click)="goToOverview()" [disabled]="storageInProgress" type="button" class="btn btn-primary pull-right">{{Globals.STATUS_BUTTON_LABEL['quit']}}</button>
+ </div>
+ </div>
+
+ </div>
+ </fieldset>
+ </form>
+ </div>
+ </div>
+</div>
diff --git a/src/app/pages/cancel-grid-measure/cancel-grid-measure.component.spec.ts b/src/app/pages/cancel-grid-measure/cancel-grid-measure.component.spec.ts
new file mode 100644
index 0000000..466ee02
--- /dev/null
+++ b/src/app/pages/cancel-grid-measure/cancel-grid-measure.component.spec.ts
@@ -0,0 +1,146 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
+
+import { CancelGridMeasureComponent } from './cancel-grid-measure.component';
+import { SessionContext } from '../../common/session-context';
+import { Router } from '../../../../node_modules/@angular/router';
+import { GridMeasureService } from '../../services/grid-measure.service';
+import { AbstractMockObservableService } from '../../testing/abstract-mock-observable.service';
+import { RouterStub } from '../../testing';
+import { FormsModule } from '../../../../node_modules/@angular/forms';
+import { GRIDMEASURE } from '../../test-data/grid-measures';
+import { Globals } from '../../common/globals';
+import { GridMeasure } from '../../model/grid-measure';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+import { MessageService } from 'primeng/api';
+
+describe('CancelGridMeasureComponent', () => {
+
+ class MockService extends AbstractMockObservableService {
+ storeGridMeasure() {
+ return this;
+ }
+ }
+
+ const gridmeasures: GridMeasure[] = JSON.parse(JSON.stringify(GRIDMEASURE));
+ let routerStub: RouterStub;
+ let component: CancelGridMeasureComponent;
+ let fixture: ComponentFixture<CancelGridMeasureComponent>;
+ let messageService: MessageService;
+ let mockService: MockService;
+ let sessionContext: SessionContext;
+ let toasterMessageService: ToasterMessageService;
+ routerStub = {
+ navigate: jasmine.createSpy('navigate').and.callThrough()
+ };
+
+ beforeEach(async(() => {
+ messageService = new MessageService;
+ sessionContext = new SessionContext();
+ mockService = new MockService();
+ toasterMessageService = new ToasterMessageService(sessionContext, messageService);
+
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule
+ ],
+ declarations: [CancelGridMeasureComponent],
+
+ providers: [
+ SessionContext,
+ { provide: ToasterMessageService, useValue: toasterMessageService },
+ { provide: Router, useValue: routerStub },
+ { provide: GridMeasureService, useValue: mockService }
+
+ ]
+ })
+ .compileComponents();
+
+ sessionContext.setGridMeasureDetail(GRIDMEASURE[0]);
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CancelGridMeasureComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should cancel the grid measure if there is a reason is and the submit button clicked', async(() => {
+ component.cancelReasonText = 'im a reason to cancel the grid measure';
+ fixture.detectChanges();
+ spyOn(component, 'doCancel').and.callThrough();
+ const button = fixture.debugElement.nativeElement.querySelector('button#submitCancelBtn');
+ button.click();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.doCancel).toHaveBeenCalled();
+ expect(component.gridMeasureDetail.statusId).toBe(Globals.STATUS.CANCELED);
+ });
+
+ }));
+
+ it('should not cancel the grid measure if there is not a reason is and the submit button clicked', async(() => {
+ component.cancelReasonText = null;
+ fixture.detectChanges();
+ spyOn(component, 'doCancel').and.callThrough();
+ const button = fixture.debugElement.nativeElement.querySelector('button#submitCancelBtn');
+ button.click();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.doCancel).toHaveBeenCalled();
+ expect(component.gridMeasureDetail.statusId).not.toBe(Globals.STATUS.CANCELED);
+ });
+
+ }));
+
+ it('should create a GridMeasure after click on applybutton', async(() => {
+ spyOn(mockService, 'storeGridMeasure').and.callThrough();
+ mockService.content = JSON.parse(JSON.stringify(gridmeasures[2]));
+
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ component.storageInProgress = true;
+
+ fixture.detectChanges();
+
+ (component as any).updateGridMeasureStatus(Globals.STATUS.CANCELED);
+ expect(mockService.storeGridMeasure).not.toHaveBeenCalled();
+
+ component.storageInProgress = false;
+
+
+ fixture.detectChanges();
+
+ (component as any).updateGridMeasureStatus(Globals.STATUS.CANCELED);
+
+ expect(mockService.storeGridMeasure).toHaveBeenCalled();
+ }));
+
+ it('should handle a service error correctly after approve button', fakeAsync(() => {
+ spyOn(console, 'log').and.callThrough();
+ mockService.error = 'Error';
+
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ fixture.detectChanges();
+ tick();
+ (component as any).updateGridMeasureStatus(Globals.STATUS.CANCELED);
+
+ tick();
+
+ expect(console.log).toHaveBeenCalled();
+ }));
+
+});
diff --git a/src/app/pages/cancel-grid-measure/cancel-grid-measure.component.ts b/src/app/pages/cancel-grid-measure/cancel-grid-measure.component.ts
new file mode 100644
index 0000000..88fac36
--- /dev/null
+++ b/src/app/pages/cancel-grid-measure/cancel-grid-measure.component.ts
@@ -0,0 +1,113 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Component, OnInit } from '@angular/core';
+import { Globals } from '../../common/globals';
+import { SessionContext } from '../../common/session-context';
+import { GridMeasure } from '../../model/grid-measure';
+import { GridMeasureValidatorFactory } from '../../custom_modules/helpers/grid-measure-validator';
+import { ErrorType } from '../../common/enums';
+import { GridMeasureService } from '../../services/grid-measure.service';
+import { Router } from '@angular/router';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+
+@Component({
+ selector: 'app-cancel-grid-measure',
+ templateUrl: './cancel-grid-measure.component.html',
+ styleUrls: ['./cancel-grid-measure.component.css']
+})
+export class CancelGridMeasureComponent implements OnInit {
+
+ Globals = Globals;
+ storageInProgress = false;
+ gridMeasureDetail: GridMeasure;
+ cancelReasonText: string;
+
+ constructor(
+ public sessionContext: SessionContext,
+ private gridMeasureService: GridMeasureService,
+ public router: Router,
+ private toasterMessageService: ToasterMessageService) { }
+
+ ngOnInit() {
+ this.gridMeasureDetail = this.sessionContext.getGridMeasureDetail();
+ }
+
+ doCancel() {
+ if (this.cancelReasonText) {
+ let oldRemark: string;
+ this.gridMeasureDetail = this.sessionContext.getGridMeasureDetail();
+ if (!this.gridMeasureDetail.remark) {
+ oldRemark = '';
+ } else {
+ oldRemark = this.gridMeasureDetail.remark + '\n\n';
+ }
+ this.gridMeasureDetail.remark = oldRemark + '----- Storno Begründung -----\n' + this.cancelReasonText + '\n';
+ this.updateGridMeasureStatus(Globals.STATUS.CANCELED);
+ this.sessionContext.setCancelStage(false);
+ } else {
+ this.toasterMessageService.showWarn('Bitte geben Sie eine Begründung ein!');
+ }
+
+ }
+
+
+ private updateGridMeasureStatus(status: number) {
+ if (this.storageInProgress) {
+ return;
+ } else {
+ this.storageInProgress = true;
+ }
+ this.gridMeasureDetail.createUser = this.gridMeasureDetail.createUser || this.sessionContext.getCurrUser().username;
+ this.gridMeasureDetail.statusId = status;
+
+ if (!GridMeasureValidatorFactory.createGM(this.toasterMessageService).validateEntity(this.gridMeasureDetail, true)) {
+ this.storageInProgress = false;
+ return;
+ }
+ this.mergeDeletedStepsAndResetMinusIds();
+ this.gridMeasureService.storeGridMeasure(this.gridMeasureDetail).subscribe(gm => {
+ this.storageInProgress = false;
+ // this.messageService.emitInfo('Netzmaßnahmes "' + gm.title + '" Status erfolgreich zu "'
+ // + this.sessionContext.getStatusById(gm.statusId).name + '" geändert!', MessageScopeEn.global);
+ this.toasterMessageService.showSuccess('Netzmaßnahmes "' + gm.title + '" Status erfolgreich zu "'
+ + this.sessionContext.getStatusById(gm.statusId).name + '" geändert!');
+ this.goToOverview();
+ },
+ error => {
+ this.storageInProgress = false;
+ // this.messageService.emitError('Netzmaßnahme', ErrorType.stornoLocked);
+ this.toasterMessageService.showError(ErrorType.stornoLocked, 'Netzmaßnahme');
+ console.log(error);
+ }
+ );
+ }
+
+ private mergeDeletedStepsAndResetMinusIds() {
+ this.gridMeasureDetail.listSingleGridmeasures.forEach(sgm => {
+ if (sgm.listSteps) {
+ sgm.listSteps.forEach(step => {
+ if (step.id === Globals.TEMP_ID_TO_SHOW_NEW_STEPS) {
+ delete step.id;
+ }
+ });
+ }
+ if (sgm.listStepsDeleted) {
+ sgm.listSteps.push.apply(sgm.listSteps, sgm.listStepsDeleted);
+ }
+ });
+ }
+
+ goToOverview() {
+ this.router.navigate(['/overview']);
+ }
+
+}
diff --git a/src/app/pages/email-distribution-entry/email-distribution-entry.component.css b/src/app/pages/email-distribution-entry/email-distribution-entry.component.css
new file mode 100644
index 0000000..4cb6ce1
--- /dev/null
+++ b/src/app/pages/email-distribution-entry/email-distribution-entry.component.css
@@ -0,0 +1,44 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+@import url("../grid-measure-detail/grid-measure-detail.component.css");
+#EmailDistributionEntry-head {
+ display: inline-block
+}
+
+#addEmailDistributionEntryGlIcon {
+ background: transparent;
+ border: none;
+ color: white;
+ font-size: 107%;
+}
+
+#addEmailDistributionEntryGlIcon:focus {
+ outline: 0 !important;
+}
+
+.email-distribution-entry-container {
+ padding-left: 0;
+}
+
+.email-distribution-list-grid-container {
+ padding-right: 0;
+}
+
+@media (max-width: 991px) {
+ .email-distribution-entry-container {
+ padding: 0;
+ }
+ .email-distribution-list-grid-container {
+ padding: 0;
+ }
+}
\ No newline at end of file
diff --git a/src/app/pages/email-distribution-entry/email-distribution-entry.component.html b/src/app/pages/email-distribution-entry/email-distribution-entry.component.html
new file mode 100644
index 0000000..909e384
--- /dev/null
+++ b/src/app/pages/email-distribution-entry/email-distribution-entry.component.html
@@ -0,0 +1,41 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+-->
+<div #emailDistributionEntryFormContainer class="col-md-4 email-distribution-entry-container">
+ <form #emailDistributionEntryForm="ngForm" (change)="onEmailDistributionEntryFormValidation(emailDistributionEntryForm.form.valid)"
+ style="width: 100%">
+ <fieldset class="form-group" disabled="{{ readOnlyForm || sessionContext.isLocked() ? 'disabled' : ''}}">
+ <div class="row">
+ <div class="col-md-12">
+ <label class="form-field-label" for="emailAddressControl">E-Mail-Adresse</label>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-12">
+ <input autofocus maxlength="256" type="text" name="emailAddress" id="emailAddress" class="form-control" list="emailsFromGridMeasure"
+ [(ngModel)]="emailDistributionEntry.emailAddress" required pattern="^[_A-Za-z0-9-+]+(\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\.[A-Za-z0-9]+)*(\.[A-Za-z]{2,})$"
+ autocomplete="off" />
+ <datalist id="emailsFromGridMeasure">
+ <option *ngFor="let emailString of emailsFromGridMeasure" [ngValue]="emailString">{{emailString}}</option>
+ </datalist>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-12" style="justify-content: flex-end">
+ <button class="btn btn-success" id="addEmailDistributionEntryBtn" (click)="processAddEmailDistributionEntry(); emailDistributionEntryForm.reset()"
+ [disabled]="storageInProgress || readOnlyForm || !emailDistributionEntryForm.form.valid">E-Mail-Adresse hinzufügen
+ <span class="glyphicon glyphicon-plus" id="addEmailDistributionEntryGlIcon"></span>
+ </button>
+ </div>
+ </div>
+ </fieldset>
+ </form>
+</div>
\ No newline at end of file
diff --git a/src/app/pages/email-distribution-entry/email-distribution-entry.component.spec.ts b/src/app/pages/email-distribution-entry/email-distribution-entry.component.spec.ts
new file mode 100644
index 0000000..e053325
--- /dev/null
+++ b/src/app/pages/email-distribution-entry/email-distribution-entry.component.spec.ts
@@ -0,0 +1,167 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { ComponentFixture, TestBed, async } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { SimpleChange } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { AbstractMockObservableService } from '../../testing/abstract-mock-observable.service';
+import { MockComponent } from '../../testing/mock.component';
+import { ActivatedRouteStub } from '../../testing/router-stubs';
+import { Globals } from './../../common/globals';
+import { EmailDistributionEntryComponent } from './email-distribution-entry.component';
+import { SessionContext } from './../../common/session-context';
+import { GridMeasureService } from '../../services/grid-measure.service';
+import { USERS } from '../../test-data/users';
+import { EmailDistributionEntry } from '../../model/email-distribution-entry';
+import { GRIDMEASURE } from '../../test-data/grid-measures';
+import { BaseDataService } from '../../services/base-data.service';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+import { MessageService } from 'primeng/api';
+
+class FakeRouter {
+ navigate(commands: any[]) {
+ return commands[0];
+ }
+}
+
+describe('EmailDistributionEntryComponent', () => {
+ let component: EmailDistributionEntryComponent;
+ let fixture: ComponentFixture<EmailDistributionEntryComponent>;
+ let activatedStub: ActivatedRouteStub;
+ let sessionContext: SessionContext;
+
+ let mockGridmeasureService;
+ let toasterMessageService: ToasterMessageService;
+ let mockBaseDataService;
+ let messageService: MessageService;
+
+ class MockGridmeasureService extends AbstractMockObservableService {
+ storeGridMeasure() {
+ return this;
+ }
+ getGridMeasure(id: number) {
+ return this;
+ }
+ }
+
+ class MockBaseDataService extends AbstractMockObservableService {
+ getEmailAddressesFromGridmeasures() {
+ return this;
+ }
+ }
+
+ beforeEach(async(() => {
+ messageService = new MessageService;
+ activatedStub = new ActivatedRouteStub();
+ sessionContext = new SessionContext();
+ toasterMessageService = new ToasterMessageService(sessionContext, messageService);
+ mockGridmeasureService = new MockGridmeasureService();
+ mockBaseDataService = new MockBaseDataService();
+
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule
+ ],
+ declarations: [
+ EmailDistributionEntryComponent,
+ MockComponent({ selector: 'input', inputs: ['options'] })
+ ],
+ providers: [
+ { provide: ActivatedRoute, useValue: activatedStub },
+ { provide: SessionContext, useValue: sessionContext },
+ { provide: ToasterMessageService, useValue: toasterMessageService },
+ { provide: BaseDataService, useValue: mockBaseDataService },
+ { provide: GridMeasureService, useValue: mockGridmeasureService }
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(EmailDistributionEntryComponent);
+ component = fixture.componentInstance;
+ sessionContext.setCurrUser(USERS[0]);
+ sessionContext.setAllUsers(USERS);
+ component.isReadOnlyForm = false;
+
+ component.isCollapsible = true;
+ component.gridMeasureDetail = GRIDMEASURE[0];
+ activatedStub.testParams = { id: 555, mode: Globals.MODE.EDIT };
+ component.ngOnInit();
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should set form in readonly mode', () => {
+ component.readOnlyForm = false;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ isReadOnlyForm: new SimpleChange(component.readOnlyForm, true, true)
+ });
+ fixture.detectChanges();
+ expect(component.readOnlyForm).toBeTruthy();
+ });
+
+ it('should convert xmlstring to json correctly', () => {
+ const xmlstring = `<root>
+ <child><textNode>First & Child</textNode></child>
+ <child><textNode>Second Child</textNode></child>
+ <testAttrs attr1='attr1Value'/>
+ </root>`;
+ const jsonstring =
+ `{"root":{"child":[{"textNode":"First & Child"},{"textNode":"Second Child"}],"testAttrs":{"_attr1":"attr1Value"}}}`;
+ expect(JSON.stringify(component.convertXmlToJsonObj(xmlstring))).toBe(jsonstring);
+ });
+
+ it('should add email-distribution-entry correctly for empty emailDistributionList', () => {
+ component.gridMeasureDetail.listEmailDistribution = [];
+ component.emailDistributionEntry = new EmailDistributionEntry();
+ component.emailDistributionEntry.id = 3;
+ component.emailDistributionEntry.emailAddress = 'test-me-new@test.de';
+ component.emailDistributionEntry.delete = false;
+ component.emailDistributionEntry._isValide = true;
+ component.processAddEmailDistributionEntry();
+ fixture.detectChanges();
+ expect(component.gridMeasureDetail.listEmailDistribution.length).toBe(1);
+ });
+
+ it('should add email-distribution-entry correctly', () => {
+ component.emailDistributionEntry = new EmailDistributionEntry();
+ component.emailDistributionEntry.id = 3;
+ component.emailDistributionEntry.emailAddress = 'test-me-new1@test.de';
+ component.emailDistributionEntry.delete = false;
+ component.emailDistributionEntry._isValide = true;
+ const numberOflistEmailDistribution = component.gridMeasureDetail.listEmailDistribution.length;
+ component.processAddEmailDistributionEntry();
+ fixture.detectChanges();
+ expect(component.gridMeasureDetail.listEmailDistribution.length).toBe(numberOflistEmailDistribution + 1);
+ });
+
+ it('should emit warning message for empty email-distribution-entry', async(() => {
+ spyOn(toasterMessageService, 'showWarn').and.callThrough();
+
+ component.emailDistributionEntry.id = 3;
+ component.emailDistributionEntry.emailAddress = '';
+ component.emailDistributionEntry.delete = false;
+ component.emailDistributionEntry._isValide = false;
+
+ component.processAddEmailDistributionEntry();
+
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect(toasterMessageService.showWarn).toHaveBeenCalled();
+ });
+ }));
+
+});
diff --git a/src/app/pages/email-distribution-entry/email-distribution-entry.component.ts b/src/app/pages/email-distribution-entry/email-distribution-entry.component.ts
new file mode 100644
index 0000000..efe9100
--- /dev/null
+++ b/src/app/pages/email-distribution-entry/email-distribution-entry.component.ts
@@ -0,0 +1,163 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+******************************************************************************
+ */
+import {
+ Component, OnInit, Input, OnChanges, SimpleChanges, ViewChild, AfterViewChecked,
+ AfterViewInit, ElementRef, OnDestroy
+} from '@angular/core';
+import { GridMeasure } from '../../model/grid-measure';
+import { EmailDistributionEntry } from '../../model/email-distribution-entry';
+import * as X2JS from '../../../assets/js/xml2json.min.js';
+import { FormGroup } from '@angular/forms';
+import { SessionContext } from './../../common/session-context';
+import { Subscription } from 'rxjs/Subscription';
+import { BaseDataService } from '../../services/base-data.service';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+
+@Component({
+ selector: 'app-email-distribution-entry',
+ templateUrl: './email-distribution-entry.component.html',
+ styleUrls: ['./email-distribution-entry.component.css']
+})
+export class EmailDistributionEntryComponent implements OnInit, OnChanges, OnDestroy, AfterViewChecked, AfterViewInit {
+ @Input() isReadOnlyForm: boolean;
+ @Input() isCollapsible = true;
+ @Input() gridMeasureDetail: GridMeasure;
+
+ form: HTMLFormElement;
+ readOnlyForm: boolean;
+ emailDistributionEntryFormValid: boolean;
+ isStatusCollapsed = true;
+ storageInProgress: boolean;
+ emailDistributionEntry: EmailDistributionEntry = new EmailDistributionEntry();
+ inactiveFields: Array<string> = [];
+ subscription: Subscription;
+ emailsFromGridMeasure: string[];
+
+ @ViewChild('emailDistributionEntryFormContainer') emailDistributionEntryFormContainer: ElementRef;
+ @ViewChild('emailDistributionEntryForm') emailDistributionEntryForm: FormGroup;
+ constructor(
+ public sessionContext: SessionContext,
+ private baseDataService: BaseDataService,
+ private toasterMessageService: ToasterMessageService) { }
+
+ ngOnInit() {
+ this.inactiveFields = this.sessionContext.getInactiveFields();
+ this.getEmailsFromGridMeasures();
+ }
+
+ ngAfterViewInit() {
+ this.initInactiveFields();
+ }
+
+ ngAfterViewChecked() {
+ if (this.emailDistributionEntryForm) {
+ this.emailDistributionEntry._isValide = this.emailDistributionEntryForm.valid;
+ }
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+
+ if (changes['isReadOnlyForm']) {
+ this.readOnlyForm = changes['isReadOnlyForm'].currentValue;
+ this.initInactiveFields();
+ }
+
+ }
+
+ private getEmailsFromGridMeasures() {
+ this.baseDataService.getEmailAddressesFromGridmeasures()
+ .subscribe(emails => this.emailsFromGridMeasure = emails, error => {
+ console.log(error);
+ });
+ }
+
+ public initInactiveFields() {
+ const el: HTMLElement = this.emailDistributionEntryFormContainer.nativeElement as HTMLElement;
+ const fields = el.querySelectorAll('*[id]');
+ for (let index = 0; index < fields.length; index++) {
+ const field = fields[index];
+ if (this.readOnlyForm || this.isFieldInactive(field['id'])) {
+ field.setAttribute('disabled', 'disabled');
+ } else {
+ field.removeAttribute('disabled');
+ }
+ }
+ }
+
+ private isFieldInactive(fieldName: string): boolean {
+ return this.inactiveFields.filter(field => field === fieldName).length > 0;
+ }
+
+ ngOnDestroy() {
+ }
+
+
+ public processAddEmailDistributionEntry() {
+
+ if (!this.isEmailDistributionEntryEmpty()) {
+ const emailDistributionEntryDeepCopy = JSON.parse(JSON.stringify(this.emailDistributionEntry));
+ emailDistributionEntryDeepCopy.id = -1;
+ emailDistributionEntryDeepCopy.delete = false;
+
+ if (!this.isMailAddressInTheList(emailDistributionEntryDeepCopy)) {
+ this.storageInProgress = true;
+ if (!this.gridMeasureDetail.listEmailDistribution || this.gridMeasureDetail.listEmailDistribution.length === 0) {
+ this.gridMeasureDetail.listEmailDistribution = new Array<EmailDistributionEntry>();
+
+ }
+
+ /* A simple push on listEmailDistribution (this.gridMeasureDetail.listEmailDistribution.push(emailDistributionEntryDeepCopy))
+ doesn't trigger Angular to refresh the view-model. This is why you have to use the following way
+ which creates a "new" array (copy of the old) and appends it. */
+ this.gridMeasureDetail.listEmailDistribution = [...this.gridMeasureDetail.listEmailDistribution, emailDistributionEntryDeepCopy];
+ this.storageInProgress = false;
+ } else {
+ // this.messageService.emitWarning('Die Email-Adresse ist schon in der Liste vorhanden!', MessageScopeEn.local);
+ this.toasterMessageService.showWarn('Die Email-Adresse ist schon in der Liste vorhanden!');
+ }
+
+ } else {
+ this.toasterMessageService.showWarn('Bitte alle Felder im Email-Verteiler-Eintrag aufüllen!');
+ // this.messageService.emitWarning('Bitte alle Felder im Email-Verteiler-Eintrag aufüllen!', MessageScopeEn.local);
+ }
+
+ }
+
+ private isMailAddressInTheList(email: EmailDistributionEntry): boolean {
+ let is: boolean;
+ this.gridMeasureDetail.listEmailDistribution.forEach(emailInList => {
+ if (emailInList.emailAddress === email.emailAddress) {
+ is = true;
+ return;
+ }
+ });
+ return is;
+
+ }
+
+ onEmailDistributionEntryFormValidation(valid: boolean) {
+ this.emailDistributionEntry._isValide = valid;
+ }
+
+ isEmailDistributionEntryEmpty() {
+ if (!this.emailDistributionEntry.emailAddress) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ convertXmlToJsonObj(xml: string) {
+ return new X2JS().xml_str2json(xml);
+ }
+
+}
+
diff --git a/src/app/pages/grid-config-modifier/grid-config-modifier.component.css b/src/app/pages/grid-config-modifier/grid-config-modifier.component.css
new file mode 100644
index 0000000..02df5c2
--- /dev/null
+++ b/src/app/pages/grid-config-modifier/grid-config-modifier.component.css
@@ -0,0 +1,76 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+#statusSwitches {
+ padding-left: 50px;
+ width: 50%;
+ display: block;
+ justify-content: flex-end;
+ padding-top: 30px;
+}
+.switch {
+ vertical-align: middle;
+ position: relative;
+ display: inline-block;
+ width: 36px;
+ height: 18px;
+}
+.switch input {
+ display: none;
+}
+
+.slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #ccc;
+ -webkit-transition: .4s;
+ transition: .4s;
+}
+
+.slider:before {
+ position: absolute;
+ content: "";
+ height: 14px;
+ width: 14px;
+ left: 5px;
+ bottom: 2px;
+ background-color: white;
+ -webkit-transition: .4s;
+ transition: .4s;
+}
+
+input:checked+.slider {
+ background-color: #337ab7;
+}
+
+input:focus+.slider {
+ box-shadow: 0 0 1px #337ab7;
+}
+
+input:checked+.slider:before {
+ -webkit-transform: translateX(12px);
+ -ms-transform: translateX(12px);
+ transform: translateX(12px);
+}
+
+/* Rounded sliders */
+
+.slider.round {
+ border-radius: 34px;
+}
+
+.slider.round:before {
+ border-radius: 50%;
+}
\ No newline at end of file
diff --git a/src/app/pages/grid-config-modifier/grid-config-modifier.component.html b/src/app/pages/grid-config-modifier/grid-config-modifier.component.html
new file mode 100644
index 0000000..8cc3052
--- /dev/null
+++ b/src/app/pages/grid-config-modifier/grid-config-modifier.component.html
@@ -0,0 +1,51 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+-->
+<h3 align="center">Änderung der Einstellungen für verfügbare Status</h3>
+
+<div id="statusSwitches">
+ <label class="switch">
+ <input type="checkbox" [(ngModel)]="modifyGridConfig.skipForApproval">
+ <span class="slider round"></span>
+ </label>
+ <label style="padding-right: 20px; padding-left: 10px; vertical-align: center">Überspringe: Zur Genehmigung</label>
+ <br>
+ <label class="switch">
+ <input type="checkbox" [(ngModel)]="modifyGridConfig.skipRequesting">
+ <span class="slider round"></span>
+ </label>
+ <label style="padding-right: 20px; padding-left: 10px; vertical-align: center">Überspringe: Anforderung</label>
+ <br>
+ <label class="switch">
+ <input type="checkbox" [(ngModel)]="modifyGridConfig.skipInWork">
+ <span class="slider round"></span>
+ </label>
+ <label style="padding-right: 20px; padding-left: 10px;">Überspringe: In Arbeit</label>
+ <br>
+ <label class="switch">
+ <input type="checkbox" [(ngModel)]="modifyGridConfig.endAfterReleased">
+ <span class="slider round"></span>
+ </label>
+ <label style="padding-right: 20px; padding-left: 10px;">Beenden nach Freigabe</label>
+ <br>
+ <label class="switch">
+ <input type="checkbox" [(ngModel)]="modifyGridConfig.endAfterApproved">
+ <span class="slider round"></span>
+ </label>
+ <label style="padding-right: 20px; padding-left: 10px;">Beenden nach Genehmigung</label>
+ <br>
+ <div id="changeStatusBtn" class="btn btn-primary" style="float: center; margin-top: 30px;" (click)="requestGridConfigChange(false)">
+ Status ändern
+ </div>
+ <div id="backBtn" class="btn btn-primary" style="float: center; margin-top: 30px;" (click)="goToOverview()">
+ Zurück
+ </div>
+</div>
diff --git a/src/app/pages/grid-config-modifier/grid-config-modifier.component.spec.ts b/src/app/pages/grid-config-modifier/grid-config-modifier.component.spec.ts
new file mode 100644
index 0000000..0320334
--- /dev/null
+++ b/src/app/pages/grid-config-modifier/grid-config-modifier.component.spec.ts
@@ -0,0 +1,94 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { GridConfigModifierComponent } from './grid-config-modifier.component';
+import { Router } from '@angular/router';
+import { ModifyGridConfigService } from '../../services/modify-grid-config.service';
+import { AbstractMockObservableService } from '../../testing/abstract-mock-observable.service';
+import { SessionContext } from '../../common/session-context';
+import { FormsModule } from '@angular/forms';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+
+describe('GridConfigModifierComponent', () => {
+ let component: GridConfigModifierComponent;
+ let fixture: ComponentFixture<GridConfigModifierComponent>;
+ let routerStub: FakeRouter;
+ let mockService: MockService;
+ let sessionContext: SessionContext;
+
+ routerStub = {
+ navigate: jasmine.createSpy('navigate').and.callThrough()
+ };
+
+ class FakeRouter {
+ navigate(commands: any[]) {
+ return commands[0];
+ }
+ }
+
+ class MockService extends AbstractMockObservableService {
+ setGridConfig() {
+ return this;
+ }
+ }
+
+ beforeEach(async(() => {
+ sessionContext = new SessionContext();
+ mockService = new MockService();
+
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule
+ ],
+ declarations: [GridConfigModifierComponent],
+ providers: [
+ SessionContext,
+ { provide: Router, useValue: routerStub },
+ { provide: ModifyGridConfigService, useValue: mockService },
+ { provide: ToasterMessageService }
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(GridConfigModifierComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should react on changeStatusBtn button click', async(() => {
+ spyOn(component, 'requestGridConfigChange').and.callThrough();
+ component.requestGridConfigChange(true);
+ fixture.detectChanges();
+ fixture.whenRenderingDone().then(() => {
+ expect(routerStub.navigate).toHaveBeenCalledWith(['/overview']);
+ expect(component.requestGridConfigChange).toHaveBeenCalled();
+ });
+
+ }));
+
+ it('should react on backBtn button click and go to overview', async(() => {
+ const button = fixture.debugElement.nativeElement.querySelector('div#backBtn');
+ button.click();
+ fixture.detectChanges();
+ fixture.whenRenderingDone().then(() => {
+ expect(routerStub.navigate).toHaveBeenCalledWith(['/overview']);
+ });
+
+ }));
+
+});
diff --git a/src/app/pages/grid-config-modifier/grid-config-modifier.component.ts b/src/app/pages/grid-config-modifier/grid-config-modifier.component.ts
new file mode 100644
index 0000000..5bd9fc6
--- /dev/null
+++ b/src/app/pages/grid-config-modifier/grid-config-modifier.component.ts
@@ -0,0 +1,48 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Component, OnInit } from '@angular/core';
+import { GridConfig } from '../../model/grid-config';
+import { ModifyGridConfigService } from '../../services/modify-grid-config.service';
+import { Router } from '@angular/router';
+
+@Component({
+ selector: 'app-status-change',
+ templateUrl: './grid-config-modifier.component.html',
+ styleUrls: ['./grid-config-modifier.component.css']
+})
+export class GridConfigModifierComponent implements OnInit {
+
+ modifyGridConfig = new GridConfig();
+
+ constructor(
+ public router: Router,
+ private modifyGridConfigService: ModifyGridConfigService
+ ) { }
+
+ ngOnInit() {
+
+ }
+
+ requestGridConfigChange(isTest?: boolean) {
+ this.modifyGridConfigService.setGridConfig(this.modifyGridConfig).subscribe(async (modGridConfService) => {
+ this.modifyGridConfig = modGridConfService;
+ });
+ this.goToOverview();
+ if (!isTest) {
+ window.location.reload();
+ }
+ }
+
+ goToOverview() {
+ this.router.navigate(['/overview']);
+ }
+}
diff --git a/src/app/pages/grid-measure-detail-header/grid-measure-detail-header.component.css b/src/app/pages/grid-measure-detail-header/grid-measure-detail-header.component.css
new file mode 100644
index 0000000..1998c70
--- /dev/null
+++ b/src/app/pages/grid-measure-detail-header/grid-measure-detail-header.component.css
@@ -0,0 +1,11 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
\ No newline at end of file
diff --git a/src/app/pages/grid-measure-detail-header/grid-measure-detail-header.component.html b/src/app/pages/grid-measure-detail-header/grid-measure-detail-header.component.html
new file mode 100644
index 0000000..22a21a1
--- /dev/null
+++ b/src/app/pages/grid-measure-detail-header/grid-measure-detail-header.component.html
@@ -0,0 +1,93 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+******************************************************************************
+
+-->
+<app-loading-spinner *ngIf="showSpinnerGrid"></app-loading-spinner>
+<div #gridMeasureDetailHeaderContainer>
+ <form #gridMeasureDetailHeaderForm="ngForm" *ngIf="!showSpinnerGrid" (change)="onGridMeasureHeaderFormValidation(gridMeasureDetailHeaderForm.form.valid)">
+ <fieldset class="form-group" disabled="{{ readOnlyForm ? 'disabled' : ''}}">
+
+ <!-- 1. Row -->
+ <div class="row">
+ <div class="col-md-2">
+ <label class="form-field-label" for="id">Nummer (ID)</label>
+ <input maxlength="256" [required]="false" type="text" name="id" id="id" [(ngModel)]="gridMeasureDetail.descriptiveId" class="form-control"
+ />
+ </div>
+ <div class="col-md-2">
+ <label class="form-field-label" for="branch">Sparte</label>
+ <select [required]="false" type="text" name="branch" id="branch" [(ngModel)]="gridMeasureDetail.branchId" class="form-control"
+ (change)="getBranchLevelsByBranch($event.target.value)">
+ <option value=""></option>
+ <option *ngFor="let branch of brancheList" value="{{ branch.id }}">{{ branch.description }}</option>
+ </select>
+ </div>
+ <div class="col-md-2">
+ <label class="form-field-label" for="level">Ebene</label>
+ <select [required]="false" type="text" name="level" id="level" [disabled]="!isBranchLevelActive" [(ngModel)]="gridMeasureDetail.branchLevelId"
+ class="form-control">
+ <option *ngFor="let levelObject of branchLevelList" value="{{levelObject.id}}">{{levelObject.name}}</option>
+ </select>
+ </div>
+ <div class="col-md-4">
+ <label class="form-field-label" for="areaOfSwitching">Gebiet (Region) der Maßnahme</label>
+ <input maxlength="256" [required]="true" type="text" list="areaOfSwitchingList" name="areaOfSwitching" id="areaOfSwitching"
+ [(ngModel)]="gridMeasureDetail.areaOfSwitching" class="form-control" autocomplete="off" />
+ <datalist id="areaOfSwitchingList">
+ <option *ngFor="let areaOfSwitchingListString of areaOfSwitchingList">{{areaOfSwitchingListString}}</option>
+ </datalist>
+ </div>
+ <div class="col-md-2">
+ <label class="form-field-label" for="statusId">Status</label>
+ <select [required]="false" type="text" name="statusId" id="statusId" [(ngModel)]="gridMeasureDetail.statusId" class="form-control">
+ <option *ngFor="let status of statusList" value="{{ status.id }}">{{ status.name }}</option>
+ </select>
+ </div>
+ </div>
+
+ <!-- 2. Row -->
+ <div class="row">
+ <div class="col-lg-12">
+ <label class="form-field-label" for="titleControl">Titel der Maßnahme</label>
+ <input maxlength="256" placeholder="Bitte Titel der Maßnahme ausfüllen" [required]="true" type="text" name="title" id="titleControl"
+ [(ngModel)]="gridMeasureDetail.title" #gmTitle (keyup)="onGridMeasureTitleChange(gmTitle.value)" class="form-control"
+ />
+ </div>
+ </div>
+
+ <!-- 3. Row -->
+ <div class="row">
+ <div class="col-md-4">
+ <label class="form-field-label" for="applicantControl">Antragsteller</label>
+ <input maxlength="256" type="text" name="applicant" id="applicantControl" [ngModel]="sessionContext.getUserMap().findAndRenderUser(gridMeasureDetail.createUser || sessionContext.getCurrUser().username) "
+ class="form-control" />
+ </div>
+ <div class="col-md-4">
+ <label class="form-field-label" for="createUserDepartment">Abteilung Ersteller</label>
+ <input maxlength="256" [required]="true" type="text" list="departmentList" name="createUserDepartment" id="createUserDepartment"
+ [(ngModel)]="gridMeasureDetail.createUserDepartment" class="form-control" autocomplete="off" />
+ <datalist id="departmentList">
+ <option *ngFor="let department of departmentList">{{ department.name }}</option>
+ </datalist>
+ </div>
+ <div class="col-md-4">
+ <label class="form-field-label" for="costCenter">Kostenstelle</label>
+ <input maxlength="256" [required]="true" type="text" list="costCenters" name="costCenter" id="costCenter" [(ngModel)]="gridMeasureDetail.costCenter"
+ class="form-control" autocomplete="off" />
+ <datalist id="costCenters">
+ <option *ngFor="let costCenter of costCenters">{{ costCenter.name }}</option>
+ </datalist>
+ </div>
+
+ </div>
+
+ </fieldset>
+ </form>
+</div>
\ No newline at end of file
diff --git a/src/app/pages/grid-measure-detail-header/grid-measure-detail-header.component.spec.ts b/src/app/pages/grid-measure-detail-header/grid-measure-detail-header.component.spec.ts
new file mode 100644
index 0000000..39fb972
--- /dev/null
+++ b/src/app/pages/grid-measure-detail-header/grid-measure-detail-header.component.spec.ts
@@ -0,0 +1,133 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { GridMeasureDetailHeaderComponent } from './grid-measure-detail-header.component';
+import { SessionContext } from '../../common/session-context';
+import { MockComponent } from '../../testing/mock.component';
+import { FormsModule } from '@angular/forms';
+import { BaseDataService } from '../../services/base-data.service';
+import { TERRITORY } from '../../test-data/territories';
+import { SimpleChange } from '@angular/core';
+import { AbstractMockObservableService } from '../../testing/abstract-mock-observable.service';
+import { USERS } from '../../test-data/users';
+
+describe('GridMeasureDetailHeaderComponent', () => {
+ let component: GridMeasureDetailHeaderComponent;
+ let fixture: ComponentFixture<GridMeasureDetailHeaderComponent>;
+ let sessionContext: SessionContext;
+ let mockBaseDataService;
+
+ class MockBaseDataService extends AbstractMockObservableService {
+ getBranchLevels(id: number) {
+ return this;
+ }
+ }
+
+ beforeEach(async(() => {
+ sessionContext = new SessionContext();
+ mockBaseDataService = new MockBaseDataService();
+
+ TestBed.configureTestingModule({
+ imports: [FormsModule],
+ declarations: [
+ GridMeasureDetailHeaderComponent,
+ MockComponent({ selector: 'app-loading-spinner', inputs: [] })],
+ providers: [
+ { provide: BaseDataService, useValue: mockBaseDataService },
+ SessionContext
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(GridMeasureDetailHeaderComponent);
+ sessionContext.setTerritories(TERRITORY);
+ sessionContext.setCurrUser(USERS[0]);
+ sessionContext.setAllUsers(USERS);
+ component = fixture.componentInstance;
+ // component.gridMeasureDetail.branchId = null;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should enable save button after filling required field', () => {
+ spyOn(component, 'onGridMeasureTitleChange').and.callThrough();
+
+ const newVal = 'TitleTest';
+ component.gridMeasureDetail.title = newVal;
+
+ // Todo call the change Event over dispatcher for Example not the method itself
+ component.onGridMeasureTitleChange(newVal);
+ fixture.detectChanges();
+
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect(component.isValidForSave).toBeTruthy('enabled for saving');
+ expect(component.onGridMeasureTitleChange).toHaveBeenCalled();
+ });
+
+ });
+
+
+
+ it('should disable save button after cleaning required field', () => {
+ spyOn(component, 'onGridMeasureTitleChange').and.callThrough();
+
+ const newVal = '';
+ component.gridMeasureDetail.title = newVal;
+
+ // Todo call the change Event over dispatcher for Example not the method itself
+ component.onGridMeasureTitleChange(newVal);
+ fixture.detectChanges();
+
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect((component as any).validForSave).toBeFalsy();
+ expect(component.onGridMeasureTitleChange).toHaveBeenCalled();
+ });
+
+ });
+
+ it('should result in invalid form after change on plannedEndtimeGridmeasure field', () => {
+ spyOn(component, 'onGridMeasureTitleChange').and.callThrough();
+
+ const newVal = 'TitleTest';
+ component.gridMeasureDetail.title = newVal;
+ component.gridMeasureDetail.plannedEndtimeGridmeasure = '2016-01-16T11:11:00z';
+
+ // Todo call the change Event over dispatcher for Example not the method itself
+ component.onGridMeasureTitleChange(newVal);
+ fixture.detectChanges();
+
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect((component as any).validForm).toBeFalsy();
+ expect(component.onGridMeasureTitleChange).toHaveBeenCalled();
+ });
+
+ });
+
+ it('should set form to readonly', () => {
+ component.readOnlyForm = false;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ isReadOnlyForm: new SimpleChange(component.readOnlyForm, true, true)
+ });
+ fixture.detectChanges();
+ expect(component.isValidForSave).toBeTruthy();
+ });
+
+});
diff --git a/src/app/pages/grid-measure-detail-header/grid-measure-detail-header.component.ts b/src/app/pages/grid-measure-detail-header/grid-measure-detail-header.component.ts
new file mode 100644
index 0000000..41a3f55
--- /dev/null
+++ b/src/app/pages/grid-measure-detail-header/grid-measure-detail-header.component.ts
@@ -0,0 +1,132 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import {
+ Component, OnInit, Input, AfterViewChecked, ViewChild, Output,
+ EventEmitter, OnChanges, SimpleChanges, ElementRef, AfterViewInit
+} from '@angular/core';
+import { GridMeasure } from '../../model/grid-measure';
+import { FormGroup } from '@angular/forms';
+import { Status } from '../../model/status';
+import { SessionContext } from '../../common/session-context';
+import { Branch } from '../../model/branch';
+import { BranchLevel } from '../../model/branch-level';
+import { BaseDataService } from '../../services/base-data.service';
+import { UserDepartment } from '../../model/user-department';
+
+@Component({
+ selector: 'app-grid-measure-detail-header',
+ templateUrl: './grid-measure-detail-header.component.html',
+ styleUrls: ['./grid-measure-detail-header.component.css', '../grid-measure-detail/grid-measure-detail.component.css']
+})
+export class GridMeasureDetailHeaderComponent implements OnInit, AfterViewChecked, OnChanges, AfterViewInit {
+
+ @Input() showSpinnerGrid: boolean;
+ @Input() gridMeasureDetail: GridMeasure = new GridMeasure();
+ @Input() isReadOnlyForm: boolean;
+ @Output() isValidForSave: EventEmitter<boolean> = new EventEmitter<boolean>();
+
+ @ViewChild('gridMeasureDetailHeaderForm') gridMeasureDetailHeaderForm: FormGroup;
+ @ViewChild('gridMeasureDetailHeaderContainer') gridMeasureDetailHeaderContainer: ElementRef;
+
+ statusList: Status[];
+ inactiveFields: Array<string> = [];
+ readOnlyForm: boolean;
+ brancheList: Branch[];
+ branchLevelList: Array<BranchLevel> = [];
+ areaOfSwitchingList: string[];
+ departmentList: UserDepartment[];
+ costCenters: any;
+ isBranchLevelActive: boolean;
+ constructor(public sessionContext: SessionContext,
+ private baseDataService: BaseDataService) { }
+
+ ngOnInit() {
+ this.getBranchLevelsByBranch(this.gridMeasureDetail.branchId);
+
+ this.inactiveFields = this.sessionContext.getInactiveFields();
+ this.statusList = this.sessionContext.getStatuses();
+ this.brancheList = this.sessionContext.getBranches();
+ this.areaOfSwitchingList = this.sessionContext.getTerritories().map(ter => ter.name);
+ this.departmentList = this.sessionContext.getAllUserDepartments();
+ this.costCenters = this.sessionContext.getCostCenters();
+
+ }
+
+ ngAfterViewInit() {
+ this.initInactiveFields();
+ }
+
+ ngAfterViewChecked() {
+
+ if (this.gridMeasureDetailHeaderForm) {
+ this.gridMeasureDetail._isHeaderValide = this.gridMeasureDetailHeaderForm.valid;
+ }
+
+ }
+
+ onGridMeasureHeaderFormValidation(valid: boolean) {
+ this.gridMeasureDetail._isHeaderValide = valid;
+ }
+
+ onGridMeasureTitleChange(value) {
+ if (value) {
+ this.isValidForSave.emit(true);
+ } else {
+ this.isValidForSave.emit(false);
+ }
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+
+ if (changes['isReadOnlyForm']) {
+ this.readOnlyForm = changes['isReadOnlyForm'].currentValue;
+ this.initInactiveFields();
+ }
+
+ }
+
+ public initInactiveFields() {
+ const el: HTMLElement = this.gridMeasureDetailHeaderContainer.nativeElement as HTMLElement;
+ const fields = el.querySelectorAll('*[id]');
+ for (let index = 0; index < fields.length; index++) {
+ const field = fields[index];
+ if (this.readOnlyForm || this.isFieldInactive(field['id'])) {
+ field.setAttribute('disabled', 'disabled');
+ } else {
+ field.removeAttribute('disabled');
+ }
+ }
+ }
+
+ private isFieldInactive(fieldName: string): boolean {
+ return this.inactiveFields.filter(field => field === fieldName).length > 0;
+ }
+
+ public getBranchLevelsByBranch(branchId: number) {
+ if (branchId) {
+ this.baseDataService.getBranchLevels(branchId)
+ .subscribe(branchLevels => {
+ this.branchLevelList = branchLevels,
+ this.isBranchLevelActive = (!branchId || branchLevels.length === 0) ? false : true;
+ },
+ error => {
+ console.log(error);
+ });
+ } else {
+ this.gridMeasureDetail.branchId = null;
+ this.gridMeasureDetail.branchLevelId = null;
+ this.branchLevelList = null;
+ this.isBranchLevelActive = false;
+ }
+
+ }
+}
diff --git a/src/app/pages/grid-measure-detail-tab/grid-measure-detail-tab.component.css b/src/app/pages/grid-measure-detail-tab/grid-measure-detail-tab.component.css
new file mode 100644
index 0000000..90b246a
--- /dev/null
+++ b/src/app/pages/grid-measure-detail-tab/grid-measure-detail-tab.component.css
@@ -0,0 +1,118 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+@media (min-width: 992px) {
+ .upload-label {
+ width: 40%;
+ margin-right: 15px;
+ }
+ .upload-label~* {
+ width: 100%;
+ }
+ .upload-buttons {
+ margin-bottom: 10px;
+ }
+ .documents {
+ margin-top: 10px;
+ }
+}
+
+.upload-buttons .fa {
+ margin-right: 6px;
+}
+
+#fileUploadLabel {
+ margin-right: 6px;
+}
+
+input[type="file"] {
+ display: none;
+}
+
+.documents {
+ width: 100%;
+}
+
+.documents table tr {
+ border: 1px solid #ddd;
+ border-radius: 3px;
+}
+
+.documents table td {
+ border: 0;
+}
+
+.documents table tr:nth-child(odd) {
+ background-color: #f5f8fc;
+}
+
+.documents table tr:hover {
+ background-color: #eee;
+}
+
+.documents .document-name {
+ cursor: pointer;
+ width: 100%;
+}
+
+.documents .document-buttons {
+ float: right;
+ min-width: 58px;
+}
+
+.documents .document-buttons span {
+ margin-right: 8px;
+ cursor: pointer;
+}
+
+.documents .document-buttons .remove {
+ color: #d9534f;
+}
+
+.documents .document-buttons .remove:hover {
+ color: #ac2925;
+}
+
+.documents .document-buttons .download {
+ color: #337ab7;
+}
+
+.documents .document-buttons .download:hover {
+ color: #286090;
+}
+
+.dropzone {
+ border: 3px dashed #ccc;
+ border-radius: 4px;
+ height: 80px;
+ font-weight: bold;
+}
+
+.dropzone-active {
+ border-color: #66afe9;
+ outline: 0;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6);
+ background: #dedcdc26;
+}
+
+.droptext {
+ display: -webkit-flex;
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ justify-content: center;
+ color: #ccc;
+}
\ No newline at end of file
diff --git a/src/app/pages/grid-measure-detail-tab/grid-measure-detail-tab.component.html b/src/app/pages/grid-measure-detail-tab/grid-measure-detail-tab.component.html
new file mode 100644
index 0000000..60d0b98
--- /dev/null
+++ b/src/app/pages/grid-measure-detail-tab/grid-measure-detail-tab.component.html
@@ -0,0 +1,173 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+******************************************************************************
+
+-->
+<app-loading-spinner *ngIf="showSpinnerGrid"></app-loading-spinner>
+<div #gridMeasureDetailTabContainer>
+ <form #gridMeasureDetailForm="ngForm" *ngIf="!showSpinnerGrid" (change)="onGridMeasureDetailFormValidation(gridMeasureDetailForm.form.valid)">
+ <fieldset class="form-group" disabled="{{ readOnlyForm ? 'disabled' : ''}}">
+
+ <!-- 1. row -->
+ <div class="row">
+ <div class="col-md-4">
+ <label class="form-field-label" for="plannedStarttimeFirstSinglemeasure">Beginn der ersten geplanten Einzelmaßnahme</label>
+ <div class="input-group">
+ <label class="input-group-addon dateRangePickerIcon" for="plannedStarttimeFirstSinglemeasure" style="cursor: no-drop;">
+ <span class="glyphicon glyphicon-calendar"></span>
+ </label>
+ <input disabled="disabled" #input *ngIf="gridMeasureDetail.plannedStarttimeFirstSinglemeasure" maxlength="256" [required]="true"
+ type="text" name="plannedStarttimeFirstSinglemeasure" id="plannedStarttimeFirstSinglemeasure" class="form-control"
+ [ngModel]="gridMeasureDetail.plannedStarttimeFirstSinglemeasure|date:dateFormatLocale" />
+ <input disabled="disabled" #input *ngIf="!gridMeasureDetail.plannedStarttimeFirstSinglemeasure" maxlength="256" [required]="true"
+ type="text" [ngModel]="gridMeasureDetail.plannedStarttimeFirstSinglemeasure" name="plannedStarttimeFirstSinglemeasure"
+ id="plannedStarttimeFirstSinglemeasure" class="form-control" />
+ </div>
+ </div>
+ <div class="col-md-4">
+ <label class="form-field-label" for="endtimeGridmeasure">Ende der Netzmaßnahme</label>
+ <div class="input-group">
+ <label class="input-group-addon dateRangePickerIcon" for="endtimeGridmeasure" style="cursor: no-drop;">
+ <span class="glyphicon glyphicon-calendar"></span>
+ </label>
+ <input disabled="disabled" #input *ngIf="gridMeasureDetail.endtimeGridmeasure" maxlength="256" [required]="false" type="text"
+ [ngModel]="gridMeasureDetail.endtimeGridmeasure|date:dateFormatLocale" name="endtimeGridmeasure" id="endtimeGridmeasure"
+ class="form-control" />
+ <input disabled="disabled" #input *ngIf="!gridMeasureDetail.endtimeGridmeasure" maxlength="256" [required]="false" type="text"
+ [ngModel]="gridMeasureDetail.endtimeGridmeasure" name="endtimeGridmeasure" id="endtimeGridmeasure" class="form-control"
+ />
+ </div>
+ </div>
+ <div class="col-md-4">
+ <label class="form-field-label" for="affectedResourceControl">Betroffenes Objekt / Betriebsmittel</label>
+ <input maxlength="256" [required]="true" type="text" name="affectedResource" id="affectedResourceControl" list="affectedResourcesList"
+ [(ngModel)]="gridMeasureDetail.affectedResource" autocomplete="off" class="form-control" />
+ <datalist id="affectedResourcesList">
+ <option *ngFor="let affectedResources of affectedResourcesList" [ngValue]="affectedResources">{{affectedResources}}</option>
+ </datalist>
+ </div>
+ </div>
+
+ <!-- 2. row -->
+ <div class="row">
+ <div class="col-md-4">
+ <label class="form-field-label" for="appointmentNumberOf">Häufigkeit</label>
+ <input maxlength="256" [required]="true" type="number" min="0" oninput="validity.valid||(value='');" name="appointmentNumberOf"
+ id="appointmentNumberOf" [(ngModel)]="gridMeasureDetail.appointmentNumberOf" (ngModelChange)="checkAppointmentNumberOfValue($event)"
+ class="form-control" />
+ </div>
+ <div class="col-md-4">
+ <label class="form-field-label" for="appointmentRepetition">Wiederholung</label>
+ <select [required]="true" type="text" name="appointmentRepetition" id="appointmentRepetition" [(ngModel)]="gridMeasureDetail.appointmentRepetition"
+ (change)="gridMeasureDetail.appointmentRepetition = $event.target.value" class="form-control">
+ <option *ngFor="let appointmentRepetitionString of appointmentRepetitionList" value="{{appointmentRepetitionString}}" [selected]="appointmentRepetitionString === gridMeasureDetail.appointmentRepetition">{{appointmentRepetitionString}}</option>
+ </select>
+ </div>
+ <div class="col-md-4">
+ <label class="form-field-label" for="timeOfReallocation">Wiederbereitstellungszeit</label>
+ <input maxlength="256" [required]="true" type="text" name="timeOfReallocation" id="timeOfReallocation" [(ngModel)]="gridMeasureDetail.timeOfReallocation"
+ class="form-control" />
+ </div>
+ </div>
+
+ <!-- 3. row -->
+ <div class="row">
+ <div class="col-md-4">
+ <label style="height: 2em;"></label>
+ <p *ngIf="!isAppointmentNumberOfValid" style="font-weight: bold;color:red">
+ <small>Der eingetragene Wert überschreitet das zulässige Limit</small>
+ </p>
+ </div>
+ </div>
+
+ <!-- 4. row -->
+ <div class="row">
+ <div class="col-lg-12">
+ <label class="form-field-label" for="description">
+ Beschreibung der Maßnahme
+ </label>
+ <textarea style="resize:none;" maxlength="1024" [required]="true" rows="3" name="description" id="description" [(ngModel)]="gridMeasureDetail.description"
+ class="form-control"></textarea>
+ </div>
+ </div>
+
+ <!-- 5. row -->
+ <div class="row">
+ <div class="col-lg-12">
+ <label class="form-field-label" for="remark">Bemerkungen</label>
+ <textarea style="resize:none" maxlength="1024" rows="3" [required]="false" name="remark" id="remark" [(ngModel)]="gridMeasureDetail.remark"
+ class="form-control" #gmRemark></textarea>
+ </div>
+ </div>
+ </fieldset>
+
+ <fieldset class="form-group" disabled="{{ readOnlyForm ? 'disabled' : ''}}">
+ <div class="row">
+ <div class="col-md-4">
+ <div class="upload-buttons">
+ <label for="file" class="btn btn-primary" id="fileUploadLabel">
+ <input type="file" onclick="this.value = null" id="file" (change)="handleFileInput($event.target.files)">
+ <i class="fa fa-folder"></i>Datei auswählen
+ </label>
+ <button type="button" [disabled]="!id || !fileSelected" (click)="uploadDocument()" class="btn btn-primary">
+ <i class="fa fa-cloud-upload"></i>Datei hochladen
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-md-4">
+ <label class="upload-label">Anhang</label>
+ <input class="form-control" type="text" name="fileName" id="fileName" [ngModel]="fileName" readonly>
+ </div>
+ <div class="col-md-4">
+ <label class="form-field-label" for="modUser">Letzter Bearbeiter</label>
+ <input maxlength="256" type="text" name="modUser" id="modUser" [ngModel]="sessionContext.getUserMap().findAndRenderUser(gridMeasureDetail.modUser|| sessionContext.getCurrUser().username) "
+ class="form-control" />
+ </div>
+ <div class="col-md-4">
+ <label class="form-field-label" for="modUserDepartment">Abteilung letzter Bearbeiter</label>
+ <input maxlength="256" [required]="true" type="text" list="departmentList" name="modUserDepartment" id="modUserDepartment"
+ [(ngModel)]="gridMeasureDetail.modUserDepartment" class="form-control" autocomplete="off" />
+ <datalist id="departmentList">
+ <option *ngFor="let department of departmentList">{{ department.name }}</option>
+ </datalist>
+ </div>
+
+ </div>
+ <div class="row">
+ <div class="col-md-4">
+ <div class="dropzone droptext" [ngClass]="{'dropzone-active':this.dragEntered}" (drop)="drop($event)" (dragover)="allowDrop($event)"
+ (dragleave)="dragleave($event)" (dragenter)="dragenter($event)">
+ Datei ziehen und hier ablegen
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-4">
+ <div *ngIf="listOfDocuments && listOfDocuments.length > 0" class="documents">
+ <label>Anhang</label>
+ <table class="table table-striped">
+ <tr *ngFor="let document of listOfDocuments;let i = index">
+ <td class="document-name" (click)="downloadDocument(document.id)">{{document.documentName}}</td>
+ <td class="document-buttons">
+ <span *ngIf="this.gridMeasureDetail.statusId < Globals.STATUS.APPLIED && !readOnlyForm" (click)="deleteDocument(document.id, i)"
+ class="glyphicon glyphicon-remove remove"></span>
+ <span (click)="downloadDocument(document.id)" class="glyphicon glyphicon-download-alt download"></span>
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </div>
+ <app-loading-spinner *ngIf="showSpinnerFileUpload"></app-loading-spinner>
+ </fieldset>
+ </form>
+</div>
\ No newline at end of file
diff --git a/src/app/pages/grid-measure-detail-tab/grid-measure-detail-tab.component.spec.ts b/src/app/pages/grid-measure-detail-tab/grid-measure-detail-tab.component.spec.ts
new file mode 100644
index 0000000..5714194
--- /dev/null
+++ b/src/app/pages/grid-measure-detail-tab/grid-measure-detail-tab.component.spec.ts
@@ -0,0 +1,648 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { async, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { SimpleChange } from '@angular/core';
+import { MockComponent } from '../../testing/mock.component';
+import { StringToDatePipe } from '../../common-components/pipes/string-to-date.pipe';
+import { SessionContext } from '../../common/session-context';
+import { DocumentService } from './../../services/document.service';
+import { AbstractMockObservableService } from '../../testing/abstract-mock-observable.service';
+import { RoleAccessHelperService } from '../../services/jobs/role-access-helper.service';
+import { GridMeasureDetailTabComponent } from './grid-measure-detail-tab.component';
+import { USERS } from '../../test-data/users';
+import { DOCUMENTS } from './../../test-data/documents';
+import { GRIDMEASURE } from '../../test-data/grid-measures';
+import { RoleAccess } from '../../model/role-access';
+import { Globals } from '../../common/globals';
+import { Document } from '../../model/document';
+import { GridMeasure } from '../../model/grid-measure';
+import * as FileSaver from 'file-saver';
+import { GridMeasureService } from '../../services/grid-measure.service';
+import { BACKENDSETTINGS } from '../../test-data/backend-settings';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+import { MessageService } from 'primeng/api';
+
+describe('GridMeasureDetailTabComponent', () => {
+ let component: GridMeasureDetailTabComponent;
+ let fixture: ComponentFixture<GridMeasureDetailTabComponent>;
+ let sessionContext: SessionContext;
+ let toasterMessageService: ToasterMessageService;
+ let gridmeasures: GridMeasure[];
+ let messageService: MessageService;
+
+ let mockGridMeasureService;
+ let mockDocService: MockDocumentService;
+ let roleAccessHelper: RoleAccessHelperService;
+
+ class MockDocumentService extends AbstractMockObservableService {
+ public uploadGridMeasureAttachments(gridmeasuereId: number, file: File) {
+ return this;
+ }
+
+ public getGridMeasureAttachments(gridmeasuereId: number) {
+ return this;
+ }
+ public deleteGridMeasureAttachment(documentId: number, index: number) {
+ return this;
+ }
+
+ public downloadGridMeasureAttachment(documentId: number) {
+ return this;
+ }
+ }
+
+ class MockGridMeasureService extends AbstractMockObservableService {
+ getAffectedResourcesDistinct() {
+ return this;
+ }
+ }
+
+ let originalTimeout;
+
+ beforeEach(async(() => {
+ messageService = new MessageService;
+ originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
+ jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000;
+ gridmeasures = JSON.parse(JSON.stringify(GRIDMEASURE));
+ sessionContext = new SessionContext();
+ mockDocService = new MockDocumentService();
+ mockGridMeasureService = new MockGridMeasureService();
+
+ roleAccessHelper = new RoleAccessHelperService();
+ toasterMessageService = new ToasterMessageService(sessionContext, messageService);
+
+ TestBed.configureTestingModule({
+ imports: [FormsModule],
+ declarations: [
+ GridMeasureDetailTabComponent,
+ StringToDatePipe,
+ MockComponent({ selector: 'input', inputs: ['options'] }),
+ MockComponent({ selector: 'app-loading-spinner', inputs: [] })
+ ],
+
+ providers: [
+ SessionContext,
+ { provide: DocumentService, useValue: mockDocService },
+ { provide: GridMeasureService, useValue: mockGridMeasureService },
+ { provide: RoleAccessHelperService, useValue: roleAccessHelper },
+ { provide: ToasterMessageService, useValue: toasterMessageService }
+ ]
+ })
+ .compileComponents();
+ }));
+
+
+ beforeEach(async(() => {
+ sessionContext.setCurrUser(USERS[0]);
+ sessionContext.setAllUsers(USERS);
+ // we need to init the component and the path... because of OnInit
+ mockDocService.content = [{ id: 1, documentName: 'docdoc.doc' }];
+ const roleAcess: RoleAccess = {
+ editRoles: [{
+ name: 'planned-policies-measureplanner',
+ gridMeasureStatusIds: [
+ 0,
+ 1
+ ]
+ }, {
+ name: 'planned-policies-superuser',
+ gridMeasureStatusIds: [
+ 0,
+ 1
+ ]
+ }, {
+ name: 'planned-policies-measureapplicant',
+ gridMeasureStatusIds: [
+ 0,
+ 1
+ ]
+ }],
+ controls: [{
+ gridMeasureStatusId: 0,
+ activeButtons: [
+ 'save',
+ 'apply',
+ 'cancel'
+ ],
+ inactiveFields: [
+ 'titeldermassnahme'
+ ]
+ },
+ {
+ gridMeasureStatusId: 1,
+ activeButtons: [
+ 'save',
+ 'cancel',
+ 'forapproval'
+ ],
+ inactiveFields: [
+ 'titeldermassnahme'
+ ]
+ }],
+ stornoSection:
+ {
+ 'stornoRoles': [
+ 'planned-policies-measureapplicant',
+ 'planned-policies-measureplanner',
+ 'planned-policies-measureapprover',
+ 'planned-policies-requester',
+ 'planned-policies-clearance'
+ ]
+ },
+ duplicateSection:
+ {
+ 'duplicateRoles': [
+ 'planned-policies-measureapplicant'
+ ]
+ }
+ };
+ roleAccessHelper.init(roleAcess);
+
+ mockGridMeasureService.content = JSON.parse(JSON.stringify(GRIDMEASURE));
+
+ sessionContext.setBackendsettings(JSON.parse(JSON.stringify(BACKENDSETTINGS)));
+ fixture = TestBed.createComponent(GridMeasureDetailTabComponent);
+ component = fixture.componentInstance;
+ }));
+
+ afterEach(async(() => {
+ jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
+ }));
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should be in able to give an input value < 1000 in the field appointmentNumberOf', async(() => {
+ component.gridMeasureDetail.appointmentNumberOf = 999;
+ spyOn(component, 'checkAppointmentNumberOfValue').and.callThrough();
+ component.checkAppointmentNumberOfValue(component.gridMeasureDetail.appointmentNumberOf);
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect(component.checkAppointmentNumberOfValue).toHaveBeenCalled();
+ expect(component.isAppointmentNumberOfValid).toBeTruthy();
+ });
+ }));
+
+ it('should react if input value > 999 in the field appointmentNumberOf', async(() => {
+ fixture.detectChanges();
+ component.gridMeasureDetail.appointmentNumberOf = 1000;
+ spyOn(component, 'checkAppointmentNumberOfValue').and.callThrough();
+ component.checkAppointmentNumberOfValue(component.gridMeasureDetail.appointmentNumberOf);
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect(component.checkAppointmentNumberOfValue).toHaveBeenCalled();
+ expect(component.isAppointmentNumberOfValid).toBeFalsy();
+ expect(component.gridMeasureDetail.appointmentNumberOf).toBe(0);
+ });
+
+ }));
+
+ it('should set form to readonly', () => {
+ component.readOnlyForm = false;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ readOnlyForm: new SimpleChange(component.readOnlyForm, true, true)
+ });
+ fixture.detectChanges();
+ expect(component.isValidForSave).toBeTruthy();
+ });
+
+ it('should set current date correctly', () => {
+ const datevalue = new Date().toISOString();
+ console.log(datevalue.substr(0, 16));
+ expect(datevalue.substr(0, 16)).toBe(component.getCurrentDateTime().substr(0, 16));
+
+ });
+
+ it('should enable save button after filling required field', () => {
+ spyOn(component, 'onGridMeasureTitleChange').and.callThrough();
+
+ const newVal = 'TitleTest';
+ component.gridMeasureDetail.title = newVal;
+
+ // Todo call the change Event over dispatcher for Example not the method itself
+ component.onGridMeasureTitleChange(newVal);
+ fixture.detectChanges();
+
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect(component.isValidForSave).toBeTruthy('enabled for saving');
+ expect(component.onGridMeasureTitleChange).toHaveBeenCalled();
+ });
+
+ });
+
+
+
+ it('should disable save button after cleaning required field', () => {
+ spyOn(component, 'onGridMeasureTitleChange').and.callThrough();
+
+ const newVal = '';
+ component.gridMeasureDetail.title = newVal;
+
+ // Todo call the change Event over dispatcher for Example not the method itself
+ component.onGridMeasureTitleChange(newVal);
+ fixture.detectChanges();
+
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect((component as any).validForSave).toBeFalsy();
+ expect(component.onGridMeasureTitleChange).toHaveBeenCalled();
+ });
+
+ });
+
+ it('should result in invalid form after change on plannedEndtimeGridmeasure field', () => {
+ spyOn(component, 'onGridMeasureTitleChange').and.callThrough();
+
+ const newVal = 'TitleTest';
+ component.gridMeasureDetail.title = newVal;
+ component.gridMeasureDetail.plannedEndtimeGridmeasure = '2016-01-16T11:11:00z';
+
+ // Todo call the change Event over dispatcher for Example not the method itself
+ component.onGridMeasureTitleChange(newVal);
+ fixture.detectChanges();
+
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect((component as any).validForm).toBeFalsy();
+ expect(component.onGridMeasureTitleChange).toHaveBeenCalled();
+ });
+
+ });
+
+ ////////// UPLOAD DOCUMENT ////////////////
+
+ it('should delete attached document', async(() => {
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[2]));
+ component.gridMeasureDetail.statusId = Globals.STATUS.NEW;
+ component.readOnlyForm = false;
+
+ spyOn(component, 'deleteDocument').and.callThrough();
+ component.listOfDocuments = DOCUMENTS;
+
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ const button = fixture.debugElement.nativeElement.querySelector('span.remove');
+ button.click();
+ fixture.detectChanges();
+ expect(component.deleteDocument).toHaveBeenCalled();
+
+ });
+ }));
+
+ it('should handleFileInput correctly', async(() => {
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[2]));
+ component.gridMeasureDetail.statusId = Globals.STATUS.APPLIED;
+ component.readOnlyForm = false;
+
+ // specs compliant (as of March 2018 only Chrome)
+ // Firefox < 62 workaround exploiting https://bugzilla.mozilla.org/show_bug.cgi?id=1422655
+ const fileList = new ClipboardEvent('').clipboardData || new DataTransfer();
+ const fileMock = new File(['foo'], 'programmatically_created.pdf', { type: 'application/pdf' });
+ fileList.items.add(fileMock);
+
+ component.handleFileInput(fileList.files);
+
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect(component.fileSelected).toBeTruthy();
+
+ });
+ }));
+
+ xit('should show a message and fail on handleFileInput: file already exists and status is applied', async(() => {
+ fixture.detectChanges();
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[2]));
+ component.gridMeasureDetail.statusId = Globals.STATUS.APPLIED;
+ component.readOnlyForm = false;
+
+ // TODO fill it via getDocumentsForId
+ component.listOfDocumentsNames.push(DOCUMENTS[1].documentName);
+ fixture.detectChanges();
+
+ spyOn(toasterMessageService, 'showWarn').and.callThrough();
+
+ // specs compliant (as of March 2018 only Chrome)
+ // Firefox < 62 workaround exploiting https://bugzilla.mozilla.org/show_bug.cgi?id=1422655
+ const fileList = new ClipboardEvent('').clipboardData || new DataTransfer();
+ const fileMock = new File(['foo'], 'test6.txt', { type: 'application/pdf' });
+ fileList.items.add(fileMock);
+
+ component.handleFileInput(fileList.files);
+
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect(component.fileSelected).toBeFalsy();
+ expect(toasterMessageService.showWarn).toHaveBeenCalled();
+
+ });
+ }));
+
+ it('should show a message: multiple files selected', async(() => {
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[2]));
+ component.gridMeasureDetail.statusId = Globals.STATUS.APPLIED;
+ component.readOnlyForm = false;
+
+ spyOn(toasterMessageService, 'showWarn').and.callThrough();
+
+ // specs compliant (as of March 2018 only Chrome)
+ // Firefox < 62 workaround exploiting https://bugzilla.mozilla.org/show_bug.cgi?id=1422655
+ const fileList = new ClipboardEvent('').clipboardData || new DataTransfer();
+ const fileMock = new File(['foo'], 'programmatically_created.pdf', { type: 'application/pdf' });
+ const fileMock2 = new File(['foo'], 'programmatically_created2.pdf', { type: 'application/pdf' });
+ fileList.items.add(fileMock);
+ fileList.items.add(fileMock2);
+
+ component.handleFileInput(fileList.files);
+
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect(component.fileSelected).toBeTruthy();
+ expect(toasterMessageService.showWarn).toHaveBeenCalled();
+
+ });
+ }));
+
+ it('should fail on handleFileInput: filList length = 0', async(() => {
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[2]));
+ component.gridMeasureDetail.statusId = Globals.STATUS.APPLIED;
+ component.readOnlyForm = false;
+
+ // specs compliant (as of March 2018 only Chrome)
+ // Firefox < 62 workaround exploiting https://bugzilla.mozilla.org/show_bug.cgi?id=1422655
+ const fileList = new ClipboardEvent('').clipboardData || new DataTransfer();
+
+ component.handleFileInput(fileList.files);
+
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect(component.fileSelected).toBeFalsy();
+ });
+ }));
+
+ it('should fail on handleFileInput: no file', async(() => {
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[2]));
+ component.gridMeasureDetail.statusId = Globals.STATUS.APPLIED;
+ component.readOnlyForm = false;
+
+ component.handleFileInput(undefined);
+
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect(component.fileSelected).toBeFalsy();
+ });
+ }));
+
+ it('should handle error while delete attached document', async(() => {
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[2]));
+ component.gridMeasureDetail.statusId = Globals.STATUS.NEW;
+ component.readOnlyForm = false;
+
+ spyOn(component, 'deleteDocument').and.callThrough();
+ spyOn(toasterMessageService, 'showError').and.callThrough();
+
+ component.listOfDocuments = DOCUMENTS;
+
+ mockDocService.error = 'process error';
+
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ const button = fixture.debugElement.nativeElement.querySelector('span.remove');
+ button.click();
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ expect(toasterMessageService.showError).toHaveBeenCalled();
+ expect(component.showSpinnerFileUpload).toBeFalsy();
+ });
+
+ });
+ }));
+
+ it('should handle a wrong filetype and size correctly', fakeAsync(() => {
+ spyOn(toasterMessageService, 'showWarn').and.callThrough();
+ component.Globals.MAX_UPLOADFILE_SIZE = 0;
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ component.fileToUpload = new File(['testfile'], 'testfile.app', {
+ type: 'application/*'
+ });
+
+ let returnValue = true;
+ fixture.detectChanges();
+ tick();
+
+ returnValue = (component as any).fileTypeCheck();
+ tick();
+ expect(returnValue).toBeFalsy();
+ expect(toasterMessageService.showWarn).toHaveBeenCalled();
+
+ returnValue = true;
+ fixture.detectChanges();
+ tick();
+
+ returnValue = (component as any).fileSizeCheck();
+ tick();
+ expect(returnValue).toBeFalsy();
+ expect(toasterMessageService.showWarn).toHaveBeenCalled();
+ }));
+
+ xit('should upload attached document', async(() => {
+ fixture.detectChanges();
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[2]));
+ component.gridMeasureDetail.statusId = Globals.STATUS.APPLIED;
+
+ // specs compliant (as of March 2018 only Chrome)
+ // Firefox < 62 workaround exploiting https://bugzilla.mozilla.org/show_bug.cgi?id=1422655
+ const fileList = new ClipboardEvent('').clipboardData || new DataTransfer();
+ const fileMock = new File(['foo'], 'programmatically_created.pdf', { type: 'application/pdf' });
+ fileList.items.add(fileMock);
+
+ spyOn(component, 'uploadDocument').and.callThrough();
+
+ fixture.detectChanges();
+
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ component.handleFileInput(fileList.files);
+ fixture.detectChanges();
+
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ const button = fixture.debugElement.nativeElement.querySelector('upload-buttons.button');
+ button.click();
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect(component.uploadDocument).toHaveBeenCalled();
+ });
+
+ });
+
+ });
+ }));
+ it('should process upload gridmeasure attachment correctly', fakeAsync(() => {
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[2]));
+ component.gridMeasureDetail.statusId = Globals.STATUS.NEW;
+ component.documentToUpload = new Document();
+ mockDocService.content = { dummyret: 1 };
+
+ (component as any).processUpload();
+
+ tick();
+
+ expect(component.showSpinnerGridFileUpload).toBeFalsy();
+ expect(component.fileSelected).toBeFalsy();
+
+
+ }));
+ it('should handle error while process upload correctly', fakeAsync(() => {
+ spyOn(console, 'log').and.callThrough();
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[2]));
+ component.gridMeasureDetail.statusId = Globals.STATUS.NEW;
+ component.documentToUpload = new Document();
+ mockDocService.error = 'process error';
+
+ (component as any).processUpload();
+
+ tick();
+
+ expect(component.showSpinnerGridFileUpload).toBeFalsy();
+ expect(console.log).toHaveBeenCalled();
+
+
+ }));
+
+ it('should download attached document', async(() => {
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[2]));
+ component.gridMeasureDetail.statusId = Globals.STATUS.NEW;
+ component.listOfDocuments = DOCUMENTS;
+
+ mockDocService.content = DOCUMENTS[0];
+ spyOn(component, 'downloadDocument').and.callThrough();
+ spyOn(FileSaver, 'saveAs').and.stub();
+
+ // (component as any).onReceiveGridMeasureDetail();
+ fixture.detectChanges();
+
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+
+ const button = fixture.debugElement.nativeElement.querySelector('span.download');
+ button.click();
+ fixture.detectChanges();
+ expect(component.downloadDocument).toHaveBeenCalled();
+
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect(FileSaver.saveAs).toHaveBeenCalled();
+
+ });
+
+ });
+ }));
+
+ it('should handle error while downloading an attached document', async(() => {
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[2]));
+ component.gridMeasureDetail.statusId = Globals.STATUS.NEW;
+ mockDocService.content = undefined;
+ mockDocService.error = 'Error while downloading Doc';
+ component.listOfDocuments = DOCUMENTS;
+
+ spyOn(component, 'downloadDocument').and.callThrough();
+ spyOn(FileSaver, 'saveAs').and.stub();
+
+ fixture.detectChanges();
+
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+
+ const button = fixture.debugElement.nativeElement.querySelector('span.download');
+ button.click();
+ fixture.detectChanges();
+ expect(component.downloadDocument).toHaveBeenCalled();
+ expect(component.showSpinnerGridFileUpload).toBeFalsy();
+ });
+ }));
+
+ const customTestTimeout: number = 1 * 60 * 1000; // explicitly set for readabilty
+
+ it('should set time correctly', async(() => {
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[2]));
+ component.gridMeasureDetail.statusId = Globals.STATUS.NEW;
+ component.readOnlyForm = false;
+
+ const mockDate = new Date().toISOString();
+
+ component.gridMeasureDetail.id = undefined;
+ component.gridMeasureDetail.appointmentStartdate = undefined;
+ component.gridMeasureDetail.plannedStarttimeFirstSequence = undefined;
+ component.gridMeasureDetail.plannedEndtimeLastSinglemeasure = undefined;
+ component.gridMeasureDetail.plannedEndtimeGridmeasure = undefined;
+ component.gridMeasureDetail.plannedStarttimeFirstSinglemeasure = undefined;
+ component.setCurrTimeIfEmpty(component.gridMeasureDetail);
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect(component.gridMeasureDetail.appointmentStartdate.substr(0, 16)).toBe(mockDate.substr(0, 16));
+ expect(component.gridMeasureDetail.plannedStarttimeFirstSequence.substr(0, 16)).toBe(mockDate.substr(0, 16));
+ expect(component.gridMeasureDetail.plannedEndtimeLastSinglemeasure.substr(0, 16)).toBe(mockDate.substr(0, 16));
+ expect(component.gridMeasureDetail.plannedEndtimeGridmeasure.substr(0, 16)).toBe(mockDate.substr(0, 16));
+ expect(component.gridMeasureDetail.plannedStarttimeFirstSinglemeasure.substr(0, 16)).toBe(mockDate.substr(0, 16));
+ });
+ }), customTestTimeout);
+
+ it('should set appointment times after changes on gridMeasureDetail', () => {
+ spyOn(component, 'setCurrTimeIfEmpty').and.callThrough();
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[2]));
+ fixture.detectChanges();
+ component.ngOnChanges({
+ gridMeasureDetail: new SimpleChange(component.gridMeasureDetail, true, true)
+ });
+ fixture.detectChanges();
+ expect(component.setCurrTimeIfEmpty).toHaveBeenCalled();
+ });
+
+ it('should call getDocumentsForId after changes on id', () => {
+ spyOn((component as any), 'getDocumentsForId').and.callThrough();
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[2]));
+ component.id = 3333;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ id: new SimpleChange(component.id, true, true)
+ });
+ fixture.detectChanges();
+ expect((component as any).getDocumentsForId).toHaveBeenCalled();
+ });
+
+ it('should call getDocumentsForId after changes on id with error in documentservice', () => {
+ spyOn((component as any), 'getDocumentsForId').and.callThrough();
+ spyOn(console, 'log').and.callThrough();
+ mockDocService.error = 'Error in Documentservice';
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[2]));
+ component.id = 666;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ id: new SimpleChange(component.id, true, true)
+ });
+ fixture.detectChanges();
+ expect((component as any).getDocumentsForId).toHaveBeenCalled();
+ expect(console.log).toHaveBeenCalled();
+ });
+});
diff --git a/src/app/pages/grid-measure-detail-tab/grid-measure-detail-tab.component.ts b/src/app/pages/grid-measure-detail-tab/grid-measure-detail-tab.component.ts
new file mode 100644
index 0000000..df30565
--- /dev/null
+++ b/src/app/pages/grid-measure-detail-tab/grid-measure-detail-tab.component.ts
@@ -0,0 +1,374 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import {
+ Component, OnInit, Input, ElementRef, ViewChild, AfterContentChecked,
+ SimpleChanges, OnChanges, Output, AfterViewChecked
+} from '@angular/core';
+import { FormGroup } from '@angular/forms';
+import { SessionContext } from '../../common/session-context';
+import { GridMeasure } from '../../model/grid-measure';
+import { Globals } from '../../common/globals';
+import { ErrorType } from '../../common/enums';
+import { DocumentService } from '../../services/document.service';
+import { Document } from './../../model/document';
+import { saveAs } from 'file-saver/FileSaver';
+import { RoleAccessHelperService } from '../../services/jobs/role-access-helper.service';
+import { EventEmitter, AfterViewInit } from '@angular/core';
+import { Util } from '../../common/util';
+import { GridMeasureService } from '../../services/grid-measure.service';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+
+@Component({
+ selector: 'app-grid-measure-detail-tab',
+ templateUrl: './grid-measure-detail-tab.component.html',
+ styleUrls: ['./grid-measure-detail-tab.component.css', '../grid-measure-detail/grid-measure-detail.component.css']
+})
+export class GridMeasureDetailTabComponent implements OnInit, OnChanges, AfterViewChecked, AfterViewInit {
+
+ @Input() showSpinnerGrid: boolean;
+ @Input() id: any;
+ @Input() gridMeasureDetail: GridMeasure = new GridMeasure();
+ @Input() readOnlyForm: boolean;
+ @Output() isValidForSave: EventEmitter<boolean> = new EventEmitter<boolean>();
+
+ Globals = Globals;
+ fileToUpload: File = null;
+ fileName: string;
+ fileSelected: boolean;
+ documentToUpload: Document = null;
+ listOfDocuments: Document[] = null;
+ listOfDocumentsNames: string[] = [];
+ dataURI: string;
+ showSpinnerFileUpload = false;
+ dragEntered = false;
+ isAppointmentNumberOfValid: boolean;
+ switchingObjectList: string[];
+ appointmentRepetitionList: string[];
+ showSpinnerGridFileUpload: boolean;
+ dateFormatLocale = 'dd.MM.yyyy HH:mm';
+ affectedResourcesList: Array<string> = [];
+ inactiveFields: Array<string> = [];
+ departmentList: any;
+
+ datePattern = '^(([0-2]?[0-9]|3[0-1])\.([0]?[1-9]|1[0-2])\.[1-2][0-9]{3})$';
+ dateTimePattern = '^(([0-2]?[0-9]|3[0-1])\.([0]?[1-9]|1[0-2])\.[1-2][0-9]{3}) (20|21|22|23|[0-1]?[0-9]{1}):([0-5]?[0-9]{1})$';
+ calcDatepickerDropOrientation = Util.calcDatepickerDropOrientation;
+
+ @ViewChild('gridMeasureDetailForm') gridMeasureDetailForm: FormGroup;
+ @ViewChild('gridMeasureDetailTabContainer') gridMeasureDetailTabContainer: ElementRef;
+
+
+ constructor(public sessionContext: SessionContext,
+ private documentService: DocumentService,
+ public roleAccessHelper: RoleAccessHelperService,
+ protected gridMeasureService: GridMeasureService,
+ private toasterMessageService: ToasterMessageService) { }
+
+ ngOnInit() {
+
+ this.inactiveFields = this.sessionContext.getInactiveFields();
+ this.isAppointmentNumberOfValid = true;
+
+ this.getAffectedResourcesDistinct();
+
+ // TODO auslagern in Globals bzw. je nach Prozess automatisch laden (separate User Story)
+ this.switchingObjectList = ['UW A', 'UW B', 'test1', 'test2'];
+ this.appointmentRepetitionList = this.sessionContext.getBackendsettings().appointmentRepetition.map(ap => ap.name);
+ this.departmentList = this.sessionContext.getAllUserDepartments();
+ }
+
+ ngAfterViewInit() {
+ this.initInactiveFields();
+ }
+
+ ngAfterViewChecked() {
+ if (this.gridMeasureDetailForm) {
+ this.gridMeasureDetail._isValide = this.gridMeasureDetailForm.valid;
+ }
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes['id'] && changes['id'].currentValue) {
+ this.getDocumentsForId();
+ }
+ if (changes['readOnlyForm']) {
+ this.initInactiveFields();
+ }
+ if (changes['gridMeasureDetail'] && changes['gridMeasureDetail'].currentValue) {
+ this.gridMeasureDetail.appointmentNumberOf = this.gridMeasureDetail.appointmentNumberOf || 0;
+ this.appointmentRepetitionList = this.sessionContext.getBackendsettings().appointmentRepetition.map(ap => ap.name);
+ if (!this.gridMeasureDetail.appointmentRepetition) {
+ this.gridMeasureDetail.appointmentRepetition = this.appointmentRepetitionList[0];
+ }
+ this.setCurrTimeIfEmpty(this.gridMeasureDetail);
+ }
+ }
+
+ getAffectedResourcesDistinct() {
+ this.gridMeasureService.getAffectedResourcesDistinct().subscribe(aResource =>
+ this.affectedResourcesList = aResource);
+ }
+
+ public initInactiveFields() {
+ const el: HTMLElement = this.gridMeasureDetailTabContainer.nativeElement as HTMLElement;
+ const fields = el.querySelectorAll('*[id]:not(button)');
+ for (let index = 0; index < fields.length; index++) {
+ const field = fields[index];
+ if (this.readOnlyForm || this.isFieldInactive(field['id'])) {
+ field.setAttribute('disabled', 'disabled');
+ } else {
+ field.removeAttribute('disabled');
+ }
+ }
+ }
+
+ private isFieldInactive(fieldName: string): boolean {
+ if ((fieldName === 'fileUploadLabel' || fieldName === 'file') && !this.id) {
+ return true;
+ } else {
+ return this.inactiveFields.filter(field => field === fieldName).length > 0;
+ }
+ }
+ setCurrTimeIfEmpty(gridMeasure: GridMeasure) {
+
+ const dateFields = [
+ 'appointmentStartdate',
+ 'plannedStarttimeFirstSequence',
+ 'plannedEndtimeLastSinglemeasure',
+ 'plannedEndtimeGridmeasure',
+ 'plannedStarttimeFirstSinglemeasure'
+ ];
+ if (gridMeasure.id) {
+ return;
+ }
+ dateFields.forEach(field => {
+ if (gridMeasure[field] === undefined) {
+ gridMeasure[field] = this.getCurrentDateTime();
+ }
+ });
+
+ }
+
+ public selectedDate(value: any, datepicker?: any) {
+ this.gridMeasureDetail[datepicker] = value.start._d;
+ }
+ onGridMeasureDetailFormValidation(valid: boolean) {
+ this.gridMeasureDetail._isValide = valid;
+ }
+ getCurrentDateTime(): string {
+ return new Date().toISOString();
+ }
+ checkAppointmentNumberOfValue(event) {
+ if (event < Globals.APPOINTMENT_NUMBER_OF_MAX_VALUE + 1) {
+ this.isAppointmentNumberOfValid = true;
+ } else {
+ this.gridMeasureDetail.appointmentNumberOf = 0;
+ this.isAppointmentNumberOfValid = false;
+ }
+ }
+ onGridMeasureTitleChange(value) {
+ if (value) {
+ this.isValidForSave.emit(true);
+ } else {
+ this.isValidForSave.emit(false);
+ }
+ }
+
+ handleFileInput(file: FileList) {
+ if (!file || file.length === 0) {
+ return;
+ }
+
+ if (file.length > 1) {
+ this.toasterMessageService.showWarn('Es kann nur jeweils eine Datei hochgeladen werden! '
+ + 'Die erste Datei ihrer Auswahl wurde übernommen.');
+
+ }
+
+ this.fileToUpload = file.item(0);
+ this.fileName = this.fileToUpload.name;
+
+ if (this.gridMeasureDetail.statusId === Globals.STATUS.APPLIED && this.checkIfFileExists(this.fileToUpload.name)) {
+ this.toasterMessageService.showWarn('Im Status Beantragt können Dateien nicht geändert werden!');
+ return;
+ }
+
+ if (!this.fileSizeCheck() || !this.fileTypeCheck()) {
+ return;
+ }
+ this.fileSelected = true;
+ }
+
+
+ private checkIfFileExists(fileName: string) {
+ if (this.listOfDocumentsNames.includes(fileName)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private fileTypeCheck(): boolean {
+ const fileType = this.fileToUpload.type;
+ if (!Globals.TYPE_WHITELIST.includes(fileType)) {
+ this.toasterMessageService.showWarn('Dieser Dateityp ist nicht erlaubt!');
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ private fileSizeCheck(): boolean {
+ const fileUploadSize = this.fileToUpload.size;
+ const allowed = Globals.MEGABYT_UNIT * Globals.MAX_UPLOADFILE_SIZE;
+ if (fileUploadSize > allowed) {
+ // Globals.MAX_UPLOADFILE_SIZE + ' Megabytes!', MessageScopeEn.local);
+ this.toasterMessageService.showWarn('Die ausgewählte Datei überschreitet die maximale zulässige Größe von ' +
+ Globals.MAX_UPLOADFILE_SIZE + ' Megabytes!');
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ uploadDocument() {
+ if (!this.fileSelected) {
+ return;
+ }
+ this.showSpinnerFileUpload = true;
+ // convertFileToBas64
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ this.dataURI = reader.result;
+ this.proccessFile();
+ };
+ reader.readAsDataURL(this.fileToUpload);
+ }
+
+ private proccessFile() {
+ const byteString = this.dataURI.split(',')[1];
+ const documentToUpload = new Document();
+ documentToUpload.data = byteString;
+
+ // TODO send to backend to check file type against whitelist
+ // const mimeString = this.dataURI.split(',')[0].split(':')[1].split(';')[0];
+ documentToUpload.documentName = this.fileToUpload.name;
+
+ this.documentToUpload = documentToUpload;
+ this.processUpload();
+ }
+
+ private processUpload() {
+ this.documentService.uploadGridMeasureAttachments(this.id, this.documentToUpload).subscribe(resp => {
+ this.showSpinnerFileUpload = false;
+ this.getDocumentsForId();
+ this.fileSelected = false;
+ this.toasterMessageService.showSuccess('Datei erfolgreich hochgeladen!');
+ },
+ error => {
+ this.toasterMessageService.showError(ErrorType.upload, '');
+ this.showSpinnerFileUpload = false;
+ console.log(error);
+ });
+ }
+
+ private getDocumentsForId() {
+ this.documentService.getGridMeasureAttachments(this.id).subscribe(gm => {
+ this.listOfDocuments = gm;
+ for (let index = 0; index < this.listOfDocuments.length; index++) {
+ this.listOfDocumentsNames.push(this.listOfDocuments[index].documentName);
+ }
+ },
+ error => {
+ this.toasterMessageService.showError(ErrorType.retrieve, 'GridMeasure');
+ console.log(error);
+ });
+ }
+
+ downloadDocument(documentId) {
+ this.showSpinnerFileUpload = true;
+ this.documentService.downloadGridMeasureAttachment(documentId).subscribe(resp => {
+ this.saveFile(resp);
+ this.toasterMessageService.showSuccess('Datei erfolgreich heruntergeladen!');
+ this.showSpinnerFileUpload = false;
+ },
+ error => {
+ this.toasterMessageService.showError(ErrorType.retrieve, 'Datei');
+ this.showSpinnerFileUpload = false;
+ console.log(error);
+ });
+ }
+
+ deleteDocument(documentId: number, index: number) {
+ this.documentService.deleteGridMeasureAttachment(documentId).subscribe(resp => {
+ this.toasterMessageService.showSuccess('Datei erfolgreich gelöscht!');
+ this.listOfDocuments.splice(index, 1);
+ },
+ error => {
+ this.toasterMessageService.showError(ErrorType.delete, 'Datei');
+ this.showSpinnerFileUpload = false;
+ console.log(error);
+ });
+ }
+
+ private saveFile(document: Document) {
+ const byteString = atob(document.data);
+
+ // write the bytes of the string to an ArrayBuffer
+ const ab = new ArrayBuffer(byteString.length);
+
+ // create a view into the buffer
+ const ia = new Uint8Array(ab);
+
+ // set the bytes of the buffer to the correct values
+ for (let i = 0; i < byteString.length; i++) {
+ ia[i] = byteString.charCodeAt(i);
+ }
+
+ // write the ArrayBuffer to a blob, and you're done
+ const blob = new Blob([ab], { type: 'application/octet-stream' });
+ saveAs(blob, document.documentName);
+
+ }
+ allowDrop(ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ }
+
+ drop(ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ // cancel drop if upload is disabled
+ const el: HTMLElement = this.gridMeasureDetailTabContainer.nativeElement as HTMLElement;
+ const uploadField = el.querySelector('#fileUploadLabel');
+
+ if (uploadField.getAttribute('disabled')) {
+ this.dragEntered = false;
+ return;
+ }
+
+ if (!this.readOnlyForm && this.id) {
+ const files = ev.target.files || ev.dataTransfer.files;
+ this.handleFileInput(files);
+ }
+ this.dragEntered = false;
+ }
+
+ dragenter(ev) {
+ this.dragEntered = true;
+ }
+
+ dragleave(ev) {
+ this.dragEntered = false;
+ }
+}
diff --git a/src/app/pages/grid-measure-detail/grid-measure-detail.component.css b/src/app/pages/grid-measure-detail/grid-measure-detail.component.css
new file mode 100644
index 0000000..21f9e28
--- /dev/null
+++ b/src/app/pages/grid-measure-detail/grid-measure-detail.component.css
@@ -0,0 +1,174 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+#gridMeasureHeaderPanel {
+ position: fixed;
+ z-index: 800;
+ margin-top: 60px;
+ width: 100%;
+ border-bottom: solid 4px #0080c0;
+}
+
+@media (max-width: 768px) {
+ #gridMeasureHeaderPanel {
+ position: relative;
+ z-index: 0;
+ margin-top: 140px;
+ }
+}
+
+#gridMeasureMainPanel {
+ margin-top: 350px;
+ transition: margin-top 0.3s;
+}
+.mainPanelCollapsed{
+ margin-top: 125px !important;
+ transition: margin-top 0.8s;
+}
+@media (max-width: 768px) {
+ #gridMeasureMainPanel {
+ margin-top: 20px;
+ }
+}
+
+#gridMeasureEmailPanel {
+ margin-top: 20px;
+}
+
+#gridMeasureStatusChangePanel {
+ margin-top: 20px;
+}
+
+.alert {
+ margin: 5px;
+}
+
+div.panel-default {
+ margin: 1px 1px;
+}
+
+.ng-valid[required],
+.ng-valid.required {
+ border-left: 5px solid #42A948;
+ /* green */
+}
+
+.ng-invalid:not(form) {
+ border-left: 5px solid #a94442;
+ /* red */
+}
+
+.error {
+ border-left: 5px solid #a94442;
+}
+
+.row {
+ margin-top: 4px;
+}
+
+.dateRangePickerIcon:hover {
+ cursor: pointer;
+}
+
+@media (min-width: 992px) {
+ .col-md-6,
+ .col-md-12 {
+ display: inline-flex;
+ }
+}
+
+@media (min-width: 992px) {
+ .form-field-label {
+ width: 100%;
+ margin-right: 15px;
+ }
+}
+
+@media (min-width: 992px) {
+ .input-group .form-control {
+ width: 100%;
+ }
+}
+
+@media all and (-ms-high-contrast: none),
+(-ms-high-contrast: active) {
+ /* IE10+ CSS styles go here */
+ .input-group .form-control {
+ width: 98.3%;
+ }
+}
+
+@media (min-width: 992px) {
+ textarea.form-control {
+ width: 100%;
+ }
+}
+
+@supports (-moz-appearance:none) {
+ .input-group .form-control {
+ width: 98.3%;
+ }
+}
+
+.input-group input {
+ z-index: 0;
+}
+
+#newSingleGridMeasureBtn {
+ background-color: #f5f8fc;
+ line-height: 275%;
+ border: 1px solid #ddd;
+ border-bottom: none;
+ border-radius: 4px 4px 0 0;
+}
+
+#newSingleGridMeasureBtn:hover {
+ background-color: white;
+}
+
+#newSingleGridMeasureBtn:focus {
+ outline: 0 !important;
+}
+
+#newSingleGridMeasureBtn:disabled {
+ cursor: default;
+ text-decoration: none;
+ color: lightgrey;
+ background-color: white;
+}
+
+.grid-measure-tabs {
+ margin: 15px 0 0 -2px;
+}
+
+.grid-measure-tabs li a {
+ color: black;
+ background-color: #f5f8fc;
+}
+
+.grid-measure-tabs li.active a {
+ background-color: white;
+}
+
+.grid-measure-tabs li a:hover {
+ background-color: white;
+ border-top-color: #ddd;
+ border-right-color: #ddd;
+}
+
+.grid-measure-tabs .invalid-tab {
+ border-left: 5px solid #a94442;
+}
+
+.grid-measure-tabs .valid-tab {
+ border-left: 5px solid #42A948;
+}
\ No newline at end of file
diff --git a/src/app/pages/grid-measure-detail/grid-measure-detail.component.html b/src/app/pages/grid-measure-detail/grid-measure-detail.component.html
new file mode 100644
index 0000000..526bafd
--- /dev/null
+++ b/src/app/pages/grid-measure-detail/grid-measure-detail.component.html
@@ -0,0 +1,121 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+-->
+<form #parentForm="ngForm" *ngIf="gridMeasureDetailReceived">
+
+ <div class="row">
+ <app-buttons-container #buttonscontainer [isValidForm]="areAllTabsValid()" [isValidForSave]="validForSave" [isReadOnlyForm]="readOnlyForm"
+ [gridMeasureStatusId]="gridMeasureDetail?.statusId" [gridMeasureId]="gridMeasureDetail?.id" (clickQuitButton)="goToOverview()"
+ (clickCancelButton)="goToCancelPage()" (clickRejectButton)="updateGridMeasureStatus(Globals.STATUS.REJECTED)" (clickSaveButton)="createGridMeasure(gridMeasureDetail)"
+ (clickApplyButton)="updateGridMeasureStatus(Globals.STATUS.APPLIED)" (clickForApprovalButton)="updateGridMeasureStatus(Globals.STATUS.FORAPPROVAL)"
+ (clickApprovedButton)="updateGridMeasureStatus(Globals.STATUS.APPROVED)" (clickRequestButton)="updateGridMeasureStatus(Globals.STATUS.REQUESTED)"
+ (clickReleaseButton)="updateGridMeasureStatus(Globals.STATUS.RELEASED)" (clickActivateButton)="updateGridMeasureStatus(Globals.STATUS.ACTIVE)"
+ (clickInWorkButton)="updateGridMeasureStatus(Globals.STATUS.IN_WORK)" (clickWorkFinishButton)="updateGridMeasureStatus(Globals.STATUS.WORK_FINISHED)"
+ (clickFinishButton)="updateGridMeasureStatus(Globals.STATUS.FINISHED)" (clickCloseButton)="updateGridMeasureStatus(Globals.STATUS.CLOSED)"
+ (clickDuplicateButton)="duplicateGM()"></app-buttons-container>
+
+ </div>
+
+ <div class="panel panel-default" id="gridMeasureHeaderPanel">
+ <div class="panel-heading">
+ <h4 class="panel-title">
+ <a data-toggle="collapse" href="#collapse1" (click)="isExpanded = !isExpanded">Netzmaßnahme</a>
+ </h4>
+ </div>
+ <div id="collapse1" class="panel-collapse collapse in">
+ <div class="panel-body">
+ <app-grid-measure-detail-header (isValidForSave)=" this.validForSave = $event;" [isReadOnlyForm]="readOnlyForm" [id]="id"
+ [showSpinnerGrid]="showSpinner" [gridMeasureDetail]="gridMeasureDetail">
+ </app-grid-measure-detail-header>
+ </div>
+ </div>
+ </div>
+
+ <div class="grid-measure-body ">
+ <div class="maincontent">
+
+ <div class="panel panel-default" [ngClass]="isExpanded ? '':'mainPanelCollapsed'" id="gridMeasureMainPanel">
+ <div class="panel-heading">
+ <h4 class="panel-title">
+ <a data-toggle="collapse" href="#collapse4">Details der Netzmaßnahme</a>
+ </h4>
+ </div>
+
+ <div id="collapse4" class="panel-collapse collapse in">
+ <div class="grid-measure-tabs">
+ <ul *ngIf="showTabs" class="nav nav-tabs">
+ <li class="active">
+ <a href="#gridmeasurepanel" (click)="onSelectGridMeasureTab()" data-toggle="tab" [ngClass]="isGridMeasureDetailValid() ? 'valid-tab' : 'invalid-tab'">Maßnahme</a>
+ </li>
+ <li *ngFor="let singleGridmeasure of gridMeasureDetail?.listSingleGridmeasures; let idx = index;">
+ <a *ngIf="!singleGridmeasure.delete" [id]="singleGridmeasure.sortorder" href="#singlegridmeasure" (click)="onSelectSingleGridMeasureTab(singleGridmeasure)"
+ data-toggle="tab" [ngClass]="isSingleGridmeasureValid(singleGridmeasure) ? 'valid-tab' : 'invalid-tab'">Einzelmaßnahme {{idx + 1}} </a>
+ </li>
+ <li>
+ <!-- more singleGridMeasures -->
+ <button [disabled]="disableNewTabBtn || isSingleGridmeasureLimitReached()" class="glyphicon glyphicon-plus" id="newSingleGridMeasureBtn"
+ (click)="createNewSingleGridMeasureTab()" [title]="isSingleGridmeasureLimitReached() ? 'Es können maximal ' + Globals.MAX_NUMBER_OF_TABS + ' Einzelmaßnahmen angelegt werden.' : ''"></button>
+ </li>
+ </ul>
+ </div>
+ <div class="tab-content clearfix">
+
+ <div class="tab-pane active" id="gridmeasurepanel">
+ <div class="panel-body">
+ <app-grid-measure-detail-tab (isValidForSave)=" this.validForSave = $event;" [readOnlyForm]="readOnlyForm" [id]="id" [showSpinnerGrid]="showSpinner"
+ [gridMeasureDetail]="gridMeasureDetail">
+ </app-grid-measure-detail-tab>
+ </div>
+ </div>
+
+ <div class="tab-pane" id="singlegridmeasure">
+ <div class="panel-body">
+ <app-single-grid-measure-detail-tab (singleGridMeasureChanged)="onSingleGridMeasureChanged($event)" [singleGridMeasure]="currentSingleGridMeasure"
+ [isReadOnlyForm]="readOnlyForm" [gridMeasureDetail]="gridMeasureDetail" [dateTimePattern]="dateTimePattern"
+ [dateFormatLocale]="dateFormatLocale">
+ </app-single-grid-measure-detail-tab>
+ </div>
+ </div>
+
+ </div>
+ </div>
+ </div>
+
+
+ <div class="panel panel-default" id="gridMeasureEmailPanel">
+ <div class="panel-heading">
+ <h4 class="panel-title">
+ <a *ngIf="isEmailDistributionStatusCollapsed" data-toggle="collapse" href="#collapse9">E-Mail Verteiler für Status Genehmigt, Storniert und Zurückgewiesen</a>
+ <div *ngIf="!isEmailDistributionStatusCollapsed">E-Mail-Verteiler</div>
+ </h4>
+ </div>
+ <div id="collapse9" class="panel-collapse panel-body collapse in" [ngClass]="{'in': !isEmailDistributionStatusCollapsed }">
+ <app-email-distribution-entry [isReadOnlyForm]="emailAddFormNotAllowed" [(gridMeasureDetail)]="gridMeasureDetail">
+ loading ...
+ </app-email-distribution-entry>
+ <app-email-distribution-list [gridId]="'email-distribution-list'" [withEditButtons]="true" [gridMeasureDetail]="gridMeasureDetail"
+ (gridMeasureChanged)="onGridMeasureChanged($event)">
+ loading ...
+ </app-email-distribution-list>
+ </div>
+ </div>
+
+
+ <div id="gridMeasureStatusChangePanel">
+ <app-status-changes [gridId]="gridMeasureDetail.id">
+ loading ...
+ </app-status-changes>
+ </div>
+
+
+ </div>
+ </div>
+</form>
\ No newline at end of file
diff --git a/src/app/pages/grid-measure-detail/grid-measure-detail.component.spec.ts b/src/app/pages/grid-measure-detail/grid-measure-detail.component.spec.ts
new file mode 100644
index 0000000..1ba71a7
--- /dev/null
+++ b/src/app/pages/grid-measure-detail/grid-measure-detail.component.spec.ts
@@ -0,0 +1,707 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+/* tslint:disable:no-unused-variable */
+import { EventEmitter } from '@angular/core';
+import { ComponentFixture, TestBed, async, fakeAsync, tick } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+import { DaterangepickerConfig } from 'ng2-daterangepicker';
+import { StringToDatePipe } from '../../common-components/pipes/string-to-date.pipe';
+import { SessionContext } from '../../common/session-context';
+import { GridMeasure } from '../../model/grid-measure';
+import { LockHelperService } from './../../services/lock-helper.service';
+import { RoleAccess } from '../../model/role-access';
+import { GridMeasureService } from '../../services/grid-measure.service';
+import { GRIDMEASURE } from '../../test-data/grid-measures';
+import { USERS } from '../../test-data/users';
+import { AbstractMockObservableService } from '../../testing/abstract-mock-observable.service';
+import { MockComponent } from '../../testing/mock.component';
+import { ActivatedRouteStub, RouterStub } from '../../testing/router-stubs';
+import { GridMeasureDetailComponent } from './grid-measure-detail.component';
+import { Globals } from '../../common/globals';
+import { RoleAccessHelperService } from '../../services/jobs/role-access-helper.service';
+import { ModeValidator } from '../../custom_modules/helpers/mode-validator';
+import { BaseDataLoaderService } from '../../services/jobs/base-data-loader.service';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+import { MessageService } from 'primeng/api';
+
+
+class FakeRouter {
+ navigate(commands: any[]) {
+ return commands[0];
+ }
+}
+
+describe('GridMeasureDetailComponent', () => {
+ let component: GridMeasureDetailComponent;
+ let fixture: ComponentFixture<GridMeasureDetailComponent>;
+ const gridmeasures: GridMeasure[] = JSON.parse(JSON.stringify(GRIDMEASURE));
+ let routerStub: RouterStub;
+ let activatedStub: ActivatedRouteStub;
+ // let router: Router;
+
+
+ routerStub = {
+ navigate: jasmine.createSpy('navigate').and.callThrough()
+ };
+
+
+ class MockService extends AbstractMockObservableService {
+ storeGridMeasure() {
+ return this;
+ }
+ getGridMeasure(id: number) {
+ return this;
+ }
+ }
+
+ class MockDocumentService extends AbstractMockObservableService {
+ public uploadGridMeasureAttachments(gridmeasuereId: number, file: File) {
+ return this;
+ }
+
+ public getGridMeasureAttachments(gridmeasuereId: number) {
+ return this;
+ }
+ public deleteGridMeasureAttachment(documentId: number, index: number) {
+ return this;
+ }
+
+ public downloadGridMeasureAttachment(documentId: number) {
+ return this;
+ }
+ }
+
+ class MockLockService extends AbstractMockObservableService {
+ checkLockedByUser(measureId: number, lockType: string, lockedByOtherEmitter$: EventEmitter<boolean>) {
+ lockedByOtherEmitter$.emit(this.content);
+ }
+
+ createLock(gridmeasureId: number, lockType: string) {
+ return this;
+ }
+
+ deleteLock(measureId: number, lockType: string) {
+ return this;
+ }
+ }
+
+
+ let toasterMessageService: ToasterMessageService;
+ let messageService: MessageService;
+ let mockService: MockService;
+ let mockDocService: MockDocumentService;
+ let mockLockHelperService: MockLockService;
+ let sessionContext: SessionContext;
+ let roleAccessHelper: RoleAccessHelperService;
+ beforeEach(async(() => {
+ activatedStub = new ActivatedRouteStub();
+ sessionContext = new SessionContext();
+ messageService = new MessageService;
+ mockService = new MockService();
+ mockDocService = new MockDocumentService();
+ mockLockHelperService = new MockLockService();
+ toasterMessageService = new ToasterMessageService(sessionContext, messageService);
+ roleAccessHelper = new RoleAccessHelperService();
+
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule
+ ],
+ declarations: [
+ GridMeasureDetailComponent,
+ StringToDatePipe,
+ MockComponent({ selector: 'input', inputs: ['options'] }),
+ MockComponent({ selector: 'app-grid-measures', inputs: ['gridId', 'withEditButtons'] }),
+ MockComponent({ selector: 'app-loading-spinner', inputs: [] }),
+ MockComponent({
+ selector: 'app-buttons-container',
+ inputs: ['activeButtons', 'isValidForm', 'isValidForSave', 'isReadOnlyForm', 'gridMeasureStatusId', 'gridMeasureId']
+ }),
+ MockComponent({
+ selector: 'app-single-grid-measure-detail-tab',
+ inputs: ['isReadOnlyForm', 'singleGridMeasure', 'gridMeasureDetail', 'dateTimePattern',
+ 'dateFormatLocale', 'tabIndex', 'showSpinnerSingleGrid']
+ }),
+ MockComponent({
+ selector: 'app-grid-measure-detail-tab',
+ inputs: ['showSpinnerGrid', 'gridMeasureDetail', 'readOnlyForm']
+ }),
+ MockComponent({
+ selector: 'app-email-distribution-entry',
+ inputs: ['isReadOnlyForm', 'gridMeasureDetail']
+ }),
+ MockComponent({
+ selector: 'app-email-distribution-list',
+ inputs: ['gridId', 'withEditButtons', 'gridMeasureDetail']
+ }),
+ MockComponent({
+ selector: 'app-status-changes',
+ inputs: ['gridId', 'withEditButtons', 'gridMeasureDetail']
+ }),
+ MockComponent({
+ selector: 'app-grid-measure-detail-header',
+ inputs: ['showSpinnerGrid', 'gridMeasureDetail', 'isReadOnlyForm']
+ })
+ ],
+ providers: [
+ SessionContext,
+ ModeValidator,
+ { provide: ActivatedRoute, useValue: activatedStub },
+ { provide: Router, useValue: routerStub },
+ { provide: GridMeasureService, useValue: mockService },
+ { provide: LockHelperService, useValue: mockLockHelperService },
+ // { provide: DocumentService, useValue: mockDocService },
+ { provide: BaseDataLoaderService, useValue: {} },
+ { provide: DaterangepickerConfig, useClass: DaterangepickerConfig },
+ { provide: RoleAccessHelperService, useValue: roleAccessHelper },
+ { provide: ToasterMessageService, useValue: toasterMessageService }
+
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(async(() => {
+ sessionContext.setCurrUser(USERS[0]);
+ sessionContext.setAllUsers(USERS);
+ // we need to init the component and the path... because of OnInit
+ mockService.content = JSON.parse(JSON.stringify(GRIDMEASURE[0]));
+ mockDocService.content = [{ id: 1, documentName: 'docdoc.doc' }];
+ const roleAcess: RoleAccess = {
+ editRoles: [{
+ name: 'planned-policies-measureplanner',
+ gridMeasureStatusIds: [
+ 0,
+ 1
+ ]
+ }, {
+ name: 'planned-policies-superuser',
+ gridMeasureStatusIds: [
+ 0,
+ 1
+ ]
+ }, {
+ name: 'planned-policies-measureapplicant',
+ gridMeasureStatusIds: [
+ 0,
+ 1
+ ]
+ }],
+ controls: [{
+ gridMeasureStatusId: 0,
+ activeButtons: [
+ 'save',
+ 'apply',
+ 'cancel'
+ ],
+ inactiveFields: [
+ 'titeldermassnahme'
+ ]
+ },
+ {
+ gridMeasureStatusId: 1,
+ activeButtons: [
+ 'save',
+ 'cancel',
+ 'forapproval'
+ ],
+ inactiveFields: [
+ 'titeldermassnahme'
+ ]
+ }],
+ stornoSection:
+ {
+ 'stornoRoles': [
+ 'planned-policies-measureapplicant',
+ 'planned-policies-measureplanner',
+ 'planned-policies-measureapprover',
+ 'planned-policies-requester',
+ 'planned-policies-clearance'
+ ]
+ },
+ duplicateSection:
+ {
+ 'duplicateRoles': [
+ 'planned-policies-measureapplicant'
+ ]
+ }
+ };
+ roleAccessHelper.init(roleAcess);
+
+ activatedStub.testParams = { id: 555, mode: Globals.MODE.EDIT };
+ fixture = TestBed.createComponent(GridMeasureDetailComponent);
+ fixture.whenStable().then(() => {
+
+ component = fixture.componentInstance;
+ component.id = activatedStub.testParams['id'];
+ fixture.detectChanges();
+ });
+ }));
+
+ it('should create', (() => {
+ expect(component).toBeTruthy();
+ }));
+
+
+
+
+
+ it('should go to overview if the url is not edit or view', async(() => {
+ spyOn(component, 'goToOverview').and.callThrough();
+ fixture.detectChanges();
+ activatedStub.testParams = { id: 555, mode: 'bla' };
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect(component.goToOverview).toHaveBeenCalled();
+ });
+
+ }));
+
+ it('should stop spinner and create a new grid measure if no id', async(() => {
+ component.id = undefined;
+ spyOn((component as any), 'checkModeAndInitiateGm').and.callThrough();
+ (component as any).checkModeAndInitiateGm('bla', false, undefined);
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect((component as any).checkModeAndInitiateGm).toHaveBeenCalledWith('bla', false, undefined);
+ expect(component.showSpinner).toBeFalsy();
+ });
+
+ }));
+
+
+
+ it('should set status to new for a newly created gridmeasure', async(() => {
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ component.gridMeasureDetail.statusId = NaN;
+ component.storageInProgress = true;
+ (component as any).createGridMeasure();
+ fixture.detectChanges();
+
+ component.storageInProgress = false;
+
+ (component as any).createGridMeasure();
+ fixture.detectChanges();
+
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect((component as any).gridMeasureDetail.statusId).toBe(Globals.STATUS.NEW);
+ });
+ }));
+
+ it('should call createNewSingleGridMeasureTab on button click', () => {
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ expect(component.gridMeasureDetail.listSingleGridmeasures.length).toBe(2);
+ component.disableNewTabBtn = false;
+ spyOn(component, 'createNewSingleGridMeasureTab').and.callThrough();
+
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ const button = fixture.debugElement.nativeElement.querySelector('#newSingleGridMeasureBtn');
+ button.click();
+ fixture.detectChanges();
+
+ expect(component.createNewSingleGridMeasureTab).toHaveBeenCalled();
+ expect(component.gridMeasureDetail.listSingleGridmeasures.length).toBe(3);
+ });
+
+ });
+
+ it('should createNewSingleGridMeasureTab if limit is not reached', () => {
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ expect(component.gridMeasureDetail.listSingleGridmeasures.length).toBe(2);
+ component.createNewSingleGridMeasureTab();
+
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.gridMeasureDetail.listSingleGridmeasures.length).toBe(3);
+ });
+
+ });
+
+ it('should not createNewSingleGridMeasureTab if limit is reached', () => {
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ const actualNumberOfSingleGridMeasureTabs = component.gridMeasureDetail.listSingleGridmeasures.length - 1;
+ for (let i = actualNumberOfSingleGridMeasureTabs; i < Globals.MAX_NUMBER_OF_TABS; i++) {
+ component.createNewSingleGridMeasureTab();
+ }
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.gridMeasureDetail.listSingleGridmeasures.length).toBe(Globals.MAX_NUMBER_OF_TABS);
+ });
+ });
+
+ it('should handle a service error correctly after create gridmeasure called', fakeAsync(() => {
+ spyOn(console, 'log').and.callThrough();
+ spyOn(mockService, 'storeGridMeasure').and.callThrough();
+ mockService.content = JSON.parse(JSON.stringify(gridmeasures[2]));
+ mockService.error = 'Error';
+
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ component.storageInProgress = false;
+
+ (component as any).createGridMeasure();
+ fixture.detectChanges();
+
+ expect(console.log).toHaveBeenCalled();
+ }));
+
+ it('should disable save button on init with no values in required fields', () => {
+ component.gridMeasureDetail = {};
+ (component as any).onReceiveGridMeasureDetail();
+
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect((component as any).validForSave).toBeFalsy('valid for save should be true');
+ });
+ });
+
+ it('should disable apply button on init with no values in required fields', async(() => {
+ component.gridMeasureDetail = {};
+ (component as any).onReceiveGridMeasureDetail();
+ fixture.detectChanges();
+
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect((component as any).validForm).toBeFalsy('Form not valid');
+ });
+ }));
+
+
+
+
+
+ it('should enable apply button after filling all required fields', async(() => {
+ component.gridMeasureDetail = gridmeasures[0];
+ (component as any).onReceiveGridMeasureDetail();
+
+ fixture.detectChanges();
+ fixture.whenRenderingDone().then(() => {
+ fixture.detectChanges();
+ expect((component as any).validForm).toBeFalsy('apply button is enabled');
+ });
+ }));
+
+
+ it('should navigate to Overview after click on abortbutton', () => {
+ spyOn(component, 'goToOverview').and.callThrough();
+ fixture.detectChanges();
+
+ component.goToOverview();
+ fixture.detectChanges();
+ expect(component.goToOverview).toHaveBeenCalled();
+ expect(routerStub.navigate).toHaveBeenCalledWith(['/overview']);
+ });
+
+ it('should not call delete lock after click on abortbutton for new unsaved gridmeasure', () => {
+ spyOn(mockLockHelperService, 'deleteLock');
+ spyOn(component, 'goToOverview').and.callThrough();
+ component.readOnlyForm = false;
+ component.id = undefined;
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ const button = fixture.debugElement.nativeElement.querySelector('button#abortButton');
+ button.click();
+ fixture.detectChanges();
+ expect(mockLockHelperService.deleteLock).toHaveBeenCalled();
+ expect(routerStub.navigate).toHaveBeenCalledWith(['/overview']);
+ });
+ });
+
+ it('should create a GridMeasure after click on applybutton', async(() => {
+ spyOn(mockService, 'storeGridMeasure').and.callThrough();
+ mockService.content = JSON.parse(JSON.stringify(gridmeasures[2]));
+
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ component.storageInProgress = true;
+
+ fixture.detectChanges();
+
+ component.updateGridMeasureStatus(Globals.STATUS.APPLIED);
+ expect(mockService.storeGridMeasure).not.toHaveBeenCalled();
+
+ component.storageInProgress = false;
+
+
+ fixture.detectChanges();
+
+ component.updateGridMeasureStatus(Globals.STATUS.APPLIED);
+
+ expect(mockService.storeGridMeasure).toHaveBeenCalled();
+ }));
+
+ it('should handle a service error correctly after applybutton', fakeAsync(() => {
+ spyOn(console, 'log').and.callThrough();
+ mockService.error = 'Error';
+
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ fixture.detectChanges();
+ tick();
+ component.updateGridMeasureStatus(Globals.STATUS.APPLIED);
+
+ tick();
+
+ expect(console.log).toHaveBeenCalled();
+ }));
+
+ it('should be able to set for approval a GridMeasure', async(() => {
+ spyOn(mockService, 'storeGridMeasure').and.callThrough();
+ mockService.content = gridmeasures[2];
+
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ component.gridMeasureDetail.statusId = Globals.STATUS.APPLIED;
+ fixture.detectChanges();
+
+ component.updateGridMeasureStatus(Globals.STATUS.FORAPPROVAL);
+ expect(mockService.storeGridMeasure).toHaveBeenCalled();
+ expect(component.gridMeasureDetail.statusId).toBe(Globals.STATUS.FORAPPROVAL);
+ }));
+
+
+ it('should handle a service error correctly after forapproval button', fakeAsync(() => {
+ spyOn(console, 'log').and.callThrough();
+ mockService.error = 'Error';
+
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ fixture.detectChanges();
+ tick();
+ component.updateGridMeasureStatus(Globals.STATUS.FORAPPROVAL);
+
+ tick();
+
+ expect(console.log).toHaveBeenCalled();
+ }));
+
+ it('should be able to approve a GridMeasure', async(() => {
+ spyOn(mockService, 'storeGridMeasure').and.callThrough();
+ mockService.content = gridmeasures[2];
+
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ component.gridMeasureDetail.statusId = Globals.STATUS.FORAPPROVAL;
+ fixture.detectChanges();
+
+ component.updateGridMeasureStatus(Globals.STATUS.APPROVED);
+ expect(mockService.storeGridMeasure).toHaveBeenCalled();
+ expect(component.gridMeasureDetail.statusId).toBe(Globals.STATUS.APPROVED);
+ }));
+
+
+ it('should handle a service error correctly after approve button', fakeAsync(() => {
+ spyOn(console, 'log').and.callThrough();
+ mockService.error = 'Error';
+
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ fixture.detectChanges();
+ tick();
+ component.updateGridMeasureStatus(Globals.STATUS.APPROVED);
+
+ tick();
+
+ expect(console.log).toHaveBeenCalled();
+ }));
+
+ it('should be able to send back a GridMeasure', async(() => {
+ spyOn(mockService, 'storeGridMeasure').and.callThrough();
+ mockService.content = gridmeasures[2];
+
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ component.gridMeasureDetail.statusId = Globals.STATUS.FORAPPROVAL;
+ fixture.detectChanges();
+
+ component.updateGridMeasureStatus(Globals.STATUS.APPLIED);
+ expect(mockService.storeGridMeasure).toHaveBeenCalled();
+ expect(component.gridMeasureDetail.statusId).toBe(Globals.STATUS.APPLIED);
+ }));
+
+
+ it('should handle a service error correctly after reject button', fakeAsync(() => {
+ spyOn(console, 'log').and.callThrough();
+ mockService.error = 'Error';
+
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ fixture.detectChanges();
+ tick();
+ component.updateGridMeasureStatus(Globals.STATUS.APPLIED);
+
+ tick();
+
+ expect(console.log).toHaveBeenCalled();
+ }));
+
+ it('should be able to cancel a GridMeasure', async(() => {
+ spyOn(mockService, 'storeGridMeasure').and.callThrough();
+ mockService.content = gridmeasures[2];
+
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ component.gridMeasureDetail.statusId = Globals.STATUS.APPROVED;
+ fixture.detectChanges();
+
+ component.updateGridMeasureStatus(Globals.STATUS.CANCELED);
+ expect(mockService.storeGridMeasure).toHaveBeenCalled();
+ expect(component.gridMeasureDetail.statusId).toBe(Globals.STATUS.CANCELED);
+ }));
+
+
+ it('should handle a service error correctly after cancel button', fakeAsync(() => {
+ spyOn(console, 'log').and.callThrough();
+ mockService.error = 'Error';
+
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ fixture.detectChanges();
+ tick();
+ component.updateGridMeasureStatus(Globals.STATUS.CANCELED);
+
+ tick();
+
+ expect(console.log).toHaveBeenCalled();
+ }));
+
+
+ it('should create a GridMeasure after click on savebutton', async(() => {
+ mockService.content = gridmeasures[2];
+ spyOn(mockService, 'storeGridMeasure').and.callThrough();
+
+ fixture.detectChanges();
+ component.createGridMeasure();
+
+ expect(mockService.storeGridMeasure).toHaveBeenCalled();
+ }));
+
+ it('should checkLockedByUser correctly when form is writable', fakeAsync(() => {
+ spyOn(mockLockHelperService, 'createLock').and.callThrough();
+ component.viewModeReadOnly = false;
+ mockLockHelperService.content = false;
+ component.checkLockedByUser();
+ tick();
+
+ fixture.detectChanges();
+ expect(mockLockHelperService.createLock).toHaveBeenCalled();
+ expect(component.showSpinner).toBeFalsy();
+ }));
+
+
+ it('should deleteLock correctly when not readonly form', fakeAsync(() => {
+ spyOn(mockLockHelperService, 'deleteLock').and.callThrough();
+ component.readOnlyForm = false;
+ mockLockHelperService.content = false;
+ component.deleteLock(false, null);
+ tick();
+
+ fixture.detectChanges();
+ expect(mockLockHelperService.deleteLock).toHaveBeenCalled();
+ }));
+
+
+ it('should deleteLock correctly when form is readonly', fakeAsync(() => {
+ spyOn(mockLockHelperService, 'deleteLock').and.callThrough();
+ component.readOnlyForm = true;
+ mockLockHelperService.content = false;
+ component.deleteLock(false, null);
+ tick();
+
+ fixture.detectChanges();
+ expect(mockLockHelperService.deleteLock).not.toHaveBeenCalled();
+ }));
+
+
+ it('should handle onUnlock correctly', fakeAsync(() => {
+ const testableComp: any = component;
+ spyOn(component, 'deleteLock').and.callThrough();
+ spyOn(component, 'recheckLockedByUser').and.callThrough();
+ mockService.error = 'Error';
+
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ fixture.detectChanges();
+ tick();
+
+ testableComp.unlockInProgress = false;
+ component.onUnlock(false);
+ tick();
+
+ expect(testableComp.unlockInProgress).toBeTruthy();
+ expect(component.deleteLock).not.toHaveBeenCalled();
+
+ tick();
+ component.onUnlock(true);
+ expect(component.recheckLockedByUser).not.toHaveBeenCalled();
+
+ component.recheckLockedByUser();
+ expect(testableComp.unlockInPorgress).toBeFalsy();
+ component.onUnlock(true);
+ tick();
+ expect(component.deleteLock).toHaveBeenCalled();
+
+
+ }));
+
+ it('should navigate to cancle page if cancel button clicked', async(() => {
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ spyOn(component, 'goToCancelPage').and.callThrough();
+ fixture.detectChanges();
+ component.goToCancelPage();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(routerStub.navigate).toHaveBeenCalled();
+ });
+
+ }));
+
+ it('should try to duplicate a gm if duplicate button clicked', async(() => {
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ component.storageInProgress = true;
+ fixture.detectChanges();
+
+ spyOn(component, 'duplicateGM').and.callThrough();
+ fixture.detectChanges();
+ component.duplicateGM();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(component.duplicateGM).toHaveBeenCalled();
+ });
+
+ }));
+
+ it('should navigate to the right grid measure after duplication', async(() => {
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ fixture.detectChanges();
+
+ spyOn((component as any), 'navigateAfterCreate').and.callThrough();
+ fixture.detectChanges();
+ (component as any).navigateAfterCreate(true, component.gridMeasureDetail);
+ fixture.detectChanges();
+ fixture.whenRenderingDone().then(() => {
+ expect((component as any).navigateAfterCreate).toHaveBeenCalled();
+ expect(routerStub.navigate).toHaveBeenCalledWith(['/overview']);
+ setTimeout(() => {
+ expect(routerStub.navigate).toHaveBeenCalledWith(['/gridMeasureDetail/', component.gridMeasureDetail.id, Globals.MODE.EDIT]);
+ }, 500);
+ });
+
+ }));
+
+ it('should navigate to overview after created new grid measure (not duplicate)', async(() => {
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(gridmeasures[0]));
+ fixture.detectChanges();
+
+ spyOn((component as any), 'navigateAfterCreate').and.callThrough();
+ fixture.detectChanges();
+ (component as any).navigateAfterCreate(false, component.gridMeasureDetail);
+ fixture.detectChanges();
+ fixture.whenRenderingDone().then(() => {
+ expect((component as any).navigateAfterCreate).toHaveBeenCalled();
+ expect(routerStub.navigate).toHaveBeenCalledWith(['/overview']);
+ });
+
+ }));
+
+});
diff --git a/src/app/pages/grid-measure-detail/grid-measure-detail.component.ts b/src/app/pages/grid-measure-detail/grid-measure-detail.component.ts
new file mode 100644
index 0000000..02dd6cf
--- /dev/null
+++ b/src/app/pages/grid-measure-detail/grid-measure-detail.component.ts
@@ -0,0 +1,524 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { SessionContext } from './../../common/session-context';
+import { GridMeasureService } from './../../services/grid-measure.service';
+import { LockHelperService } from './../../services/lock-helper.service';
+import { GridMeasure } from './../../model/grid-measure';
+import {
+ Component, OnInit, ViewChild, OnDestroy, EventEmitter, ChangeDetectorRef, AfterViewChecked, HostBinding
+} from '@angular/core';
+import { Router, ActivatedRoute } from '@angular/router';
+import { DaterangepickerConfig } from 'ng2-daterangepicker';
+import { ErrorType } from '../../common/enums';
+import { UserDepartment } from './../../model/user-department';
+import { Globals } from './../../common/globals';
+import { RoleAccess } from '../../model/role-access';
+import { RoleAccessHelperService } from '../../services/jobs/role-access-helper.service';
+import { ToasterButtonEventEn } from './../../common/enums';
+import { GridMeasureValidatorFactory } from '../../custom_modules/helpers/grid-measure-validator';
+import { ModeValidator } from '../../custom_modules/helpers/mode-validator';
+import { SingleGridMeasure } from '../../model/single-grid-measure';
+import { SingleGridMeasureDetailTabComponent } from '../single-grid-measure-detail-tab/single-grid-measure-detail-tab.component';
+import { Util } from '../../common/util';
+import { CloneGridMeasureHelper } from '../../custom_modules/helpers/clone-grid-measure-helper';
+import { BaseDataLoaderService } from '../../services/jobs/base-data-loader.service';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+
+@Component({
+ selector: 'app-grid-measure-detail',
+ templateUrl: './grid-measure-detail.component.html',
+ styleUrls: ['./grid-measure-detail.component.css']
+})
+
+export class GridMeasureDetailComponent implements OnInit, OnDestroy, AfterViewChecked {
+
+
+ private unlockInProgress = false;
+ dragEntered = false;
+ console = console;
+
+ Globals = Globals;
+ readOnlyForm = false; // true to set the whole form readonly(disable form)
+ viewModeReadOnly = false; // setting that was provided when starting the form
+ emailAddFormNotAllowed = true;
+ showSpinner = true;
+ departmentList: UserDepartment[];
+ gridMeasureDetail: GridMeasure;
+ currentSingleGridMeasure: SingleGridMeasure = new SingleGridMeasure();
+ endDate: Date;
+ startDate: Date;
+ switchingObjectList: string[];
+ areaOfSwitchingList: string[];
+ responsibleOnSiteNameList: string[];
+ levelList: any[];
+ validForSave: boolean;
+ validForUpload: boolean;
+ fileToUpload: File = null;
+ showTabs = true;
+ isExpanded = true;
+ fileReaderReady = false;
+ id: any;
+ fileSelected: boolean;
+ isAppointmentNumberOfValid: boolean;
+ storageInProgress = false;
+ dateFormatLocale = 'dd.MM.yyyy HH:mm';
+ roleAccessCurrUser: RoleAccess;
+ datePattern = '^(([0-2]?[0-9]|3[0-1])\.([0]?[1-9]|1[0-2])\.[1-2][0-9]{3})$';
+ dateTimePattern = '^(([0-2]?[0-9]|3[0-1])\.([0]?[1-9]|1[0-2])\.[1-2][0-9]{3}) (20|21|22|23|[0-1]?[0-9]{1}):([0-5]?[0-9]{1})$';
+ mode: string;
+ gridMeasureFormValid: boolean;
+ gridMeasureDetailReceived: boolean;
+ singleGridValidity: any;
+ disableNewTabBtn: boolean;
+ validityTabState: any[] = new Array<any>();
+ isEmailDistributionStatusCollapsed = true;
+ private sub: any;
+
+ @ViewChild(SingleGridMeasureDetailTabComponent) singleGridMeasureDetailTabComponent: SingleGridMeasureDetailTabComponent;
+
+ constructor(
+ public router: Router,
+ private route: ActivatedRoute,
+ private gridMeasureService: GridMeasureService,
+ private lockHelperService: LockHelperService,
+ public sessionContext: SessionContext,
+ public daterangepickerConfig: DaterangepickerConfig,
+ public roleAccessHelper: RoleAccessHelperService,
+ public modeValidator: ModeValidator,
+ private ref: ChangeDetectorRef,
+ private baseDataLoader: BaseDataLoaderService,
+ private toasterMessageService: ToasterMessageService) { }
+
+ ngAfterViewChecked() {
+ this.ref.detectChanges();
+ }
+
+ ngOnInit() {
+
+ this.sessionContext.setIsLocked(false);
+ this.roleAccessCurrUser = this.roleAccessHelper.getRoleAccessDefinitions();
+ this.id = this.route.snapshot.params.id;
+ const editModeAllowedForRole = false;
+ const mode$ = '';
+
+ this.checkModeAndInitiateGm(mode$, editModeAllowedForRole, this.id);
+
+ this.daterangepickerConfig.settings = {
+ timePicker: true,
+ timePicker24Hour: true,
+ timePickerSeconds: false,
+ timePickerIncrement: 1,
+ useCurrent: true,
+ drops: 'up',
+ locale: {
+ format: 'DD.MM.YYYY HH:mm',
+ applyLabel: 'Übernehmen',
+ cancelLabel: 'Abbrechen',
+ daysOfWeek: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'],
+ monthNames: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli',
+ 'August', 'September', 'Oktober', 'November', 'Dezember'],
+ firstDay: 1
+ }
+ };
+
+ this.toasterMessageService.toasterEmitter$.subscribe(toasterButtonEvntEn => this.processBannerButtonEvent(toasterButtonEvntEn));
+ }
+
+ private processBannerButtonEvent(toasterButtonEvntEn: any) {
+ this.onUnlock(toasterButtonEvntEn === ToasterButtonEventEn.unlockGridMeasure);
+ }
+
+ private checkModeAndInitiateGm(mode$: string, editModeAllowedForRole: boolean, id: number) {
+ if (id) {
+ this.validForUpload = true;
+ this.sub = this.
+ route.params.subscribe(params => {
+ mode$ = params['mode'];
+ if (this.mode && this.mode !== mode$) {
+ this.modeValidator.handleGridMeasureMode(this.gridMeasureDetail, this.mode);
+ }
+ if (params['mode'] !== Globals.MODE.EDIT && params['mode'] !== Globals.MODE.VIEW) {
+ this.goToOverview();
+ } else {
+ this.gridMeasureService.getGridMeasure(this.id).subscribe(async (gm) => {
+
+ this.gridMeasureDetail = gm;
+
+ this.showSpinner = false;
+ editModeAllowedForRole = this.modeValidator.isEditModeAllowed(this.gridMeasureDetail);
+ this.viewModeReadOnly = !(editModeAllowedForRole && (params['mode'] === Globals.MODE.EDIT
+ || params['mode'] === Globals.MODE.VIEW));
+ this.readOnlyForm = this.viewModeReadOnly;
+ this.emailAddFormNotAllowed = !this.modeValidator.isEmailEditModeAllowed(this.gridMeasureDetail);
+ if (!this.viewModeReadOnly) {
+ this.checkLockedByUser();
+ }
+ this.onReceiveGridMeasureDetail();
+ this.gridMeasureDetail.listSingleGridmeasures.forEach((element, index) => {
+ this.showTabs = false;
+
+ setTimeout(() => {
+ this.currentSingleGridMeasure = this.gridMeasureDetail.listSingleGridmeasures[index];
+
+ if (index === this.gridMeasureDetail.listSingleGridmeasures.length - 1) {
+ setTimeout(() => {
+ this.showTabs = true;
+ }, 50);
+ }
+ }, 50);
+ });
+ });
+ }
+ });
+ this.mode = mode$;
+ } else {
+ this.showSpinner = false;
+ this.gridMeasureDetail = new GridMeasure();
+ this.gridMeasureDetail.createUser = this.sessionContext.getCurrUser().username;
+ this.gridMeasureDetail.plannedStarttimeFirstSinglemeasure = null;
+ this.gridMeasureDetail.endtimeGridmeasure = null;
+ this.onReceiveGridMeasureDetail();
+ }
+ }
+
+ onUnlock(isUnlocked: boolean) {
+ if (this.unlockInProgress) {
+ return;
+ } else {
+ this.unlockInProgress = true;
+ }
+ const deleteEvent$: EventEmitter<boolean> = new EventEmitter<boolean>();
+ deleteEvent$.subscribe(b => this.recheckLockedByUser());
+ if (isUnlocked) {
+ this.deleteLock(true, deleteEvent$);
+ }
+ }
+ recheckLockedByUser() {
+ this.checkLockedByUser();
+ this.unlockInProgress = false;
+ }
+
+ ngOnDestroy() {
+ this.clearComponent();
+ }
+
+ private clearComponent() {
+ this.lockHelperService.deleteLock(this.id, Globals.GRIDMEASURE_LOCK_TAG, false, null);
+ if (this.sub) {
+ this.sub.unsubscribe();
+ }
+ // this.messageService.clearBannerLocalEvent$.emit(true);
+ // this.toasterComponent.clear();
+ }
+
+ private onReceiveGridMeasureDetail() {
+ if (this.gridMeasureDetail.title) {
+ this.validForSave = true;
+ } else {
+ this.validForSave = false;
+ }
+
+ if (!this.gridMeasureDetail.id) {
+ // Setzt den intialen Status auf 0 TODO: muss je nach Prozess angepasst werden (separate User Story)
+ this.gridMeasureDetail.statusId = Globals.STATUS.NEW;
+ }
+
+ if (this.gridMeasureDetail) {
+
+ // If this.roleAccessCurrUser is undefined page was refresehd or reached by external call (directlink from email).
+ if (!this.roleAccessCurrUser) {
+ this.baseDataLoader.loadBaseData();
+ this.roleAccessCurrUser = this.roleAccessHelper.getRoleAccessDefinitions();
+ }
+
+ const ctrl = this.roleAccessCurrUser.controls.filter(control => control.gridMeasureStatusId === this.gridMeasureDetail.statusId)[0];
+ this.sessionContext.setInactiveFieldsArray(ctrl.inactiveFields || []);
+ }
+
+ this.disableNewTabButtonForModeAndStatus();
+
+ this.gridMeasureDetailReceived = true;
+ }
+
+ disableNewTabButtonForModeAndStatus() {
+ const gm = this.gridMeasureDetail;
+ if (this.mode === Globals.MODE.VIEW || (gm.statusId !== Globals.STATUS.NEW && gm.statusId !== Globals.STATUS.APPLIED)) {
+ this.disableNewTabBtn = true;
+ }
+ }
+
+ goToOverview() {
+ // this.toasterComponent.clear();
+ // this.messageService.clearBannerLocalEvent$.emit(true);
+ this.router.navigate(['/overview']);
+ }
+
+ createGridMeasure(newGridMeaure?: GridMeasure, isDuplicate?: boolean) {
+ let tmpGM = new GridMeasure();
+ if (isDuplicate) {
+ tmpGM = newGridMeaure;
+ } else {
+ tmpGM = this.gridMeasureDetail;
+ }
+
+ if (this.storageInProgress) {
+ return;
+ } else {
+ this.storageInProgress = true;
+ }
+ tmpGM.createUser = tmpGM.createUser || this.sessionContext.getCurrUser().username;
+ const actualStatus = tmpGM.statusId;
+ if (!actualStatus) {
+ tmpGM.statusId = Globals.STATUS.NEW;
+ }
+ this.setPlannedDatesFromSingleGridMeasures();
+
+ tmpGM.listSingleGridmeasures.forEach(sgm => {
+ if (this.storageInProgress) {
+ if (!GridMeasureValidatorFactory.createsingleGM(this.toasterMessageService).validateEntity(sgm, true)) {
+ this.storageInProgress = false;
+ return;
+ } else {
+ return;
+ }
+ }
+
+ });
+ if (!this.storageInProgress) {
+ return;
+ }
+ if (!GridMeasureValidatorFactory.createGM(this.toasterMessageService).validateEntity(tmpGM, true)) {
+ this.storageInProgress = false;
+ return;
+ }
+
+ this.mergeDeletedStepsAndResetMinusIds();
+ this.mergeDeletedEmailDistributionEntriesAndResetMinusIds();
+ this.gridMeasureService.storeGridMeasure(tmpGM).subscribe(gm => {
+ if (!actualStatus) {
+ this.id = gm.id;
+ }
+
+ this.storageInProgress = false;
+ this.toasterMessageService.showSuccess('Netzmaßnahme "' + gm.title + '" erfolgreich gespeichert!');
+ this.validForUpload = true;
+ this.readOnlyForm = isNaN(actualStatus);
+
+ this.navigateAfterCreate(isDuplicate, gm);
+ },
+ error => {
+ this.storageInProgress = false;
+ this.toasterMessageService.showError(ErrorType.create, 'Netzmaßnahme');
+ console.log(error);
+ }
+ );
+ }
+
+ updateGridMeasureStatus(status: number) {
+ if (this.storageInProgress) {
+ return;
+ } else {
+ this.storageInProgress = true;
+ }
+ this.gridMeasureDetail.createUser = this.gridMeasureDetail.createUser || this.sessionContext.getCurrUser().username;
+ this.gridMeasureDetail.statusId = status;
+
+ this.gridMeasureDetail.listSingleGridmeasures.forEach(sgm => {
+ if (this.storageInProgress) {
+ if (!GridMeasureValidatorFactory.createsingleGM(this.toasterMessageService).validateEntity(sgm, true)) {
+ this.storageInProgress = false;
+ return;
+ } else {
+ return;
+ }
+ }
+
+ });
+ if (!this.storageInProgress) {
+ return;
+ }
+
+ if (!GridMeasureValidatorFactory.createGM(this.toasterMessageService).validateEntity(this.gridMeasureDetail, true)) {
+ this.storageInProgress = false;
+ return;
+ }
+ this.mergeDeletedStepsAndResetMinusIds();
+ this.mergeDeletedEmailDistributionEntriesAndResetMinusIds();
+ this.gridMeasureService.storeGridMeasure(this.gridMeasureDetail).subscribe(gm => {
+ this.storageInProgress = false;
+ this.toasterMessageService.showSuccess('Netzmaßnahmes "' + gm.title + '" Status erfolgreich zu "'
+ + this.sessionContext.getStatusById(gm.statusId).name + '" geändert!');
+ this.goToOverview();
+ },
+ error => {
+ this.storageInProgress = false;
+ this.toasterMessageService.showError(ErrorType.update, 'Netzmaßnahme');
+ console.log(error);
+ }
+ );
+ }
+
+ createNewSingleGridMeasureTab() {
+ if (!this.isSingleGridmeasureLimitReached()) {
+ const singleGM = new SingleGridMeasure;
+ singleGM.sortorder = this.gridMeasureDetail.listSingleGridmeasures[
+ this.gridMeasureDetail.listSingleGridmeasures.length + -1].sortorder + 1;
+ this.gridMeasureDetail.listSingleGridmeasures.push(singleGM);
+ this.currentSingleGridMeasure = singleGM;
+ Util.showSingleGridmeasureTab(singleGM.sortorder);
+ }
+ }
+
+ checkLockedByUser() {
+ const isLockedByOther$: EventEmitter<boolean> = new EventEmitter<boolean>();
+ isLockedByOther$.subscribe(locked => {
+ this.readOnlyForm = this.viewModeReadOnly || locked; // readOnly stays readonly, no matter if locked or not
+ if (!this.readOnlyForm) {
+ this.lockHelperService.createLock(this.id, Globals.GRIDMEASURE_LOCK_TAG, null);
+
+ }
+ if (locked) {
+ this.disableNewTabBtn = true;
+ this.sessionContext.setIsLocked(locked);
+ }
+ });
+
+ this.lockHelperService.checkLockedByUser(this.id, Globals.GRIDMEASURE_LOCK_TAG, isLockedByOther$);
+ }
+
+ deleteLock(force: boolean, deleteEvent$: EventEmitter<boolean>) {
+ if (force || !this.readOnlyForm) {
+ this.lockHelperService.deleteLock(this.id, Globals.GRIDMEASURE_LOCK_TAG, force, deleteEvent$);
+ this.disableNewTabBtn = false;
+ }
+ }
+
+ isSingleGridmeasureValid(singleGridmeasure: SingleGridMeasure): boolean {
+ return singleGridmeasure._isValide;
+ }
+
+ isGridMeasureDetailValid(): boolean {
+ return this.gridMeasureDetail._isValide;
+ }
+
+ isSingleGridmeasureLimitReached(): boolean {
+ if (this.gridMeasureDetail && this.gridMeasureDetail.listSingleGridmeasures) {
+ return this.gridMeasureDetail.listSingleGridmeasures.filter(singleGridMeasure =>
+ !singleGridMeasure.delete).length >= Globals.MAX_NUMBER_OF_TABS;
+ } else {
+ return false;
+ }
+ }
+
+ areAllTabsValid(): boolean {
+ return this.showTabs && this.gridMeasureDetail._isValide && this.gridMeasureDetail._isHeaderValide
+ && this.gridMeasureDetail.listSingleGridmeasures
+ && this.gridMeasureDetail.listSingleGridmeasures.filter(singleGridMeasure =>
+ !singleGridMeasure._isValide && !singleGridMeasure.delete).length === 0;
+ }
+
+ onSelectSingleGridMeasureTab(singleGridmeasure: SingleGridMeasure): void {
+ if (singleGridmeasure !== this.currentSingleGridMeasure) {
+ this.currentSingleGridMeasure = singleGridmeasure;
+ }
+ }
+
+ onSelectGridMeasureTab(): void {
+ this.setPlannedDatesFromSingleGridMeasures();
+ }
+
+ onSingleGridMeasureChanged(ev): void {
+ this.currentSingleGridMeasure = ev;
+ this.setPlannedDatesFromSingleGridMeasures();
+ }
+
+ onGridMeasureChanged(ev): void {
+ this.gridMeasureDetail = ev;
+ }
+
+ setPlannedDatesFromSingleGridMeasures() {
+ const currentPlannedBeginDates: string[] = [];
+ const currentPlannedEndDates: string[] = [];
+ currentPlannedBeginDates.push(this.currentSingleGridMeasure.plannedStarttimeSinglemeasure);
+ currentPlannedEndDates.push(this.currentSingleGridMeasure.plannedEndtimeSinglemeasure);
+ for (const sgm of this.gridMeasureDetail.listSingleGridmeasures) {
+ currentPlannedBeginDates.push(sgm.plannedStarttimeSinglemeasure);
+ currentPlannedEndDates.push(sgm.plannedEndtimeSinglemeasure);
+ }
+ this.gridMeasureDetail.plannedStarttimeFirstSinglemeasure = Util.getEarliestValidDateString(currentPlannedBeginDates);
+ this.gridMeasureDetail.endtimeGridmeasure = Util.getLatestValidDateString(currentPlannedEndDates);
+ }
+
+ mergeDeletedStepsAndResetMinusIds() {
+ this.gridMeasureDetail.listSingleGridmeasures.forEach(sgm => {
+ if (sgm.listSteps) {
+ sgm.listSteps.forEach(step => {
+ if (step.id === Globals.TEMP_ID_TO_SHOW_NEW_STEPS) {
+ delete step.id;
+ }
+ });
+ }
+ if (sgm.listStepsDeleted) {
+ sgm.listSteps.push.apply(sgm.listSteps, sgm.listStepsDeleted);
+ }
+ });
+ }
+
+ mergeDeletedEmailDistributionEntriesAndResetMinusIds() {
+
+ if (this.gridMeasureDetail.listEmailDistribution) {
+ this.gridMeasureDetail.listEmailDistribution.forEach(emailDistributionEntry => {
+ if (emailDistributionEntry.id === Globals.TEMP_ID_TO_SHOW_NEW_EMAILDISTRIBUTIONENTRYS) {
+ delete emailDistributionEntry.id;
+ }
+ });
+ }
+ const listAsStringArray = new Array<string>();
+ this.gridMeasureDetail.listEmailDistribution.filter(e => !e.preconfigured).forEach(ede => listAsStringArray.push(ede.emailAddress));
+ if (listAsStringArray.length === 0) {
+ this.gridMeasureDetail.emailAddresses = null;
+ } else {
+ this.gridMeasureDetail.emailAddresses = listAsStringArray.join(';');
+ }
+ if (this.gridMeasureDetail.listEmailDistributionDeleted) {
+ this.gridMeasureDetail.listEmailDistribution.push.apply(
+ this.gridMeasureDetail.listEmailDistribution,
+ this.gridMeasureDetail.listEmailDistributionDeleted);
+ }
+ }
+
+ duplicateGM() {
+ if (this.gridMeasureDetail.id) {
+ const duplicatedGM = CloneGridMeasureHelper.cloneGridMeasure(
+ this.gridMeasureDetail);
+ this.createGridMeasure(duplicatedGM, true);
+ this.clearComponent();
+ }
+ }
+
+ private navigateAfterCreate(isDuplicate: boolean, gm: GridMeasure) {
+ if (!isDuplicate) {
+ this.goToOverview();
+ } else {
+ this.goToOverview();
+ setTimeout(() => {
+ this.router.navigate(['/gridMeasureDetail/', gm.id, Globals.MODE.EDIT]);
+ }, 200);
+ }
+ }
+
+ goToCancelPage() {
+ this.sessionContext.setGridMeasureDetail(this.gridMeasureDetail);
+ this.sessionContext.setCancelStage(true);
+ this.router.navigate(['gridMeasureDetail/' + this.id + '/' + this.mode + '/cancelGridMeasure']);
+ }
+}
diff --git a/src/app/pages/loggedout/loggedout.component.css b/src/app/pages/loggedout/loggedout.component.css
new file mode 100644
index 0000000..66f2d8e
--- /dev/null
+++ b/src/app/pages/loggedout/loggedout.component.css
@@ -0,0 +1,24 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+.logout-outer {
+ width: 100%;
+ height: 90%;
+ position: absolute;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #f8fafd;
+}
+
+.logout-inner {
+ text-align: center;
+}
\ No newline at end of file
diff --git a/src/app/pages/loggedout/loggedout.component.html b/src/app/pages/loggedout/loggedout.component.html
new file mode 100644
index 0000000..e83d505
--- /dev/null
+++ b/src/app/pages/loggedout/loggedout.component.html
@@ -0,0 +1,25 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+-->
+
+<div class="logout-outer">
+ <div class="logout-inner">
+ <img src="assets/img/logo_openkonsequenz.png">
+ <div>
+ <h2>Sie wurden ausgeloggt ...</h2>
+ <h4>Über das Portal können Sie sich erneut einloggen</h4>
+ <button #tabCloseBtn style="margin-top: 10%" type="button" class="btn btn-primary" onclick="window.close()">Browser-Tab
+ schließen
+ </button>
+ </div>
+ </div>
+ <!-- <app-version-info>...Loading Version-Info...</app-version-info> -->
+</div>
\ No newline at end of file
diff --git a/src/app/pages/loggedout/loggedout.component.spec.ts b/src/app/pages/loggedout/loggedout.component.spec.ts
new file mode 100644
index 0000000..bb5ab5f
--- /dev/null
+++ b/src/app/pages/loggedout/loggedout.component.spec.ts
@@ -0,0 +1,35 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { LoggedoutPageComponent } from './loggedout.component';
+
+describe('LoggedoutPageComponent', () => {
+ let component: LoggedoutPageComponent;
+ let fixture: ComponentFixture<LoggedoutPageComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [LoggedoutPageComponent]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(LoggedoutPageComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/pages/loggedout/loggedout.component.ts b/src/app/pages/loggedout/loggedout.component.ts
new file mode 100644
index 0000000..64b816d
--- /dev/null
+++ b/src/app/pages/loggedout/loggedout.component.ts
@@ -0,0 +1,30 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
+
+@Component({
+ selector: 'app-loggedout',
+ templateUrl: './loggedout.component.html',
+ styleUrls: ['./loggedout.component.css']
+})
+export class LoggedoutPageComponent implements OnInit {
+
+ constructor(
+ ) { }
+
+ @ViewChild('tabCloseBtn') button: ElementRef;
+
+ ngOnInit() {
+ this.button.nativeElement.focus();
+ }
+
+}
diff --git a/src/app/pages/logout/logout.component.css b/src/app/pages/logout/logout.component.css
new file mode 100644
index 0000000..8f18cd6
--- /dev/null
+++ b/src/app/pages/logout/logout.component.css
@@ -0,0 +1,24 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+.logout-outer {
+ width: 100%;
+ height: 90%;
+ position: absolute;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #f8fafd;
+}
+
+.logout-inner {
+ text-align: center;
+}
\ No newline at end of file
diff --git a/src/app/pages/logout/logout.component.html b/src/app/pages/logout/logout.component.html
new file mode 100644
index 0000000..5be9805
--- /dev/null
+++ b/src/app/pages/logout/logout.component.html
@@ -0,0 +1,23 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+-->
+<div class="logout-outer">
+ <div class="logout-inner">
+ <div class="center">
+ <img src="assets/img/logo_openkonsequenz.png">
+ <h3>Wollen Sie sich abmelden?</h3>
+ </div>
+ <div class="center-margin">
+ <button #yesBtn id="logoutbutton" class="btn btn-primary" type="button" (click)="logout()">Ja</button>
+ <button id="homebutton" class=" btn btn-primary " (click)="goToOverview() ">Nein</button>
+ </div>
+ </div>
+</div>
\ No newline at end of file
diff --git a/src/app/pages/logout/logout.component.spec.ts b/src/app/pages/logout/logout.component.spec.ts
new file mode 100644
index 0000000..07993df
--- /dev/null
+++ b/src/app/pages/logout/logout.component.spec.ts
@@ -0,0 +1,130 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { NgModule } from '@angular/core';
+import { LogoutPageComponent } from './logout.component';
+import { LoggedoutPageComponent } from '../loggedout/loggedout.component';
+import { MockComponent } from '../../testing/mock.component';
+import { Router } from '@angular/router';
+import { MessageServiceCustom } from '../../services/message.service';
+import { AuthenticationService } from '../../services/authentication.service';
+import { AbstractMockObservableService } from '../../testing/abstract-mock-observable.service';
+import { Observable } from 'rxjs/Observable';
+import { SessionContext } from './../../common/session-context';
+
+class FakeRouter {
+ navigate(commands: any[]) {
+ return commands[0];
+ }
+}
+@NgModule({
+ declarations: [LoggedoutPageComponent]
+})
+class TestModule { }
+
+class MockAuthService extends AbstractMockObservableService {
+
+ logout() {
+ return this;
+ }
+}
+
+describe('LogoutComponent', () => {
+ let router: Router;
+ let component: LogoutPageComponent;
+ let fixture: ComponentFixture<LogoutPageComponent>;
+ let routerStub: FakeRouter;
+ let mockAuthService;
+ let messageService;
+ let sessionContext;
+
+ routerStub = {
+ navigate: jasmine.createSpy('navigate').and.callThrough()
+ };
+
+ beforeEach(async(() => {
+ sessionContext = new SessionContext();
+ router = new FakeRouter() as any as Router;
+ mockAuthService = new MockAuthService();
+ messageService = new MessageServiceCustom(sessionContext);
+
+ TestBed.configureTestingModule({
+ declarations: [LogoutPageComponent,
+ LoggedoutPageComponent,
+ MockComponent({ selector: 'app-version-info' })
+ ],
+ providers: [
+ { provide: Router, useValue: routerStub },
+ { provide: MessageServiceCustom, useValue: messageService },
+ { provide: AuthenticationService, useValue: mockAuthService },
+ { provide: SessionContext, useClass: SessionContext }
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(LogoutPageComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should navigate to Overview after click on no-button', async(() => {
+ spyOn(component, 'goToOverview').and.callThrough();
+
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ const button = fixture.debugElement.nativeElement.querySelector('button#homebutton');
+ button.click();
+ fixture.detectChanges();
+ expect(component.goToOverview).toHaveBeenCalled();
+ expect(routerStub.navigate).toHaveBeenCalledWith(['/overview']);
+ });
+ }));
+
+ it('should logout after click on yes-button', async(() => {
+ mockAuthService.content = Observable.of('');
+ spyOn(component, 'logout').and.callThrough();
+
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ const button = fixture.debugElement.nativeElement.querySelector('button#logoutbutton');
+ button.click();
+ fixture.detectChanges();
+ expect(component.logout).toHaveBeenCalled();
+ expect(routerStub.navigate).toHaveBeenCalledWith(['/loggedout']);
+ });
+ }));
+
+ it('should logout on error response', async(() => {
+ mockAuthService.error = 'MOCKERROR';
+ spyOn(component, 'logout').and.callThrough();
+
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ const button = fixture.debugElement.nativeElement.querySelector('button#logoutbutton');
+ button.click();
+ fixture.detectChanges();
+ expect(component.loggedOut).toBeFalsy();
+ expect(component.logout).toHaveBeenCalled();
+ expect(routerStub.navigate).toHaveBeenCalledWith(['/loggedout']);
+ });
+ }));
+
+});
diff --git a/src/app/pages/logout/logout.component.ts b/src/app/pages/logout/logout.component.ts
new file mode 100644
index 0000000..811ab43
--- /dev/null
+++ b/src/app/pages/logout/logout.component.ts
@@ -0,0 +1,57 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
+import { Router } from '@angular/router';
+import { MessageDefines, MessageServiceCustom } from '../../services/message.service';
+import { AuthenticationService } from '../../services/authentication.service';
+import { SessionContext } from '../../common/session-context';
+
+@Component({
+ selector: 'app-logout',
+ templateUrl: './logout.component.html',
+ styleUrls: ['./logout.component.css']
+})
+export class LogoutPageComponent implements OnInit {
+
+ @ViewChild('yesBtn') button: ElementRef;
+ loggedOut = false;
+
+ ngOnInit() {
+ this.button.nativeElement.focus();
+ }
+
+
+ constructor(
+ private msgService: MessageServiceCustom,
+ private authService: AuthenticationService,
+ private router: Router,
+ private sessionContext: SessionContext
+ ) { }
+
+ logout() {
+ this.authService.logout().subscribe(res => {
+ this.msgService.loginLogoff$.emit(MessageDefines.MSG_LOG_OFF);
+ this.loggedOut = true;
+ this.router.navigate(['/loggedout']);
+ this.sessionContext.clearStorage();
+ },
+ error => {
+ console.log(error);
+ this.router.navigate(['/loggedout']);
+ }
+ );
+ }
+
+ goToOverview(): void {
+ this.router.navigate(['/overview']);
+ }
+}
diff --git a/src/app/pages/overview/overview.component.css b/src/app/pages/overview/overview.component.css
new file mode 100644
index 0000000..71d3da3
--- /dev/null
+++ b/src/app/pages/overview/overview.component.css
@@ -0,0 +1,22 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+/* Overview styling */
+.navbar-right {
+ display: inline-flex;
+}
+.alert{
+ margin: 5px;
+}
+
+.btn-group .btn {
+ width: 85px;
+}
\ No newline at end of file
diff --git a/src/app/pages/overview/overview.component.html b/src/app/pages/overview/overview.component.html
new file mode 100644
index 0000000..a003376
--- /dev/null
+++ b/src/app/pages/overview/overview.component.html
@@ -0,0 +1,50 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+-->
+
+<div class="overview-body">
+ <div>
+ <div class="maincontent">
+ <div class="row">
+ <div class="col-xs-4">
+ <span style="font-size: 30px">Netzmaßnahmen</span>
+ </div>
+ <div class="col-xs-4 center" view="'list'">
+ <div class="btn-group">
+ <div class="btn btn-primary" (click)="view = 'list'" [class.active]="view === 'list'">
+ Tabelle
+ </div>
+ <div class="btn btn-primary" (click)="view = 'calendar'" [class.active]="view === 'calendar'">
+ Kalender
+ </div>
+ </div>
+ </div>
+ <div class="col-xs-4">
+ <div *ngIf="createGridMeasureAllowed" id="goToGridMeasureDetailBtn" class="btn btn-primary pull-right" (click)="goToGridMeasureDetail()">
+ Netzmaßnahme erstellen
+ </div>
+ </div>
+ </div>
+ <div class="panel-group">
+ <div *ngIf="view ==='calendar'">
+ <app-custom-calendar>
+ loading ...
+ </app-custom-calendar>
+ </div>
+ <div *ngIf="view ==='list'">
+ <app-grid-measures [gridId]="'gridMeasures'" [withEditButtons]="true">
+ loading ...
+ </app-grid-measures>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
\ No newline at end of file
diff --git a/src/app/pages/overview/overview.component.spec.ts b/src/app/pages/overview/overview.component.spec.ts
new file mode 100644
index 0000000..d087b5e
--- /dev/null
+++ b/src/app/pages/overview/overview.component.spec.ts
@@ -0,0 +1,95 @@
+/**
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { MainNavigationComponent } from '../../common-components/main-navigation/main-navigation.component';
+import { MockComponent } from '../../testing/mock.component';
+import { OverviewComponent } from './overview.component';
+import { Router } from '@angular/router';
+import { SessionContext } from '../../common/session-context';
+import { NgModule } from '@angular/core';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { VersionInfoComponent } from '../../common-components/version-info/version-info.component';
+import { USERS } from '../../test-data/users';
+
+class FakeRouter {
+ navigate(commands: any[]) {
+ return commands[0];
+ }
+}
+
+@NgModule({
+ declarations: [],
+ entryComponents: [
+ VersionInfoComponent
+ ]
+})
+class TestModule { }
+
+describe('OverviewComponent', () => {
+ const sessionContext: SessionContext = new SessionContext();
+ let component: OverviewComponent;
+ let fixture: ComponentFixture<OverviewComponent>;
+ let routerStub: FakeRouter;
+
+ routerStub = {
+ navigate: jasmine.createSpy('navigate').and.callThrough()
+ };
+
+ beforeEach(async(() => {
+
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule,
+ BrowserAnimationsModule
+ // ,
+ // NoopAnimationsModule
+ ],
+ declarations: [
+ OverviewComponent,
+ MainNavigationComponent,
+ MockComponent({ selector: 'app-custom-calendar', inputs: ['view', 'viewDate', 'viewDateChange', 'viewDate'] }),
+ MockComponent({ selector: 'app-grid-measures', inputs: ['gridId', 'withEditButtons'] }),
+ MockComponent({ selector: 'app-version-info' })
+ ],
+ providers: [
+ { provide: Router, useValue: routerStub },
+ { provide: SessionContext, useValue: sessionContext }
+ ],
+ }).compileComponents();
+
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(OverviewComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should navigate to grid-measure-detail on button-click', async(() => {
+ sessionContext.setCurrUser(USERS[1]);
+
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ const button = fixture.debugElement.nativeElement.querySelector('div#goToGridMeasureDetailBtn');
+ button.click();
+ fixture.detectChanges();
+ expect(routerStub.navigate).toHaveBeenCalledWith(['/gridMeasureDetail']);
+ });
+
+ }));
+
+});
diff --git a/src/app/pages/overview/overview.component.ts b/src/app/pages/overview/overview.component.ts
new file mode 100644
index 0000000..2b8bce9
--- /dev/null
+++ b/src/app/pages/overview/overview.component.ts
@@ -0,0 +1,43 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Globals } from './../../common/globals';
+import { Component, OnInit } from '@angular/core';
+import { SessionContext } from '../../common/session-context';
+import { User } from '../../model/user';
+import { Router } from '@angular/router';
+
+@Component({
+ selector: 'app-overview',
+ templateUrl: './overview.component.html',
+ styleUrls: ['./overview.component.css']
+})
+
+export class OverviewComponent implements OnInit {
+
+ user: User;
+ view: any = 'list';
+ createGridMeasureAllowed = false;
+
+ constructor(public router: Router,
+ public sessionContext: SessionContext) { }
+
+ ngOnInit() {
+ this.user = this.sessionContext.getCurrUser();
+ this.createGridMeasureAllowed = this.user.roles.includes(Globals.OAUTH2CONF_SUPERUSER_ROLE) ||
+ this.user.roles.includes(Globals.OAUTH2CONF_MEASUREAPPLICANT_ROLE);
+ this.sessionContext.setCancelStage(false);
+ }
+
+ goToGridMeasureDetail() {
+ this.router.navigate(['/gridMeasureDetail']);
+ }
+}
diff --git a/src/app/pages/single-grid-measure-detail-tab/single-grid-measure-detail-tab.component.css b/src/app/pages/single-grid-measure-detail-tab/single-grid-measure-detail-tab.component.css
new file mode 100644
index 0000000..58e9052
--- /dev/null
+++ b/src/app/pages/single-grid-measure-detail-tab/single-grid-measure-detail-tab.component.css
@@ -0,0 +1,45 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+@import url("../grid-measure-detail/grid-measure-detail.component.css");
+.actions {
+ display: block;
+ padding: 15px 0 25px 0;
+}
+
+#deleteSingleGridMeasureBtnIcon {
+ background: transparent;
+ border: none;
+ color: white;
+ font-size: 107%;
+}
+
+#deleteSingleGridMeasureBtnIcon:focus {
+ outline: 0 !important;
+}
+
+.step-container {
+ padding-left: 0;
+}
+
+.steps-grid-container {
+ padding-right: 0;
+}
+
+@media (max-width: 991px) {
+ .step-container {
+ padding: 0;
+ }
+ .steps-grid-container {
+ padding: 30px 0 0 0;
+ }
+}
\ No newline at end of file
diff --git a/src/app/pages/single-grid-measure-detail-tab/single-grid-measure-detail-tab.component.html b/src/app/pages/single-grid-measure-detail-tab/single-grid-measure-detail-tab.component.html
new file mode 100644
index 0000000..1fa3626
--- /dev/null
+++ b/src/app/pages/single-grid-measure-detail-tab/single-grid-measure-detail-tab.component.html
@@ -0,0 +1,130 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+******************************************************************************
+-->
+<!-- <form #singleGridForm="ngForm" *ngIf="!showSpinnerSingleGrid" [singleGridMeasureFormValid]="!singleGridForm.form.valid"> -->
+<div #singleGridMeasureDetailFormCotainer>
+ <form #singleGridForm="ngForm" (change)="onSingleGridFormValidation(singleGridForm.form.valid)">
+ <fieldset class="form-group" disabled="{{ readOnlyForm ? 'disabled' : ''}}">
+ <div class="row actions">
+ <div class="col-lg-12">
+ <button class="btn btn-primary pull-right" id="createInvertedStep" (click)="duplicateSingleGM()" [disabled]="isSingleGridmeasureLimitReached()">Rückschaltung
+ planen
+ </button>
+ <button class="btn btn-danger pull-left" id="deleteSingleGridMeasureBtn" (click)="onDeleteBtnClick()" [disabled]="isLastSingleGridmeasure()">Einzelmaßnahme
+ löschen
+ <span class="glyphicon glyphicon-trash" id="deleteSingleGridMeasureBtnIcon"></span>
+ </button>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-12">
+ <label class="form-field-label" for="titleControl">Titel der Einzelmaßnahme</label>
+ <input autofocus maxlength="256" [required]="true" type="text" name="singleGMTitle" id="singleGMTitle" class="form-control"
+ [(ngModel)]="singleGridMeasure.title" />
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-4">
+ <label class="form-field-label" for="plannedStarttimeSinglemeasure">Beginn der geplanten Einzelmaßnahme</label>
+ <div class="input-group">
+ <label class="input-group-addon dateRangePickerIcon" for="plannedStarttimeSinglemeasure">
+ <span class="glyphicon glyphicon-calendar"></span>
+ </label>
+ <input #input *ngIf="singleGridMeasure.plannedStarttimeSinglemeasure" maxlength="256" [required]="true" type="text" [pattern]="dateTimePattern"
+ name="plannedStarttimeSinglemeasure" id="plannedStarttimeSinglemeasure" class="form-control" [ngModel]="singleGridMeasure.plannedStarttimeSinglemeasure|date:dateFormatLocale "
+ (ngModelChange)="singleGridMeasure.plannedStarttimeSinglemeasure = $event.target?.value" daterangepicker [options]="{drops: calcDatepickerDropOrientation(input), autoUpdateInput: false,startDate:singleGridMeasure.plannedStarttimeSinglemeasure | stringToDate , singleDatePicker: true}"
+ (applyDaterangepicker)="singleGridMeasure.plannedStarttimeSinglemeasure=$event.picker.startDate; " (selected)="selectedDate($event, 'singleGridMeasure.plannedStarttimeSinglemeasure')"
+ />
+ <input #input *ngIf="!singleGridMeasure.plannedStarttimeSinglemeasure" maxlength="256" [required]="true" type="text" [pattern]="dateTimePattern"
+ [ngModel]="singleGridMeasure.plannedStarttimeSinglemeasure" name="plannedStarttimeSinglemeasure" id="plannedStarttimeSinglemeasure"
+ class="form-control" autocomplete="no" daterangepicker [options]="{drops: calcDatepickerDropOrientation(input), autoUpdateInput: false, singleDatePicker: true}"
+ (applyDaterangepicker)="singleGridMeasure.plannedStarttimeSinglemeasure=$event.picker.startDate;" (selected)="selectedDate($event, 'singleGridMeasure.plannedStarttimeSinglemeasure')"
+ />
+ </div>
+ </div>
+
+
+ <div class="col-md-4">
+ <label class="form-field-label" for="plannedEndtimeSinglemeasure">Ende der geplanten Einzelmaßnahme</label>
+ <div class="input-group">
+ <label class="input-group-addon dateRangePickerIcon" for="plannedEndtimeSinglemeasure">
+ <span class="glyphicon glyphicon-calendar"></span>
+ </label>
+ <input #input *ngIf="singleGridMeasure.plannedEndtimeSinglemeasure" maxlength="256" [required]="true" type="text" [pattern]="dateTimePattern"
+ name="plannedEndtimeSinglemeasure" id="plannedEndtimeSinglemeasure" class="form-control" daterangepicker [ngModel]="singleGridMeasure.plannedEndtimeSinglemeasure|date:dateFormatLocale "
+ (ngModelChange)="singleGridMeasure.plannedEndtimeSinglemeasure = $event.target?.value" [options]="{drops: calcDatepickerDropOrientation(input), autoUpdateInput: false, startDate:singleGridMeasure.plannedEndtimeSinglemeasure | stringToDate , singleDatePicker: true}"
+ (applyDaterangepicker)="singleGridMeasure.plannedEndtimeSinglemeasure=$event.picker.startDate; " (selected)="selectedDate($event, 'singleGridMeasure.plannedEndtimeSinglemeasure')"
+ />
+ <input #input *ngIf="!singleGridMeasure.plannedEndtimeSinglemeasure" maxlength="256" [required]="true" type="text" [pattern]="dateTimePattern"
+ [ngModel]="singleGridMeasure.plannedEndtimeSinglemeasure" name="plannedEndtimeSinglemeasure" id="plannedEndtimeSinglemeasure"
+ class="form-control" autocomplete="no" daterangepicker [options]="{drops: calcDatepickerDropOrientation(input), autoUpdateInput: false, singleDatePicker: true}"
+ (applyDaterangepicker)="singleGridMeasure.plannedEndtimeSinglemeasure=$event.picker.startDate;" (selected)="selectedDate($event, 'singleGridMeasure.plannedEndtimeSinglemeasure')"
+ />
+ </div>
+ </div>
+ <div class="col-md-4">
+ <label class="form-field-label" for="affectedResourceControl">Betroffenes Objekt / Betriebsmittel</label>
+ <input disabled="disabled" maxlength="256" [required]="true" type="text" name="affectedResource" id="affectedResourceControl"
+ list="affectedResourcesList" [(ngModel)]="gridMeasureDetail.affectedResource" autocomplete="off" class="form-control"
+ />
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-4">
+ <label class="form-field-label" for="responsibleOnSiteName">Verantwortlich Vor-Ort</label>
+ <input maxlength="256" [required]="true" type="text" list="responsibleOnSiteNameList" name="responsibleOnSiteName" id="responsibleOnSiteName"
+ [(ngModel)]="singleGridMeasure.responsibleOnSiteName" class="form-control" autocomplete="off" />
+ <datalist id="responsibleOnSiteNameList">
+ <option value=""></option>
+ <option *ngFor="let responsibleOnSiteNameString of responsibleOnSiteNameList" value="{{ responsibleOnSiteNameString }}">{{responsibleOnSiteNameString}}</option>
+ </datalist>
+ </div>
+
+ <div class="col-md-4">
+ <label class="form-field-label" for="responsibleOnSiteDepartment">Abteilung Verantwortlicher Vor-Ort</label>
+ <input maxlength="256" [required]="false" type="text" list="responsibleOnSiteDepartmentList" name="responsibleOnSiteDepartment"
+ id="responsibleOnSiteDepartment" [(ngModel)]="singleGridMeasure.responsibleOnSiteDepartment" class="form-control"
+ autocomplete="off" />
+ <datalist id="responsibleOnSiteDepartmentList">
+ <option *ngFor="let department of responsibleOnSiteDepartmentList">{{ department.name }}</option>
+ </datalist>
+ </div>
+ <div class="col-md-4">
+ <label class="form-field-label" for="networkControl">Netzführung / Netzservice / Genehmiger</label>
+ <input maxlength="256" [required]="false" type="text" list="networkControlsList" name="networkControl" id="networkControl"
+ [(ngModel)]="singleGridMeasure.networkControl" class="form-control" autocomplete="off" />
+ <datalist id="networkControlsList">
+ <option *ngFor="let networkControlsString of networkControlsList">{{networkControlsString}}</option>
+ </datalist>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-lg-12">
+ <label class="form-field-label" for="singleGMDescription">Beschreibung</label>
+ <textarea style="resize:none;" maxlength="1024" [required]="true" rows="3" name="singleGMDescription" id="singleGMDescription"
+ class="form-control" [(ngModel)]="singleGridMeasure.description"></textarea>
+ </div>
+ </div>
+ <hr>
+ </fieldset>
+
+ <div class="col-md-4 step-container">
+ <app-step style="width: 100%" [isReadOnlyForm]="readOnlyForm" [(singleGridMeasure)]="singleGridMeasure">
+ loading ...
+ </app-step>
+ </div>
+ <div class="col-md-8 steps-grid-container">
+ <app-steps style="width: 100%" [gridId]="'steps'" [withEditButtons]="true" [gridMeasureDetail]="gridMeasureDetail" [(singleGridMeasure)]="singleGridMeasure">
+ loading ...
+ </app-steps>
+ </div>
+ </form>
+</div>
\ No newline at end of file
diff --git a/src/app/pages/single-grid-measure-detail-tab/single-grid-measure-detail-tab.component.spec.ts b/src/app/pages/single-grid-measure-detail-tab/single-grid-measure-detail-tab.component.spec.ts
new file mode 100644
index 0000000..aff8551
--- /dev/null
+++ b/src/app/pages/single-grid-measure-detail-tab/single-grid-measure-detail-tab.component.spec.ts
@@ -0,0 +1,303 @@
+import { Router } from '@angular/router';
+import { ModeValidator } from './../../custom_modules/helpers/mode-validator';
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+******************************************************************************
+ */
+
+import { async, fakeAsync, tick, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SingleGridMeasureDetailTabComponent } from './single-grid-measure-detail-tab.component';
+import { StringToDatePipe } from '../../common-components/pipes/string-to-date.pipe';
+import { MockComponent } from '../../testing/mock.component';
+import { FormsModule } from '@angular/forms';
+import { SimpleChange } from '@angular/core';
+import { SessionContext } from './../../common/session-context';
+import { CimCacheService } from './../../services/cim-cache.service';
+import { AbstractMockObservableService } from '../../testing/abstract-mock-observable.service';
+import { GRIDMEASURE } from '../../test-data/grid-measures';
+import { RESSOURCETYPESRESPONSE, RESSOURCEWITHTYPERESPONSE, RESSOURCEWITHTYPERESPONSE_2 } from '../../test-data/cim-cache-responses';
+import { GridMeasureService } from '../../services/grid-measure.service';
+import { RoleAccessHelperService } from '../../services/jobs/role-access-helper.service';
+import { Util } from '../../common/util';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+import { MessageService } from 'primeng/api';
+
+
+describe('SingleGridMeasureDetailTabComponent', () => {
+ let component: SingleGridMeasureDetailTabComponent;
+ let fixture: ComponentFixture<SingleGridMeasureDetailTabComponent>;
+
+ class MockCimCacheService extends AbstractMockObservableService {
+ getRessourceTypes() {
+ return this;
+ }
+
+ getRessourcesWithType() {
+ return this;
+ }
+ }
+
+ class MockGridmeasureService extends AbstractMockObservableService {
+ storeGridMeasure() {
+ return this;
+ }
+ getGridMeasure(id: number) {
+ return this;
+ }
+ }
+
+ let mockCimCacheService;
+ let sessionContext: SessionContext;
+ let mockGridmeasureService;
+ let roleAccessHelper;
+ let toasterMessageService: ToasterMessageService;
+ let messageService: MessageService;
+
+ beforeEach(async(() => {
+ mockCimCacheService = new MockCimCacheService();
+ sessionContext = new SessionContext();
+ mockGridmeasureService = new MockGridmeasureService();
+ roleAccessHelper = new RoleAccessHelperService();
+ messageService = new MessageService;
+ toasterMessageService = new ToasterMessageService(sessionContext, messageService);
+
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule
+ ],
+ declarations: [
+ SingleGridMeasureDetailTabComponent,
+ StringToDatePipe,
+ MockComponent({ selector: 'input', inputs: ['options'] }),
+ MockComponent({
+ selector: 'app-step',
+ inputs: ['isReadOnlyForm', 'singleGridMeasure', 'gridMeasureDetail']
+ }),
+ MockComponent({
+ selector: 'app-steps',
+ inputs: ['gridId', 'withEditButtons', 'singleGridMeasure', 'gridMeasureDetail']
+ })
+ ],
+ providers: [
+ ModeValidator,
+ { provide: Router },
+ { provide: CimCacheService, useValue: mockCimCacheService },
+ { provide: SessionContext, useClass: SessionContext },
+ { provide: GridMeasureService, useValue: mockGridmeasureService },
+ { provide: RoleAccessHelperService, useValue: roleAccessHelper },
+ { provide: ToasterMessageService, useValue: toasterMessageService }
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SingleGridMeasureDetailTabComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ xit('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should set form in readonly mode', () => {
+ component.readOnlyForm = false;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ isReadOnlyForm: new SimpleChange(component.readOnlyForm, true, true)
+ });
+ fixture.detectChanges();
+ expect(component.readOnlyForm).toBeTruthy();
+ });
+
+ it('should have one singlegridmeasure', () => {
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(GRIDMEASURE[0]));
+ expect(component.gridMeasureDetail.listSingleGridmeasures.length).toBe(2);
+ const gridMeasure = component.gridMeasureDetail;
+ gridMeasure.listSingleGridmeasures.pop();
+
+ fixture.detectChanges();
+ component.ngOnChanges({
+ gridMeasureDetail: new SimpleChange(component.gridMeasureDetail, gridMeasure, true)
+ });
+ fixture.detectChanges();
+ expect(component.gridMeasureDetail.listSingleGridmeasures.length).toBe(1);
+
+ gridMeasure.listSingleGridmeasures.pop();
+ fixture.detectChanges();
+ component.ngOnChanges({
+ gridMeasureDetail: new SimpleChange(component.gridMeasureDetail, gridMeasure, true)
+ });
+ fixture.detectChanges();
+ expect(component.gridMeasureDetail.listSingleGridmeasures.length).toBe(1);
+
+ });
+
+ it('should handle cim-service error on init', fakeAsync(() => {
+ spyOn(console, 'log').and.callThrough();
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(GRIDMEASURE[0]));
+ mockCimCacheService.error = 'CimCache error';
+ mockCimCacheService.content = [];
+
+ component.ngOnInit();
+ tick();
+
+ fixture.detectChanges();
+ expect(console.log).toHaveBeenCalled();
+ }));
+
+ it('should handle cim-service error on onChangeResourceGroup', fakeAsync(() => {
+ spyOn(console, 'log').and.callThrough();
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(GRIDMEASURE[0]));
+ mockCimCacheService.error = 'CimCache error';
+ mockCimCacheService.content = [];
+
+ component.onChangeResourceGroup('fake');
+ tick();
+
+ fixture.detectChanges();
+ expect(console.log).toHaveBeenCalled();
+ }));
+
+ it('should call getRessourceTypes on init', fakeAsync(() => {
+
+ spyOn((component as any), 'getRessourceTypes').and.callThrough();
+ spyOn((component as any), 'processRessourceTypesResponse').and.callThrough();
+ spyOn((component as any), 'getRessourceTypesWithType').and.callThrough();
+ (component as any).cimCacheService.content = RESSOURCETYPESRESPONSE;
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(GRIDMEASURE[0]));
+
+ component.ngOnInit();
+ tick();
+
+ fixture.detectChanges();
+ expect((component as any).getRessourceTypes).toHaveBeenCalled();
+
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect((component as any).processRessourceTypesResponse).toHaveBeenCalled();
+
+ component.onChangeResourceGroup(null);
+ fixture.detectChanges();
+ expect(component.singleGMAffectedResourcesList.length).toBe(0);
+
+ component.onChangeResourceGroup('ac-line-segment');
+ fixture.detectChanges();
+ expect((component as any).getRessourceTypesWithType).toHaveBeenCalled();
+ });
+ }));
+
+ it('should convert xmlstring to json correctly', () => {
+ const xmlstring = `<root>
+ <child><textNode>First & Child</textNode></child>
+ <child><textNode>Second Child</textNode></child>
+ <testAttrs attr1='attr1Value'/>
+ </root>`;
+ const jsonstring = `{"root":{"child":[{"textNode":"First & Child"},{"textNode":"Second Child"}],"testAttrs":{"_attr1":"attr1Value"}}}`;
+ expect(JSON.stringify(component.convertXmlToJsonObj(xmlstring))).toBe(jsonstring);
+ });
+
+ it('should processRessourceWithTypeResponse correctly', () => {
+ const res_input = RESSOURCEWITHTYPERESPONSE;
+
+ component.singleGridMeasure.powerSystemResource = null;
+ component.processRessourceWithTypeResponse(res_input);
+ fixture.detectChanges();
+ expect(component.singleGridMeasure.powerSystemResource.cimName).toContain(' - PowerTransformer');
+ });
+
+ it('should processRessourceWithTypeResponse with array type correctly', () => {
+ const res_input = RESSOURCEWITHTYPERESPONSE_2;
+
+ component.singleGMAffectedResourcesList = [];
+ component.processRessourceWithTypeResponse(res_input);
+ fixture.detectChanges();
+ expect(component.singleGMAffectedResourcesList.length).toBe(2);
+ });
+
+ it('should processRessourceTypesResponse correctly', () => {
+ const res_input = RESSOURCETYPESRESPONSE;
+ component.singleGMAffectedResourcesGroupList = [];
+ component.processRessourceTypesResponse(res_input);
+ fixture.detectChanges();
+ expect(component.singleGMAffectedResourcesGroupList.length).toBe(21);
+ });
+
+ // it('should init InactiveFields correctly with readonly form=false', () => {
+ // const el: HTMLElement = component.singleGridMeasureDetailFormContainer.nativeElement as HTMLElement;
+ // component.readOnlyForm = false;
+ // const fields: NodeListOf<Element> = el.querySelectorAll('#singleGMAffectedResourcesGroupList');
+ // const htmlElem: HTMLInputElement = fields.item(0) as HTMLInputElement;
+ // htmlElem.disabled = true;
+ // component.initInactiveFields();
+ // expect((fields.item(0) as HTMLInputElement).disabled).toBe(false);
+ // });
+
+ // it('should init InactiveFields correctly with readonly form=true', () => {
+ // const el: HTMLElement = component.singleGridMeasureDetailFormContainer.nativeElement as HTMLElement;
+ // component.readOnlyForm = true;
+ // const fields: NodeListOf<Element> = el.querySelectorAll('#singleGMAffectedResourcesGroupList');
+ // const htmlElem: HTMLInputElement = fields.item(0) as HTMLInputElement;
+ // htmlElem.disabled = false;
+ // component.initInactiveFields();
+ // expect((fields.item(0) as HTMLInputElement).disabled).toBe(true);
+ // });
+
+ it('should work with duplicateSingleGM correctly', () => {
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(GRIDMEASURE[0]));
+ component.singleGridMeasure = JSON.parse(JSON.stringify(GRIDMEASURE[0].listSingleGridmeasures[0]));
+
+ const lengthBefore = component.gridMeasureDetail.listSingleGridmeasures.length;
+ const compList = component.gridMeasureDetail.listSingleGridmeasures[0].listSteps;
+
+ component.duplicateSingleGM();
+
+ expect(component.gridMeasureDetail.listSingleGridmeasures.length).toBe(lengthBefore + 1);
+ expect(compList[0].switchingObject).toBe(
+ component.gridMeasureDetail.listSingleGridmeasures[lengthBefore].listSteps[compList.length - 1].switchingObject);
+ });
+
+ it('should emit decision message after click on delete', () => {
+ spyOn((component as any).toasterMessageService, 'showSingleGMDeleteConfirm');
+
+ component.onDeleteBtnClick();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect((component as any).toasterMessageService.showSingleGMDeleteConfirm).toHaveBeenCalled();
+ });
+ });
+
+ it('should process delete and emit info message', () => {
+ spyOn((component as any).toasterMessageService, 'showSuccess');
+ spyOn(component, 'getPreviousSingleGridmeasure').and.callThrough();
+ spyOn(Util, 'showGridmeasureTab');
+ component.gridMeasureDetail = JSON.parse(JSON.stringify(GRIDMEASURE[0]));
+ component.singleGridMeasure = JSON.parse(JSON.stringify(GRIDMEASURE[0].listSingleGridmeasures[1]));
+
+ expect(component.singleGridMeasure.id).toBe(5);
+ (component as any).processDeleteSingleGridMeasure();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect((component as any).toasterMessageService.showSuccess).toHaveBeenCalled();
+ expect(component.getPreviousSingleGridmeasure).toHaveBeenCalled();
+ expect(component.singleGridMeasure.id).toBe(3);
+ });
+
+ (component as any).processDeleteSingleGridMeasure();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect((component as any).messageService.emitInfo).toHaveBeenCalled();
+ expect(component.getPreviousSingleGridmeasure).toHaveBeenCalled();
+ expect(Util.showGridmeasureTab).toHaveBeenCalled();
+ });
+ });
+
+});
diff --git a/src/app/pages/single-grid-measure-detail-tab/single-grid-measure-detail-tab.component.ts b/src/app/pages/single-grid-measure-detail-tab/single-grid-measure-detail-tab.component.ts
new file mode 100644
index 0000000..e10d13c
--- /dev/null
+++ b/src/app/pages/single-grid-measure-detail-tab/single-grid-measure-detail-tab.component.ts
@@ -0,0 +1,299 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+******************************************************************************
+ */
+import {
+ Component, OnInit, Input, OnChanges, SimpleChanges, ViewChild, AfterViewChecked, ElementRef, Output,
+ EventEmitter, OnDestroy, AfterViewInit
+} from '@angular/core';
+import { GridMeasure } from '../../model/grid-measure';
+import { SingleGridMeasure } from '../../model/single-grid-measure';
+import * as X2JS from '../../../assets/js/xml2json.min.js';
+import { ErrorType } from '../../common/enums';
+import { CimCacheService } from '../../services/cim-cache.service';
+import { PowerSystemResource } from './../../model/power-system-resource';
+import { FormGroup } from '@angular/forms';
+import { Util } from '../../common/util';
+import { ToasterButtonEventEn } from './../../common/enums';
+import { Globals } from './../../common/globals';
+import { Subscription } from 'rxjs/Subscription';
+import { CloneGridMeasureHelper } from '../../custom_modules/helpers/clone-grid-measure-helper';
+import { RoleAccessHelperService } from '../../services/jobs/role-access-helper.service';
+import { SessionContext } from '../../common/session-context';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+import { UserDepartment } from '../../model/user-department';
+
+@Component({
+ selector: 'app-single-grid-measure-detail-tab',
+ templateUrl: './single-grid-measure-detail-tab.component.html',
+ styleUrls: ['./single-grid-measure-detail-tab.component.css']
+})
+export class SingleGridMeasureDetailTabComponent implements OnInit, OnChanges, OnDestroy, AfterViewChecked, AfterViewInit {
+ @Input() isReadOnlyForm: boolean;
+ @Input() gridMeasureDetail: GridMeasure = new GridMeasure();
+ @Input() dateTimePattern: string;
+ @Input() dateFormatLocale: string;
+ @Input() singleGridMeasure: SingleGridMeasure = new SingleGridMeasure();
+ @Output() singleGridMeasureChanged = new EventEmitter<SingleGridMeasure>();
+
+ form: HTMLFormElement;
+ readOnlyForm: boolean;
+ singleGridMeasureFormValid: boolean;
+ storageInProgress: boolean;
+
+ currentPowerSystemResourceDB = new PowerSystemResource();
+ tmpPowerSystemResource = new PowerSystemResource();
+ currSelectedResourceGroup: string;
+ singleGMAffectedResourcesGroupList = [''];
+ singleGMAffectedResourcesList: Array<PowerSystemResource> = [];
+ calcDatepickerDropOrientation = Util.calcDatepickerDropOrientation;
+ subscription: Subscription;
+ inactiveFields: Array<string> = [];
+ responsibleOnSiteNameList: string[];
+ networkControlsList: string[];
+ responsibleOnSiteDepartmentList: UserDepartment[];
+
+ @ViewChild('singleGridMeasureDetailFormCotainer') singleGridMeasureDetailFormContainer: ElementRef;
+ @ViewChild('singleGridForm') singleGridForm: FormGroup;
+ constructor(private cimCacheService: CimCacheService,
+ public roleAccessHelper: RoleAccessHelperService,
+ public sessionContext: SessionContext,
+ private toasterMessageService: ToasterMessageService) { }
+
+ ngOnInit() {
+ this.networkControlsList = this.sessionContext.getNetworkControlsFromSingleGridmeasures();
+ this.responsibleOnSiteNameList = this.sessionContext.getResponsiblesOnSiteFromGridmeasures();
+ this.responsibleOnSiteDepartmentList = this.sessionContext.getAllUserDepartments();
+
+ this.inactiveFields = this.sessionContext.getInactiveFields();
+ this.getRessourceTypes();
+ if (this.singleGridMeasure.powerSystemResource) {
+ this.currentPowerSystemResourceDB = JSON.parse(JSON.stringify(this.singleGridMeasure.powerSystemResource));
+ }
+ this.subscription = this.toasterMessageService.toasterEmitter$.subscribe(
+ toasterButtonEventEn => this.processToasterButtonEvent(toasterButtonEventEn));
+
+ }
+
+ private processToasterButtonEvent(toasterButtonEventEn: any) {
+ if (toasterButtonEventEn === ToasterButtonEventEn.deleteSingleGridMeasure) {
+ this.processDeleteSingleGridMeasure();
+ }
+ }
+
+ ngAfterViewInit() {
+ this.initInactiveFields();
+ }
+
+ ngAfterViewChecked() {
+ if (this.singleGridForm) {
+ this.singleGridMeasure._isValide = this.singleGridForm.valid;
+ }
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+
+ if (changes['isReadOnlyForm']) {
+ this.readOnlyForm = changes['isReadOnlyForm'].currentValue;
+ this.initInactiveFields();
+ }
+ if (changes['gridMeasureDetail'] && changes['gridMeasureDetail'].currentValue) {
+ if (!this.gridMeasureDetail.listSingleGridmeasures || this.gridMeasureDetail.listSingleGridmeasures.length === 0) {
+ this.gridMeasureDetail.listSingleGridmeasures = new Array();
+ const singleGM = new SingleGridMeasure();
+ singleGM.sortorder = 1;
+ this.gridMeasureDetail.listSingleGridmeasures.push(singleGM);
+ }
+
+ this.singleGridMeasure = this.gridMeasureDetail.listSingleGridmeasures[0];
+ }
+ }
+
+ ngOnDestroy() {
+ this.subscription.unsubscribe();
+ }
+
+ public selectedDate(value: any, datepicker?: any) {
+ this.gridMeasureDetail[datepicker] = value.start._d;
+ }
+
+ public onDeleteBtnClick() {
+ this.toasterMessageService.showSingleGMDeleteConfirm('Wollen Sie die Einzelmaßnahme wirklich löschen?');
+ }
+
+ public duplicateSingleGM() {
+ const newReversedSingleGridMeasure = CloneGridMeasureHelper.cloneSingleGridMeasure(
+ this.gridMeasureDetail, this.singleGridMeasure);
+
+ this.gridMeasureDetail.listSingleGridmeasures.push(newReversedSingleGridMeasure);
+ this.singleGridMeasure = newReversedSingleGridMeasure;
+ this.singleGridMeasureChanged.emit(this.singleGridMeasure);
+ Util.showSingleGridmeasureTab(newReversedSingleGridMeasure.sortorder);
+ }
+
+ private processDeleteSingleGridMeasure() {
+ if (this.singleGridMeasure.id) {
+ this.singleGridMeasure.delete = true;
+ } else {
+ this.gridMeasureDetail.listSingleGridmeasures = this.gridMeasureDetail.listSingleGridmeasures.filter(singleGridMeasure => {
+ return singleGridMeasure.sortorder !== this.singleGridMeasure.sortorder;
+ });
+ }
+
+ this.toasterMessageService.showSuccess('Einzelmaßnahme wurde gelöscht.');
+
+ const previousSingleGridMeasure: SingleGridMeasure = this.getPreviousSingleGridmeasure();
+
+ if (previousSingleGridMeasure) {
+ this.singleGridMeasure = previousSingleGridMeasure;
+ this.singleGridMeasureChanged.emit(this.singleGridMeasure);
+ Util.showSingleGridmeasureTab(previousSingleGridMeasure.sortorder);
+ } else {
+ Util.showGridmeasureTab();
+ }
+ }
+
+ onSingleGridFormValidation(valid: boolean) {
+ this.singleGridMeasure._isValide = valid;
+ }
+
+ private getRessourceTypes() {
+ this.cimCacheService.getRessourceTypes().subscribe(res => {
+ this.processRessourceTypesResponse(res);
+ },
+ error => {
+ this.toasterMessageService.showError(ErrorType.retrieve, 'CimCache');
+ console.log(error);
+ });
+ }
+
+ onChangeResourceGroup(val) {
+ if (val) {
+ this.currSelectedResourceGroup = val;
+ this.singleGMAffectedResourcesList = [];
+ this.getRessourceTypesWithType(val);
+ } else {
+ this.singleGMAffectedResourcesList = [];
+ }
+
+ }
+
+ onChangeResourceType(val) {
+ this.singleGridMeasure.powerSystemResource.cimName = this.currSelectedResourceGroup + ' - ' + val.cimName;
+ }
+
+ private getRessourceTypesWithType(value: string) {
+ this.cimCacheService.getRessourcesWithType(value).subscribe(res => {
+ this.processRessourceWithTypeResponse(res);
+ },
+ error => {
+ // this.messageService.emitError('CimCache', ErrorType.retrieve);
+ this.toasterMessageService.showError(ErrorType.retrieve, 'CimCache');
+ console.log(error);
+ });
+ }
+ public initInactiveFields() {
+ const el: HTMLElement = this.singleGridMeasureDetailFormContainer.nativeElement as HTMLElement;
+ const fields = el.querySelectorAll('*[id]');
+ for (let index = 0; index < fields.length; index++) {
+ const field = fields[index];
+ if (this.readOnlyForm || this.isFieldInactive(field['id'])) {
+ field.setAttribute('disabled', 'disabled');
+ } else {
+ if (!(field.id === 'deleteSingleGridMeasureBtn' && this.isLastSingleGridmeasure())) {
+ field.removeAttribute('disabled');
+ }
+ }
+ }
+ }
+
+ private isFieldInactive(fieldName: string): boolean {
+ return this.inactiveFields.filter(field => field === fieldName).length > 0;
+ }
+ convertXmlToJsonObj(xml: string) {
+ return new X2JS().xml_str2json(xml);
+ }
+
+ processRessourceWithTypeResponse(res: string): void {
+ const jsonObj = this.convertXmlToJsonObj(res);
+ const powerSystemResources = jsonObj.ResponseMessage.Payload.PowerSystemResources;
+
+ /* tslint:disable */
+ for (const prop in powerSystemResources) {
+ const anonymousPowerSystemResources = powerSystemResources[prop];
+
+ if (Array.isArray(anonymousPowerSystemResources)) {
+ anonymousPowerSystemResources.forEach(element => {
+ const powerSystemResource = new PowerSystemResource();
+ powerSystemResource.cimId = element.mRID;
+ powerSystemResource.cimName = element.name;
+ powerSystemResource.cimDescription = element.description;
+ this.singleGMAffectedResourcesList.push(powerSystemResource);
+ });
+ } else {
+ const powerSystemResource = new PowerSystemResource();
+ powerSystemResource.cimId = anonymousPowerSystemResources.mRID;
+ powerSystemResource.cimName = anonymousPowerSystemResources.name;
+ powerSystemResource.cimDescription = anonymousPowerSystemResources.description;
+ this.singleGMAffectedResourcesList.push(powerSystemResource);
+ }
+
+ }
+ if (this.singleGMAffectedResourcesList.length === 0) {
+ // keine Daten "Objekt"
+ const powerSystemResource = new PowerSystemResource();
+ powerSystemResource.cimId = '-1';
+ powerSystemResource.cimName = 'keine Daten';
+ this.singleGMAffectedResourcesList.push(powerSystemResource);
+ }
+ this.tmpPowerSystemResource = this.singleGMAffectedResourcesList[0];
+ this.singleGridMeasure.powerSystemResource = JSON.parse(JSON.stringify(this.tmpPowerSystemResource));
+
+ this.singleGridMeasure.powerSystemResource.cimName = this.currSelectedResourceGroup + ' - ' + this.tmpPowerSystemResource.cimName;
+
+ /* tslint:enable */
+
+ }
+
+ processRessourceTypesResponse(res: string): void {
+ const jsonObj = this.convertXmlToJsonObj(res);
+ const PSRTypeList = jsonObj.ResponseMessage.Payload.PowerSystemResourceTypes.PSRType;
+ for (let index = 0; index < PSRTypeList.length; index++) {
+ this.singleGMAffectedResourcesGroupList.push(PSRTypeList[index].name);
+ }
+ }
+
+ isSingleGridmeasureLimitReached(): boolean {
+ if (this.gridMeasureDetail && this.gridMeasureDetail.listSingleGridmeasures) {
+ return this.gridMeasureDetail.listSingleGridmeasures.filter(singleGridMeasure =>
+ !singleGridMeasure.delete).length >= Globals.MAX_NUMBER_OF_TABS;
+ } else {
+ return false;
+ }
+ }
+
+ isLastSingleGridmeasure(): boolean {
+ if (this.gridMeasureDetail && this.gridMeasureDetail.listSingleGridmeasures) {
+ return this.gridMeasureDetail.listSingleGridmeasures.filter(singleGridMeasure => !singleGridMeasure.delete).length <= 1;
+ } else {
+ return true;
+ }
+ }
+
+
+ getPreviousSingleGridmeasure(): SingleGridMeasure {
+ const previousSingleGridmeasures: SingleGridMeasure[] = this.gridMeasureDetail.listSingleGridmeasures.filter(singleGridMeasure => {
+ return !singleGridMeasure.delete && singleGridMeasure.sortorder < this.singleGridMeasure.sortorder;
+ });
+
+ return previousSingleGridmeasures
+ && previousSingleGridmeasures.length > 0 ?
+ previousSingleGridmeasures[previousSingleGridmeasures.length - 1] : null;
+ }
+}
diff --git a/src/app/pages/step/step.component.css b/src/app/pages/step/step.component.css
new file mode 100644
index 0000000..865f13a
--- /dev/null
+++ b/src/app/pages/step/step.component.css
@@ -0,0 +1,33 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+@import url("../grid-measure-detail/grid-measure-detail.component.css");
+#Step-head {
+ display: inline-block
+}
+
+#addStepGlIcon {
+ background: transparent;
+ border: none;
+ color: white;
+ font-size: 107%;
+}
+
+#addStepGlIcon:focus {
+ outline: 0 !important;
+}
+
+#treeTable {
+ max-height: 200px;
+ overflow-y: auto;
+ overflow-x: auto;
+}
\ No newline at end of file
diff --git a/src/app/pages/step/step.component.html b/src/app/pages/step/step.component.html
new file mode 100644
index 0000000..c8f792b
--- /dev/null
+++ b/src/app/pages/step/step.component.html
@@ -0,0 +1,102 @@
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+-->
+<div #stepFormContainer>
+ <form #stepForm="ngForm" (change)="onStepFormValidation(stepForm.form.valid)">
+ <fieldset class="form-group" disabled="{{ readOnlyForm ? 'disabled' : ''}}">
+
+ <div class="row">
+ <div class="col-md-12">
+ <label class="form-field-label" for="stepAffectedResourcesGroupList">Betriebsmittel-Gruppe</label>
+ </div>
+ <div class="col-md-12">
+ <select type="text" (change)="onChangeResourceGroup($event.target.value)" name="stepAffectedResourcesGroupList" id="stepAffectedResourcesGroupList"
+ class="form-control">
+ <option *ngFor="let stepAffectedResourcesGroupListString of stepAffectedResourcesGroupList" value="{{stepAffectedResourcesGroupListString}}">{{stepAffectedResourcesGroupListString}}</option>
+ </select>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-12">
+ <label class="form-field-label" for="stepAffectedResources">Betriebsmittel</label>
+ </div>
+ </div>
+ <div class="row" id="treeTable">
+ <div class="col-md-12" *ngIf="!isTreeAvailable">Noch keine Betriebsmittel-Gruppe ausgewählt</div>
+ <div *ngIf="isTreeAvailable">
+ <tree [tree]="tree" #treeComponent (loadNextLevel)="handleNextLevel($event)" (nodeSelected)="handleSelected($event)" [settings]="{rootIsVisible: false}"></tree>
+ </div>
+ </div>
+ </fieldset>
+ <fieldset class="form-group" disabled="{{ readOnlyForm ? 'disabled' : ''}}">
+ <div class="row">
+ <div class="col-md-12">
+ <label class="form-field-label" for="switchingObject">Objekt der Schaltung</label>
+ </div>
+ <div class="col-md-12">
+ <input maxlength="256" type="text" list="" [required]="true" name="stepSwitchingObject" id="stepSwitchingObject" class="form-control"
+ [(ngModel)]="step.switchingObject" />
+ <datalist id="switchingObjectList">
+ <option></option>
+ </datalist>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-6">
+ <label class="form-field-label" for="presentStateControl">Ist-Zustand</label>
+ </div>
+ <div class="col-md-6">
+ <label class="form-field-label" for="targetStateControl">Soll-Zustand</label>
+ </div>
+ <div class="col-md-6">
+ <input autofocus maxlength="256" type="text" [required]="true" name="stepPresentState" id="stepPresentState" class="form-control"
+ [(ngModel)]="step.presentState" />
+ </div>
+ <div class="col-md-6">
+ <input autofocus maxlength="256" type="text" [required]="true" name="stepTargetState" id="stepTargetState" class="form-control"
+ [(ngModel)]="step.targetState" />
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-6">
+ <label class="form-field-label" for="presentTimeControl">Ist-Zeit</label>
+ </div>
+ <div class="col-md-6">
+ <label class="form-field-label" for="typeControl">Typ</label>
+ </div>
+ <div class="col-md-6">
+ <input autofocus maxlength="256" type="text" [required]="true" name="stepPresentTime" id="stepPresentTime" class="form-control"
+ [(ngModel)]="step.presentTime" />
+ </div>
+ <div class="col-md-6">
+ <input autofocus maxlength="256" type="text" [required]="true" name="stepType" id="stepType" class="form-control" [(ngModel)]="step.type"
+ />
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-12">
+ <label class="form-field-label" for="stepOperator">Ausführender</label>
+ </div>
+ <div class="col-md-12">
+ <input autofocus maxlength="256" type="text" [required]="true" name="stepOperator" id="stepOperator" class="form-control"
+ [(ngModel)]="step.operator" />
+ </div>
+ </div>
+ </fieldset>
+ <div class="row">
+ <div class="col-md-12" style="justify-content: flex-end">
+ <button class="btn btn-success" id="addStepBtn" (click)="processAddStep()" [disabled]="storageInProgress">Schritt hinzufügen
+ <span class="glyphicon glyphicon-plus" id="addStepGlIcon"></span>
+ </button>
+ </div>
+ </div>
+ </form>
+</div>
\ No newline at end of file
diff --git a/src/app/pages/step/step.component.spec.ts b/src/app/pages/step/step.component.spec.ts
new file mode 100644
index 0000000..0580e7e
--- /dev/null
+++ b/src/app/pages/step/step.component.spec.ts
@@ -0,0 +1,307 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { ComponentFixture, TestBed, async, fakeAsync, tick } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { SimpleChange } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { AbstractMockObservableService } from '../../testing/abstract-mock-observable.service';
+import { MockComponent } from '../../testing/mock.component';
+import { ActivatedRouteStub } from '../../testing/router-stubs';
+import { Globals } from './../../common/globals';
+import { StepComponent } from './step.component';
+import { SessionContext } from './../../common/session-context';
+import { CimCacheService } from './../../services/cim-cache.service';
+import { GridMeasureService } from '../../services/grid-measure.service';
+import { USERS } from '../../test-data/users';
+import { Step } from '../../model/step';
+import { GRIDMEASURE } from '../../test-data/grid-measures';
+import {
+ RESSOURCEWITHTYPERESPONSEEMPTYLIST,
+ RESSOURCETYPESRESPONSE, RESSOURCEWITHTYPERESPONSE, RESSOURCEWITHTYPERESPONSE_2
+} from '../../test-data/cim-cache-responses';
+import { TREE_STRUCTURE } from '../../test-data/tree';
+import { POWERSYSTEMRESOURCES } from '../../test-data/power-system-resources';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+import { MessageService } from 'primeng/api';
+
+class FakeRouter {
+ navigate(commands: any[]) {
+ return commands[0];
+ }
+}
+
+describe('StepComponent', () => {
+ let component: StepComponent;
+ let fixture: ComponentFixture<StepComponent>;
+ let routerStub: FakeRouter;
+ let activatedStub: ActivatedRouteStub;
+ let sessionContext: SessionContext;
+
+ let mockCimCacheService;
+ let mockGridmeasureService;
+ let toasterMessageService: ToasterMessageService;
+ let messageService: MessageService;
+
+
+ routerStub = {
+ navigate: jasmine.createSpy('navigate').and.callThrough()
+ };
+
+ class MockCimCacheService extends AbstractMockObservableService {
+ getRessourceTypes() {
+ return this;
+ }
+
+ getRessourcesWithType() {
+ return this;
+ }
+
+ }
+
+ class MockGridmeasureService extends AbstractMockObservableService {
+ storeGridMeasure() {
+ return this;
+ }
+ getGridMeasure(id: number) {
+ return this;
+ }
+ }
+
+ beforeEach(async(() => {
+ messageService = new MessageService;
+ activatedStub = new ActivatedRouteStub();
+ mockCimCacheService = new MockCimCacheService();
+ sessionContext = new SessionContext();
+ toasterMessageService = new ToasterMessageService(sessionContext, messageService);
+ mockGridmeasureService = new MockGridmeasureService();
+
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule
+ ],
+ declarations: [
+ StepComponent,
+ MockComponent({ selector: 'input', inputs: ['options'] }),
+ MockComponent({ selector: 'tree', inputs: ['tree', 'settings'] })
+ ],
+ providers: [
+ { provide: ActivatedRoute, useValue: activatedStub },
+ { provide: Router, useValue: routerStub },
+ { provide: SessionContext, useValue: sessionContext },
+ { provide: ToasterMessageService, useValue: toasterMessageService },
+ { provide: CimCacheService, useValue: mockCimCacheService },
+ { provide: GridMeasureService, useValue: mockGridmeasureService }
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(StepComponent);
+ component = fixture.componentInstance;
+ sessionContext.setCurrUser(USERS[0]);
+ sessionContext.setAllUsers(USERS);
+ component.isReadOnlyForm = false;
+ component.dateTimePattern =
+ '^(([0-2]?[0-9]|3[0-1])\.([0]?[1-9]|1[0-2])\.[1-2][0-9]{3}) (20|21|22|23|[0-1]?[0-9]{1}):([0-5]?[0-9]{1})$';
+ component.dateFormatLocale = 'dd.MM.yyyy HH:mm';
+ component.isCollapsible = true;
+ component.singleGridMeasure = GRIDMEASURE[0].listSingleGridmeasures[0];
+ activatedStub.testParams = { id: 555, mode: Globals.MODE.EDIT };
+ component.ngOnInit();
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should set form in readonly mode', () => {
+ component.readOnlyForm = false;
+ fixture.detectChanges();
+ component.ngOnChanges({
+ isReadOnlyForm: new SimpleChange(component.readOnlyForm, true, true)
+ });
+ fixture.detectChanges();
+ expect(component.readOnlyForm).toBeTruthy();
+ });
+
+ it('should handle cim-service error on init', fakeAsync(() => {
+ spyOn(console, 'log').and.callThrough();
+ component.singleGridMeasure = GRIDMEASURE[0].listSingleGridmeasures[0];
+ mockCimCacheService.error = 'CimCache error';
+ mockCimCacheService.content = [];
+
+ component.ngOnInit();
+ tick();
+
+ fixture.detectChanges();
+ expect(console.log).toHaveBeenCalled();
+ }));
+
+ xit('should handle cim-service error on onChangeResourceGroup', fakeAsync(() => {
+ spyOn(console, 'log').and.callThrough();
+ component.singleGridMeasure = GRIDMEASURE[0].listSingleGridmeasures[0];
+ mockCimCacheService.error = 'CimCache error';
+ mockCimCacheService.content = [];
+
+ component.onChangeResourceGroup('fake');
+ tick();
+
+ fixture.detectChanges();
+ expect(console.log).toHaveBeenCalled();
+ }));
+
+ it('should call getRessourceTypes on init', fakeAsync(() => {
+
+ spyOn((component as any), 'getRessourceTypes').and.callThrough();
+ spyOn((component as any), 'processRessourceTypesResponse').and.callThrough();
+ spyOn((component as any), 'getRessourceTypesWithType').and.callThrough();
+ (component as any).cimCacheService.content = RESSOURCETYPESRESPONSE;
+ component.singleGridMeasure = GRIDMEASURE[0].listSingleGridmeasures[0];
+
+ component.ngOnInit();
+ tick();
+
+ fixture.detectChanges();
+ expect((component as any).getRessourceTypes).toHaveBeenCalled();
+
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect((component as any).processRessourceTypesResponse).toHaveBeenCalled();
+
+ component.onChangeResourceGroup(null);
+ fixture.detectChanges();
+ expect(component.stepAffectedResourcesList.length).toBe(0);
+
+ component.onChangeResourceGroup('ac-line-segment');
+ fixture.detectChanges();
+ expect((component as any).getRessourceTypesWithType).toHaveBeenCalled();
+ });
+ }));
+
+ it('should convert xmlstring to json correctly', () => {
+ const xmlstring = `<root>
+ <child><textNode>First & Child</textNode></child>
+ <child><textNode>Second Child</textNode></child>
+ <testAttrs attr1='attr1Value'/>
+ </root>`;
+ const jsonstring =
+ `{"root":{"child":[{"textNode":"First & Child"},{"textNode":"Second Child"}],"testAttrs":{"_attr1":"attr1Value"}}}`;
+ expect(JSON.stringify(component.convertXmlToJsonObj(xmlstring))).toBe(jsonstring);
+ });
+
+ it('should processRessourceWithTypeResponse with empty result correctly', () => {
+ const res_input = RESSOURCEWITHTYPERESPONSEEMPTYLIST;
+ component.stepAffectedResourcesList = [];
+ component.tmpPowerSystemResource = null;
+ const tmp = component.processRessourceWithTypeResponse(res_input);
+ fixture.detectChanges();
+ expect(tmp[0].cimName).toBe('keine Daten');
+ });
+
+ it('should processRessourceWithTypeResponse correctly', () => {
+ const res_input = RESSOURCEWITHTYPERESPONSE;
+
+ component.tmpPowerSystemResource = null;
+ const tmp = component.processRessourceWithTypeResponse(res_input);
+ fixture.detectChanges();
+ expect(tmp[0].cimName).toBe('PowerTransformer');
+ });
+
+ it('should processRessourceWithTypeResponse with array type correctly', () => {
+ const res_input = RESSOURCEWITHTYPERESPONSE_2;
+
+ component.stepAffectedResourcesList = [];
+ const tmp = component.processRessourceWithTypeResponse(res_input);
+ fixture.detectChanges();
+ expect(tmp.length).toBe(2);
+ });
+
+ it('should processRessourceTypesResponse correctly', () => {
+ const res_input = RESSOURCETYPESRESPONSE;
+ component.stepAffectedResourcesGroupList = [];
+ component.processRessourceTypesResponse(res_input);
+ fixture.detectChanges();
+ expect(component.stepAffectedResourcesGroupList.length).toBe(21);
+ });
+
+ it('should not be in able to add step if not all fileds are completed', () => {
+ component.singleGridMeasure.listSteps = [];
+ component.step = new Step();
+ component.step.singleGridmeasureId = 3;
+ component.step.switchingObject = 'Regler 123';
+ component.step.targetState = '42';
+ component.processAddStep();
+ fixture.detectChanges();
+ expect(component.singleGridMeasure.listSteps.length).toBe(0);
+ });
+
+ it('should add step correctly', () => {
+ component.step = new Step();
+ component.step.singleGridmeasureId = 3;
+ component.step.switchingObject = 'Regler 123';
+ component.step.targetState = '42';
+ component.step.presentTime = '2.4.2018';
+ component.step.operator = 'otto';
+ component.step.type = 'supertype';
+ component.step.presentState = 'top';
+ const numberOflistSteps = component.singleGridMeasure.listSteps.length;
+ component.processAddStep();
+ fixture.detectChanges();
+ expect(component.singleGridMeasure.listSteps.length).toBe(numberOflistSteps + 1);
+ });
+
+ it('should emit warning message for empty step', async(() => {
+ spyOn(toasterMessageService, 'showWarn').and.callThrough();
+
+ component.step = new Step();
+ component.step.singleGridmeasureId = 3;
+ component.step.switchingObject = null;
+ component.step.targetState = null;
+
+ component.processAddStep();
+
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect(toasterMessageService.showWarn).toHaveBeenCalled();
+ });
+ }));
+
+ it('should handleSelected element from tree', () => {
+ const tree = JSON.parse(JSON.stringify(TREE_STRUCTURE));
+ component.handleSelected(tree);
+ fixture.detectChanges();
+ expect(component.step.switchingObject).toBe(tree.node.value + '');
+ });
+
+ it('should handleSelected element from tree', () => {
+ spyOn(component, 'handleNextLevel').and.callThrough();
+ const tree = JSON.parse(JSON.stringify(TREE_STRUCTURE));
+ component.handleNextLevel(tree);
+ fixture.detectChanges();
+ expect(component.handleNextLevel).toHaveBeenCalled();
+ });
+
+ xit('should prepareTreeModel and show the tree', async(() => {
+ spyOn(component, 'prepareTreeModel').and.returnValue(Promise.resolve);
+ const resources = JSON.parse(JSON.stringify(POWERSYSTEMRESOURCES));
+ fixture.detectChanges();
+ component.prepareTreeModel(resources, 1);
+
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect(component.isTreeAvailable).toBeTruthy();
+ });
+
+ }));
+
+});
diff --git a/src/app/pages/step/step.component.ts b/src/app/pages/step/step.component.ts
new file mode 100644
index 0000000..59173ae
--- /dev/null
+++ b/src/app/pages/step/step.component.ts
@@ -0,0 +1,311 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+******************************************************************************
+ */
+import {
+ Component, OnInit, Input, OnChanges, SimpleChanges, ViewChild, AfterViewChecked,
+ ElementRef, OnDestroy, ChangeDetectorRef, AfterViewInit
+} from '@angular/core';
+import { SingleGridMeasure } from '../../model/single-grid-measure';
+import { Step } from '../../model/step';
+import * as X2JS from '../../../assets/js/xml2json.min.js';
+import { ErrorType } from '../../common/enums';
+import { CimCacheService } from '../../services/cim-cache.service';
+import { PowerSystemResource } from './../../model/power-system-resource';
+import { FormGroup } from '@angular/forms';
+import { SessionContext } from './../../common/session-context';
+import { Subscription } from 'rxjs/Subscription';
+import { TreeModel, Tree } from 'ng2-tree';
+import { TreeModelImpl } from './../../model/TreeModelImpl';
+import { ToasterMessageService } from '../../services/toaster-message.service';
+
+@Component({
+ selector: 'app-step',
+ templateUrl: './step.component.html',
+ styleUrls: ['./step.component.css']
+})
+export class StepComponent implements OnInit, OnChanges, OnDestroy, AfterViewChecked, AfterViewInit {
+ @Input() isReadOnlyForm: boolean;
+ @Input() dateTimePattern: string;
+ @Input() dateFormatLocale: string;
+ @Input() isCollapsible = true;
+ @Input() singleGridMeasure: SingleGridMeasure;
+
+ form: HTMLFormElement;
+ readOnlyForm: boolean;
+ stepFormValid: boolean;
+ isStatusCollapsed = true;
+ storageInProgress: boolean;
+ step: Step = new Step();
+ currentPowerSystemResourceDB: PowerSystemResource;
+ stepAffectedResourcesString: PowerSystemResource;
+ stepAffectedResourcesGroupList = [''];
+ stepAffectedResourcesList: Array<PowerSystemResource> = [];
+ subscription: Subscription;
+ tmpPowerSystemResource: PowerSystemResource;
+ inactiveFields: Array<string> = [];
+ treeIdCounter = 1;
+ tree: TreeModel = { value: '' };
+ isTreeAvailable = false;
+
+ @ViewChild('treeComponent') treeComponent;
+ @ViewChild('stepFormContainer') stepFormCotainer: ElementRef;
+ @ViewChild('stepForm') stepForm: FormGroup;
+
+ constructor(private cimCacheService: CimCacheService,
+ private sessionContext: SessionContext,
+ private toasterMessageService: ToasterMessageService) { }
+
+ ngOnInit() {
+ this.inactiveFields = this.sessionContext.getInactiveFields();
+ this.getRessourceTypes();
+ this.currentPowerSystemResourceDB = this.singleGridMeasure.powerSystemResource;
+ }
+
+ ngAfterViewInit() {
+ this.initInactiveFields();
+ }
+
+ ngAfterViewChecked() {
+ if (this.stepForm) {
+ this.step._isValide = this.stepForm.valid;
+ }
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+
+ if (changes['isReadOnlyForm']) {
+ this.readOnlyForm = changes['isReadOnlyForm'].currentValue;
+ this.initInactiveFields();
+ }
+ }
+
+ public initInactiveFields() {
+ const el: HTMLElement = this.stepFormCotainer.nativeElement as HTMLElement;
+ const fields = el.querySelectorAll('*[id]');
+ for (let index = 0; index < fields.length; index++) {
+ const field = fields[index];
+ if (this.readOnlyForm || this.isFieldInactive(field['id'])) {
+ field.setAttribute('disabled', 'disabled');
+ } else {
+ field.removeAttribute('disabled');
+ }
+ }
+ }
+ private isFieldInactive(fieldName: string): boolean {
+ return this.inactiveFields.filter(field => field === fieldName).length > 0;
+ }
+
+ ngOnDestroy() {
+ }
+
+ public onUseRessourceBtnClick(selectedVal: any) {
+ if (selectedVal) {
+ this.step.switchingObject = selectedVal;
+ }
+ }
+
+ public processAddStep() {
+
+ if (!this.isStepEmpty()) {
+
+ const stepDeepCopy = JSON.parse(JSON.stringify(this.step));
+ stepDeepCopy.id = -1;
+
+ this.storageInProgress = true;
+ if (!this.singleGridMeasure.listSteps || this.singleGridMeasure.listSteps.length === 0) {
+ this.singleGridMeasure.listSteps = new Array();
+ stepDeepCopy.sortorder = 1;
+ } else {
+ // stepDeepCopy.sortorder = this.singleGridMeasure.listSteps.length + 1;
+ stepDeepCopy.sortorder = this.singleGridMeasure.listSteps[this.singleGridMeasure.listSteps.length - 1].sortorder + 1;
+ }
+
+ /* A simple push on liststeps (this.singleGridMeasure.listSteps.push(stepDeepCopy)) doesn't
+ trigger Angular to refresh the view-model. This is why you have to use the following way
+ which creates a "new" array (copy of the old) and appends it. */
+ this.singleGridMeasure.listSteps = [...this.singleGridMeasure.listSteps, stepDeepCopy];
+
+ this.storageInProgress = false;
+ } else {
+ this.toasterMessageService.showWarn('Bitte alle Felder in Schrittsequenz aufüllen!');
+ }
+
+ }
+
+ onStepFormValidation(valid: boolean) {
+ this.step._isValide = valid;
+ }
+
+ isStepEmpty() {
+ if (!this.step.switchingObject || !this.step.targetState || !this.step.presentState
+ || !this.step.presentTime || !this.step.type || !this.step.operator) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ onChangeResourceGroup(val) {
+ if (val) {
+ this.stepAffectedResourcesList = [];
+ this.getRessourceTypesWithType(val);
+ } else {
+ this.stepAffectedResourcesList = [];
+ this.isTreeAvailable = false;
+ }
+ }
+
+
+ private getRessourceTypes() {
+ this.cimCacheService.getRessourceTypes().subscribe(res => {
+ this.processRessourceTypesResponse(res);
+ },
+ error => {
+ this.toasterMessageService.showError(ErrorType.retrieve, 'CimCache');
+ console.log(error);
+ });
+ }
+
+ private async getRessourceTypesWithType(value: string) {
+ const newTreeModel: TreeModel = new TreeModelImpl();
+
+ const xmlStringRes = await this.cimCacheService.getRessourcesWithType(value).toPromise().catch(error => {
+ this.toasterMessageService.showError(ErrorType.retrieve, 'CimCache');
+ });
+
+ this.stepAffectedResourcesList = this.processRessourceWithTypeResponse(xmlStringRes);
+ this.tmpPowerSystemResource = this.stepAffectedResourcesList[0];
+ this.treeIdCounter = 1;
+ const newTreeChilds = await this.prepareTreeModel(this.stepAffectedResourcesList);
+ newTreeModel.children = newTreeChilds;
+ this.tree = newTreeModel;
+ }
+
+ convertXmlToJsonObj(xml: string) {
+ return new X2JS().xml_str2json(xml);
+ }
+
+
+ processRessourceWithTypeResponse(res: string): Array<PowerSystemResource> {
+ if (!res) {
+ return;
+ }
+ const jsonObj = this.convertXmlToJsonObj(res);
+ const powerSystemResources = jsonObj.ResponseMessage.Payload.PowerSystemResources;
+
+ const powerSystemResourceRetList = new Array<PowerSystemResource>();
+
+ /* tslint:disable */
+ for (const prop in powerSystemResources) {
+ const anonymousPowerSystemResources = powerSystemResources[prop];
+
+ if (Array.isArray(anonymousPowerSystemResources)) {
+ anonymousPowerSystemResources.forEach(element => {
+ const powerSystemResource = new PowerSystemResource();
+ powerSystemResource.cimId = element.mRID;
+ powerSystemResource.cimName = element.name;
+ powerSystemResource.cimDescription = element.description;
+ powerSystemResourceRetList.push(powerSystemResource);
+ });
+ } else {
+ const powerSystemResource = new PowerSystemResource();
+ powerSystemResource.cimId = anonymousPowerSystemResources.mRID;
+ powerSystemResource.cimName = anonymousPowerSystemResources.name;
+ powerSystemResource.cimDescription = anonymousPowerSystemResources.description;
+ powerSystemResourceRetList.push(powerSystemResource);
+ }
+ }
+ if (powerSystemResourceRetList.length === 0) {
+ // keine Daten "Objekt"
+ const powerSystemResource = new PowerSystemResource();
+ powerSystemResource.cimId = '-1';
+ powerSystemResource.cimName = 'keine Daten';
+ powerSystemResourceRetList.push(powerSystemResource);
+ }
+
+ /* tslint:enable */
+
+ return powerSystemResourceRetList;
+
+ }
+
+ handleSelected(tree: Tree) {
+ this.step.switchingObject = tree.node.value + '';
+ }
+
+ async handleNextLevel(tree: Tree) {
+ const currentNodeId = tree.node.id;
+ const oopNodeController = this.treeComponent.getControllerByNodeId(tree.node.id);
+ const nodeIdNr = +currentNodeId;
+
+ // Für die richtige Anbindung auskommentieren bzw. Webservice für die "Objekt der Schaltung" implementieren
+ // Fehlt jdoch noch im CIM-Cache
+ /* const value = tree.node.value + '';
+ const xmlStringRes = await this.cimCacheService.getRessourcesWithType(value).toPromise().catch(error => {
+ this.messageService.emitError('CimCache', ErrorType.retrieve);
+ }); */
+
+ // ...deswegen Fake Childs atm fest auf 'ac-line-segment'
+ const xmlStringRes = await this.cimCacheService.getRessourcesWithType('ac-line-segment').toPromise().catch(error => {
+ this.toasterMessageService.showError(ErrorType.retrieve, 'CimCache');
+ });
+
+ const newTreeChildren = await this.prepareTreeModel(this.processRessourceWithTypeResponse(xmlStringRes), nodeIdNr);
+
+ oopNodeController.setChildren(newTreeChildren);
+ }
+
+ async prepareTreeModel(nodes: Array<PowerSystemResource>, treeId?: number) {
+ const newChildsLvl1: TreeModel[] = [];
+
+ // richtige Variante jedoch noch nicht im CIM-Cache verfügbar
+ // const promises = nodes.map((pwrElementLvl1) => this.cimCacheService.getRessourcesWithType(pwrElementLvl1.cimName).toPromise());
+
+ // Fake Childs
+ const promises = nodes.map((pwrElementLvl1) => this.cimCacheService.getRessourcesWithType('substation-type').toPromise());
+ const results = await Promise.all(promises);
+
+ for (let index = 0; index < results.length; index++) {
+ const newChildsLvl2: TreeModel[] = [];
+ const asyncResult = results[index];
+ const pwrElementLvl1 = nodes[index];
+ const powerSystemResourceListLvl2 = this.processRessourceWithTypeResponse(asyncResult);
+
+ if (powerSystemResourceListLvl2 && powerSystemResourceListLvl2.length > 0 && powerSystemResourceListLvl2[0].cimId !== '-1') {
+ // Childs vorhanden
+ newChildsLvl1.push({
+ emitLoadNextLevel: true, id: this.treeIdCounter++, value: pwrElementLvl1.cimName,
+ valueObject: pwrElementLvl1, children: newChildsLvl2
+ });
+ this.isTreeAvailable = true;
+ } else {
+ // keine Childs vorhanden
+ newChildsLvl1.push({
+ emitLoadNextLevel: false, id: this.treeIdCounter++, value: pwrElementLvl1.cimName,
+ valueObject: pwrElementLvl1
+ });
+ this.isTreeAvailable = false;
+ }
+
+ }
+
+
+ return newChildsLvl1;
+ }
+
+ processRessourceTypesResponse(res: string): void {
+ const jsonObj = this.convertXmlToJsonObj(res);
+ const PSRTypeList = jsonObj.ResponseMessage.Payload.PowerSystemResourceTypes.PSRType;
+ for (let index = 0; index < PSRTypeList.length; index++) {
+ this.stepAffectedResourcesGroupList.push(PSRTypeList[index].name);
+ }
+ }
+
+}
diff --git a/src/app/services/auth-guard.service.spec.ts b/src/app/services/auth-guard.service.spec.ts
new file mode 100644
index 0000000..4fcda72
--- /dev/null
+++ b/src/app/services/auth-guard.service.spec.ts
@@ -0,0 +1,57 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { TestBed, async, inject } from '@angular/core/testing';
+import { Router } from '@angular/router';
+import { AuthGuard } from './auth-guard.service';
+import { SessionContext } from '../common/session-context';
+
+class FakeRouter {
+ navigate(commands: any[]) {
+ return commands[0];
+ }
+}
+
+describe('AuthGuard', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ AuthGuard,
+ SessionContext,
+ { provide: Router, useClass: FakeRouter }
+ ]
+ })
+ .compileComponents();
+ });
+
+ it('should instantiate the service', inject([AuthGuard], (authGuard: AuthGuard) => {
+ expect(authGuard instanceof AuthGuard).toBe(true);
+ }));
+
+ it('should allow activating route if user in session exists',
+ inject([AuthGuard, SessionContext], (authGuard: AuthGuard, sessionContext: SessionContext) => {
+ spyOn(sessionContext, 'getCurrUser').and.returnValue('dummy');
+
+ expect(authGuard.canActivate()).toBe(true);
+ })
+ );
+
+ it('should redirect to loggedout page if no user in session exists',
+ inject([AuthGuard, SessionContext, Router], (authGuard: AuthGuard, sessionContext: SessionContext, router: Router) => {
+ spyOn(sessionContext, 'getCurrUser').and.returnValue(null);
+ spyOn(router, 'navigate');
+
+ expect(authGuard.canActivate()).toBe(false);
+ expect(router.navigate).toHaveBeenCalledWith(['/loggedout']);
+ })
+ );
+
+});
diff --git a/src/app/services/auth-guard.service.ts b/src/app/services/auth-guard.service.ts
new file mode 100644
index 0000000..57064b1
--- /dev/null
+++ b/src/app/services/auth-guard.service.ts
@@ -0,0 +1,30 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Injectable } from '@angular/core';
+import { CanActivate, Router } from '@angular/router';
+import { SessionContext } from '../common/session-context';
+
+@Injectable()
+export class AuthGuard implements CanActivate {
+ constructor(
+ private router: Router,
+ private sessionContext: SessionContext) {}
+
+ public canActivate(): boolean {
+ if (this.sessionContext.getCurrUser()) {
+ return true;
+ } else {
+ this.router.navigate(['/loggedout']);
+ return false;
+ }
+ }
+}
diff --git a/src/app/services/authentication.service.spec.ts b/src/app/services/authentication.service.spec.ts
new file mode 100644
index 0000000..d1fa9aa
--- /dev/null
+++ b/src/app/services/authentication.service.spec.ts
@@ -0,0 +1,56 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { HttpClientModule, HttpXhrBackend } from '@angular/common/http';
+import { TestBed, async, inject } from '@angular/core/testing';
+import { MockBackend } from '@angular/http/testing';
+import 'rxjs/add/observable/of';
+import 'rxjs/add/operator/catch';
+import 'rxjs/add/operator/do';
+import 'rxjs/add/operator/toPromise';
+import { SessionContext } from '../common/session-context';
+import { BaseHttpService, HttpMethodEn } from '../services/base-http.service';
+import { AuthenticationService } from './authentication.service';
+import { MockBaseHttpService } from '../testing/mock-base-http.service';
+
+describe('Http-AuthenticationService (mockBackend)', () => {
+ let sessionContext: SessionContext;
+ let mockedBaseHttpService: MockBaseHttpService;
+
+ beforeEach(() => {
+ sessionContext = new SessionContext();
+ mockedBaseHttpService = new MockBaseHttpService();
+
+ TestBed.configureTestingModule({
+ imports: [HttpClientModule],
+ providers: [
+ AuthenticationService,
+ { provide: SessionContext, useValue: sessionContext },
+ { provide: BaseHttpService, useValue: mockedBaseHttpService }
+ ]
+ })
+ .compileComponents();
+ });
+
+ it('can instantiate service when inject service',
+ inject([AuthenticationService], (service: AuthenticationService) => {
+ expect(service instanceof AuthenticationService).toBe(true);
+ }));
+
+
+ it('should call logout', inject([AuthenticationService], (service: AuthenticationService) => {
+ expect(service).toBeTruthy();
+
+ service.logout();
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('logout');
+ }));
+});
diff --git a/src/app/services/authentication.service.ts b/src/app/services/authentication.service.ts
new file mode 100644
index 0000000..07ab805
--- /dev/null
+++ b/src/app/services/authentication.service.ts
@@ -0,0 +1,29 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import { SessionContext } from '../common/session-context';
+import { BaseHttpService, HttpCallInfo, HttpMethodEn } from './base-http.service';
+import { Globals } from '../common/globals';
+
+@Injectable()
+export class AuthenticationService {
+
+ constructor(
+ private baseHttpService: BaseHttpService,
+ private sessionContext: SessionContext) { }
+
+ public logout(): Observable<any> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/logout', null), this.sessionContext);
+ }
+}
diff --git a/src/app/services/backend-settings.service.spec.ts b/src/app/services/backend-settings.service.spec.ts
new file mode 100644
index 0000000..4a52d09
--- /dev/null
+++ b/src/app/services/backend-settings.service.spec.ts
@@ -0,0 +1,46 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+******************************************************************************
+*/
+import { TestBed, inject } from '@angular/core/testing';
+
+import { BackendSettingsService } from './backend-settings.service';
+import { SessionContext } from '../common/session-context';
+import { MockBaseHttpService } from '../testing/mock-base-http.service';
+import { BaseHttpService, HttpMethodEn } from './base-http.service';
+
+describe('BackendSettingsService', () => {
+ let sessionContext: SessionContext;
+ let mockedBaseHttpService: MockBaseHttpService;
+
+ beforeEach(() => {
+ sessionContext = new SessionContext();
+ mockedBaseHttpService = new MockBaseHttpService();
+
+ TestBed.configureTestingModule({
+ providers: [BackendSettingsService,
+ { provide: SessionContext, useValue: sessionContext },
+ { provide: BaseHttpService, useValue: mockedBaseHttpService },
+ SessionContext]
+ });
+ });
+
+ it('should be created', inject([BackendSettingsService], (service: BackendSettingsService) => {
+ expect(service).toBeTruthy();
+ }));
+
+ it('should call getBackendSettings', inject([BackendSettingsService], (service: BackendSettingsService) => {
+ service.getBackendSettings();
+
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toBe('/getBackendSettings');
+
+ }));
+
+});
diff --git a/src/app/services/backend-settings.service.ts b/src/app/services/backend-settings.service.ts
new file mode 100644
index 0000000..adc72a9
--- /dev/null
+++ b/src/app/services/backend-settings.service.ts
@@ -0,0 +1,32 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Injectable } from '@angular/core';
+import { SessionContext } from '../common/session-context';
+import { BaseHttpService, HttpMethodEn, HttpCallInfo } from './base-http.service';
+import { Observable } from 'rxjs/Observable';
+import { Globals } from '../common/globals';
+import { BackendSettings } from '../model/backend-settings';
+
+@Injectable()
+export class BackendSettingsService {
+
+ constructor(
+ private baseHttpService: BaseHttpService,
+ private sessionContext: SessionContext) { }
+
+
+ getBackendSettings(): Observable<BackendSettings> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/getBackendSettings', null),
+ this.sessionContext);
+ }
+}
diff --git a/src/app/services/base-data.service.spec.ts b/src/app/services/base-data.service.spec.ts
new file mode 100644
index 0000000..cf03252
--- /dev/null
+++ b/src/app/services/base-data.service.spec.ts
@@ -0,0 +1,117 @@
+/*
+ ******************************************************************************
+ * Copyright © 2018 PTA GmbH.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ *
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ ******************************************************************************
+ */
+import { TestBed, inject } from '@angular/core/testing';
+import { BaseHttpService, HttpMethodEn } from './base-http.service';
+import { BaseDataService } from './base-data.service';
+import { MockBaseHttpService } from '../testing/mock-base-http.service';
+import { SessionContext } from '../common/session-context';
+
+describe('Service: BaseData', () => {
+ let sessionContext: SessionContext;
+ let mockedBaseHttpService: MockBaseHttpService;
+
+ beforeEach(() => {
+ sessionContext = new SessionContext();
+ mockedBaseHttpService = new MockBaseHttpService();
+
+ TestBed.configureTestingModule({
+ providers: [
+ BaseDataService,
+ { provide: SessionContext, useValue: sessionContext },
+ { provide: BaseHttpService, useValue: mockedBaseHttpService },
+ SessionContext
+ ]
+ });
+ });
+
+ it('should call getResponsiblesOnSiteFromGridmeasures', inject([BaseDataService], (service: BaseDataService) => {
+ expect(service).toBeTruthy();
+
+ service.getResponsiblesOnSiteFromGridmeasures();
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('getResponsiblesOnSiteFromGridmeasures');
+
+ }));
+
+ it('should call getBranches', inject([BaseDataService], (service: BaseDataService) => {
+ expect(service).toBeTruthy();
+
+ service.getBranches();
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('getBranches');
+
+ }));
+
+ it('should call getBrancheLevels', inject([BaseDataService], (service: BaseDataService) => {
+ expect(service).toBeTruthy();
+
+ service.getBranchLevels(1);
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('getBranchLevels');
+
+ }));
+
+ it('should call getStatuses', inject([BaseDataService], (service: BaseDataService) => {
+ expect(service).toBeTruthy();
+
+ service.getStatuses();
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('getGmStatus');
+
+ }));
+
+ it('should call getCostCenters', inject([BaseDataService], (service: BaseDataService) => {
+ expect(service).toBeTruthy();
+
+ service.getCostCenters();
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('getCostCenters');
+
+ }));
+
+ it('should call getUserDepartment', inject([BaseDataService], (service: BaseDataService) => {
+ expect(service).toBeTruthy();
+
+ service.getUserDepartments();
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('getUserDepartments');
+
+ }));
+
+ it('should call getEmailAddressesFromTemplates', inject([BaseDataService], (service: BaseDataService) => {
+ expect(service).toBeTruthy();
+
+ service.getEmailAddressesFromTemplates();
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('getEmailAddressesFromTemplates');
+
+ }));
+
+ it('should call getEmailAddressesFromGridmeasures', inject([BaseDataService], (service: BaseDataService) => {
+ expect(service).toBeTruthy();
+
+ service.getEmailAddressesFromGridmeasures();
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('getMailAddressesFromGridmeasures');
+
+ }));
+
+ it('should call getTerritories', inject([BaseDataService], (service: BaseDataService) => {
+ expect(service).toBeTruthy();
+
+ service.getTerritories();
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('getTerritories');
+
+ }));
+});
+
diff --git a/src/app/services/base-data.service.ts b/src/app/services/base-data.service.ts
new file mode 100644
index 0000000..61491fa
--- /dev/null
+++ b/src/app/services/base-data.service.ts
@@ -0,0 +1,86 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import 'rxjs/add/observable/throw';
+import 'rxjs/add/operator/catch';
+import 'rxjs/add/operator/map';
+import { Globals } from '../common/globals';
+import { SessionContext } from '../common/session-context';
+import { Status } from '../model/status';
+import { UserDepartment } from '../model/user-department';
+import { Branch } from './../model/branch';
+import { CostCenter } from './../model/cost-center';
+import { BaseHttpService, HttpCallInfo, HttpMethodEn } from './base-http.service';
+import { BranchLevel } from '../model/branch-level';
+import { Territory } from '../model/territory';
+
+@Injectable()
+export class BaseDataService {
+
+ constructor(
+ private baseHttpService: BaseHttpService,
+ private sessionContext: SessionContext) {
+ }
+
+ public getNetworkControlsFromSingleGridmeasures(): Observable<string[]> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/getNetworkControlsFromSingleGridmeasures', null),
+ this.sessionContext);
+ }
+
+ public getResponsiblesOnSiteFromGridmeasures(): Observable<string[]> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/getResponsiblesOnSiteFromGridmeasures', null),
+ this.sessionContext);
+ }
+
+ public getBranches(): Observable<Branch[]> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/getBranches', null), this.sessionContext);
+ }
+ public getBranchLevels(branchId: number): Observable<BranchLevel[]> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/getBranchLevelsByBranch/' + branchId, null),
+ this.sessionContext);
+ }
+
+ public getStatuses(): Observable<Status[]> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/getGmStatus', null), this.sessionContext);
+ }
+ public getCostCenters(): Observable<CostCenter[]> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/getCostCenters', null), this.sessionContext);
+ }
+ public getUserDepartments(): Observable<UserDepartment[]> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/getUserDepartments', null), this.sessionContext);
+ }
+
+ public getEmailAddressesFromTemplates(): Observable<string[]> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/getEmailAddressesFromTemplates', null), this.sessionContext);
+ }
+
+ public getEmailAddressesFromGridmeasures(): Observable<string[]> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/getMailAddressesFromGridmeasures', null),
+ this.sessionContext);
+ }
+
+ public getTerritories(): Observable<Territory[]> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/getTerritories', null), this.sessionContext);
+ }
+}
+
diff --git a/src/app/services/base-http.service.spec.ts b/src/app/services/base-http.service.spec.ts
new file mode 100644
index 0000000..68ad795
--- /dev/null
+++ b/src/app/services/base-http.service.spec.ts
@@ -0,0 +1,310 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { TestBed, fakeAsync, inject, tick } from '@angular/core/testing';
+import { ErrorType } from '../common/enums';
+import { Globals } from '../common/globals';
+import { SessionContext } from '../common/session-context';
+import { MessageServiceCustom } from '../services/message.service';
+import { BaseHttpService, HttpCallInfo, HttpMethodEn } from './base-http.service';
+import { HttpClient, HttpClientModule, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
+
+class MockResponse {
+ status: number;
+ data: any;
+
+ json() {
+ return this.data;
+ }
+}
+
+describe('BaseHttpService', () => {
+ class BaseHttpServiceMock extends BaseHttpService {
+ public constructor(public messageService: MessageServiceCustom, public http: HttpClient) {
+ super(messageService, http);
+ }
+ public getBaseUrl(): string {
+ return super.getBaseUrl();
+ }
+ }
+ let sessionContext: SessionContext;
+
+ beforeEach(() => {
+ sessionContext = new SessionContext();
+ sessionContext.clearStorage();
+
+ TestBed.configureTestingModule({
+ imports: [HttpClientModule],
+ providers: [
+ BaseHttpService,
+ MessageServiceCustom,
+ { provide: SessionContext, useValue: sessionContext }
+ ]
+ });
+
+ });
+
+ it('should deliver the BaseUrl', () => {
+ const bhs = new BaseHttpServiceMock(this.messageService, null);
+ expect(bhs.getBaseUrl()).toBe(Globals.BASE_URL);
+ });
+
+ it('should provide a correct envelope with POST', inject([BaseHttpService], (baseservice: BaseHttpService) => {
+ const callInfo = new HttpCallInfo('ServiceName', HttpMethodEn.post, 'testMeTester', { data: 'blabla' });
+ const openService: any = baseservice;
+
+ const env = openService.buildEnvelope(callInfo, []);
+
+ expect(env.method).toBe(HttpMethodEn.post);
+ expect(env.serviceName).toBe('ServiceName');
+ expect(env.uriFragment).toBe('testMeTester');
+ expect(env.payload).toBeTruthy();
+ expect(env.headers.length).toBe(0);
+ }));
+ it('should provide a correct envelope with GET', inject([BaseHttpService], (baseservice: BaseHttpService) => {
+ const callInfo = new HttpCallInfo('ServiceName', HttpMethodEn.get, 'testMeTester', { data: 'blabla' });
+ const openService: any = baseservice;
+
+ const env = openService.buildEnvelope(callInfo, []);
+
+ expect(env.method).toBe(HttpMethodEn.get);
+ expect(env.serviceName).toBe('ServiceName');
+ expect(env.uriFragment).toBe('testMeTester');
+ expect(env.payload).toBeUndefined();
+ expect(env.headers.length).toBe(0);
+ }));
+
+ it('should map the headers correctly', inject([BaseHttpService], (baseservice: BaseHttpService) => {
+ const openService: any = baseservice;
+ sessionContext.setCurrSessionId('TOKKO');
+ const headers: HttpHeaders = openService.createCommonHeaders(sessionContext);
+
+ const micsEnvelopHeaders: any[] = openService.mapHeaders(headers);
+
+ expect(micsEnvelopHeaders.find(x => x.attribute === 'Accept').value).toBe('application/json');
+ expect(micsEnvelopHeaders.find(x => x.attribute === Globals.SESSION_TOKEN_TAG).value).toBe('TOKKO');
+ }));
+
+
+ it('should extract Data from a response correctly (302)', inject([BaseHttpService], fakeAsync((baseservice: BaseHttpService) => {
+ let msgWasFired = false;
+ const openService: any = baseservice;
+ const response = new MockResponse();
+ response.status = 302;
+ response.data = 'payload';
+
+ sessionContext.centralHttpResultCode$.subscribe(x => {
+ expect(x).toBe(302);
+ msgWasFired = true;
+ });
+
+ expect(openService.extractData(response, sessionContext).data).toBe('payload');
+ tick();
+
+ expect(msgWasFired).toBeTruthy();
+ })));
+
+ it('should extract Data from a response correctly (299)', inject([BaseHttpService], fakeAsync((baseservice: BaseHttpService) => {
+ let msgWasFired = false;
+ const openService: any = baseservice;
+ const response = new MockResponse();
+ response.status = 299;
+ response.data = 'payload';
+
+ sessionContext.centralHttpResultCode$.subscribe(x => {
+ expect(x).toBe(299);
+ msgWasFired = true;
+ });
+
+ expect(openService.extractData(response, sessionContext).data).toBe('payload');
+ tick();
+
+ expect(msgWasFired).toBeTruthy();
+ })));
+
+
+ it('should throw Exception on a response correctly (111)', inject([BaseHttpService], fakeAsync((baseservice: BaseHttpService) => {
+ let msgWasFired = false;
+ const openService: any = baseservice;
+ const response = new MockResponse();
+ response.status = 111;
+ response.data = 'payload';
+
+ sessionContext.centralHttpResultCode$.subscribe(x => {
+ expect(x).toBe(111);
+ msgWasFired = true;
+ });
+
+ expect(function () { openService.extractData(response, sessionContext); }).toThrowError();
+ tick();
+
+ expect(msgWasFired).toBeTruthy();
+ })));
+
+ it('should throw Exception on a response correctly (310)', inject([BaseHttpService], fakeAsync((baseservice: BaseHttpService) => {
+ let msgWasFired = false;
+ const openService: any = baseservice;
+ const response = new MockResponse();
+ response.status = 310;
+ response.data = 'payload';
+
+ sessionContext.centralHttpResultCode$.subscribe(x => {
+ expect(x).toBe(310);
+ msgWasFired = true;
+ });
+
+ expect(function () { openService.extractData(response, sessionContext); }).toThrowError();
+ tick();
+
+ expect(msgWasFired).toBeTruthy();
+ })));
+
+ it('should throw Exception on a null- response ', inject([BaseHttpService], fakeAsync((baseservice: BaseHttpService) => {
+ let msgWasFired = false;
+ const openService: any = baseservice;
+
+ sessionContext.centralHttpResultCode$.subscribe(x => {
+ expect(x).toBe(310);
+ msgWasFired = true;
+ });
+
+ expect(function () { openService.extractData(null, sessionContext); }).toThrowError();
+ tick();
+
+ expect(msgWasFired).toBeFalsy();
+ })));
+
+
+ it('should extract the session id correctly', inject([BaseHttpService], (baseservice: BaseHttpService) => {
+ const openService: any = baseservice;
+ sessionContext.setCurrSessionId('untouched');
+
+ openService.extractSessionId(null, sessionContext);
+ expect(sessionContext.getCurrSessionId()).toBe('untouched');
+
+ let headers = new HttpHeaders();
+
+ headers = headers.append('Accept', 'application/json');
+ headers = headers.append('content-Type', 'application/json');
+
+ openService.extractSessionId(headers, sessionContext);
+ expect(sessionContext.getCurrSessionId()).toBe('untouched');
+
+ headers = headers.append(Globals.SESSION_TOKEN_TAG, 'hit me');
+
+ openService.extractSessionId(headers, sessionContext);
+ expect(sessionContext.getCurrSessionId()).toBe('hit me');
+
+
+
+ }));
+
+ it('should handle an text error promise correctly', inject([BaseHttpService], fakeAsync((baseservice: BaseHttpService) => {
+ let msgWasFired = false;
+ const openService: any = baseservice;
+ const response = new MockResponse();
+ response.status = 401;
+ response.data = 'payload';
+
+ openService.handleErrorPromise('texterror').subscribe(x => { },
+ e => {
+ expect(e).toBe('texterror');
+ msgWasFired = true;
+ });
+
+ tick();
+
+ expect(msgWasFired).toBeTruthy();
+
+
+ })));
+
+ it('should handle an 401 error promise correctly', inject([BaseHttpService], fakeAsync((baseservice: BaseHttpService) => {
+ let msgWasFired = false;
+ const openService: any = baseservice;
+ const response = new HttpErrorResponse({
+ status: 401, statusText: 'Bad Request'
+ });
+
+ openService.handleErrorPromise(response).subscribe(x => { },
+ e => {
+ expect(e).toBe(ErrorType.authentication);
+ msgWasFired = true;
+ });
+
+ tick();
+
+ expect(msgWasFired).toBeTruthy();
+ })));
+
+ it('should handle an 500 error promise correctly', inject([BaseHttpService], fakeAsync((baseservice: BaseHttpService) => {
+ let msgWasFired = false;
+ const openService: any = baseservice;
+ const response = new HttpErrorResponse({ status: 500 });
+
+
+ openService.handleErrorPromise(response).subscribe(x => { },
+ e => {
+ expect(e).toContain('500');
+ msgWasFired = true;
+ });
+
+ tick();
+
+ expect(msgWasFired).toBeTruthy();
+
+
+ })));
+
+ it('should handle an error with body promise correctly', inject([BaseHttpService], fakeAsync((baseservice: BaseHttpService) => {
+ let msgWasFired = false;
+ const openService: any = baseservice;
+ const response = new HttpErrorResponse({
+ status: 405, statusText: 'Method Not Allowed'
+ });
+
+ openService.handleErrorPromise(response).subscribe(x => { },
+ e => {
+ expect(e).toContain('405 - Method Not Allowed');
+ msgWasFired = true;
+ });
+
+ tick();
+
+ expect(msgWasFired).toBeTruthy();
+
+
+ })));
+
+ it('should handle an other error with message promise correctly', inject([BaseHttpService], fakeAsync((baseservice: BaseHttpService) => {
+ let msgWasFired = false;
+ const openService: any = baseservice;
+
+ const response = { message: 'other error' };
+
+
+ openService.handleErrorPromise(response).subscribe(x => { },
+ e => {
+ expect(e).toContain('other error');
+ msgWasFired = true;
+ });
+
+ tick();
+
+ expect(msgWasFired).toBeTruthy();
+
+
+ })));
+
+
+
+});
+
diff --git a/src/app/services/base-http.service.ts b/src/app/services/base-http.service.ts
new file mode 100644
index 0000000..39b6bdd
--- /dev/null
+++ b/src/app/services/base-http.service.ts
@@ -0,0 +1,173 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { HttpClient, HttpHeaders, HttpResponse, HttpErrorResponse } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { UUID } from 'angular2-uuid';
+import { Observable } from 'rxjs/Observable';
+import { BannerMessage } from '../common/banner-message';
+import { ErrorType } from '../common/enums';
+import { Globals } from '../common/globals';
+import { SessionContext } from '../common/session-context';
+import { MessageServiceCustom } from './message.service';
+
+export enum HttpMethodEn {
+ get = 'GET',
+ post = 'POST',
+ put = 'PUT',
+ delete = 'DELETE'
+}
+
+export class HttpCallInfo {
+ constructor(
+ public serviceName: string,
+ public method: HttpMethodEn,
+ public uriFragment: string,
+ public payload: any
+ ) { }
+}
+
+class MicsEnvelopeHeader {
+ constructor(
+ public attribute: string,
+ public value: string) { }
+}
+
+class MicsEnvelope {
+ public serviceName: string;
+ public method: string;
+ public uriFragment: string;
+ public payload: string;
+ public headers: MicsEnvelopeHeader[];
+}
+
+export interface BaseHttpServiceInterface {
+ callService(callInfo: HttpCallInfo, sessionContext: SessionContext);
+}
+
+@Injectable()
+export class BaseHttpService implements BaseHttpServiceInterface {
+
+ constructor(protected messageService: MessageServiceCustom,
+ protected http: HttpClient) {
+ }
+
+ public callService(callInfo: HttpCallInfo, sessionContext: SessionContext, providedHeaders: HttpHeaders = null): Observable<any> {
+ let headers: HttpHeaders;
+ if (providedHeaders == null) {
+ headers = this.createCommonHeaders(sessionContext);
+ } else {
+ headers = providedHeaders;
+ }
+
+ const acceptValue = headers.get('Accept');
+
+ let responseType;
+ if (acceptValue.includes('xml')) {
+ responseType = 'text';
+ } else {
+ responseType = 'json';
+ }
+
+ const body = JSON.stringify(this.buildEnvelope(callInfo, headers));
+ return this.http.post(this.getBaseUrl() + '/dispatch', body, { headers: headers, responseType: responseType })
+ .catch(error => {
+ return this.handleErrorPromise(error);
+ });
+
+ }
+
+
+ private buildEnvelope(callInfo: HttpCallInfo, headers: HttpHeaders): MicsEnvelope {
+ const envelope = new MicsEnvelope();
+ envelope.method = callInfo.method;
+ envelope.serviceName = callInfo.serviceName;
+ envelope.uriFragment = callInfo.uriFragment;
+ if (callInfo.method === HttpMethodEn.post || callInfo.method === HttpMethodEn.put) {
+ envelope.payload = btoa(encodeURIComponent(JSON.stringify(callInfo.payload)));
+ }
+ envelope.headers = this.mapHeaders(headers);
+ return envelope;
+ }
+
+ private mapHeaders(headers: HttpHeaders): MicsEnvelopeHeader[] {
+ const mappedHeaders = new Array<MicsEnvelopeHeader>();
+
+ if (headers.keys().length > 0) {
+ const keys = headers.keys();
+ keys.forEach(key => {
+ mappedHeaders.push(new MicsEnvelopeHeader(key, headers.get(key)));
+ });
+ }
+ return mappedHeaders;
+ }
+
+ protected getBaseUrl(): string {
+ return Globals.BASE_URL;
+ }
+
+ public createCommonHeaders(sessionContext: SessionContext): HttpHeaders {
+ let headers = new HttpHeaders();
+ headers = headers.append('Accept', 'application/json');
+ headers = headers.append('content-Type', 'application/json');
+ headers = headers.append('Access-Control-Allow-Origin', '*');
+ headers = headers.set('Authorization', 'Bearer ' + sessionContext.getAccessToken());
+ headers = headers.append('unique-TAN', UUID.UUID());
+ if (sessionContext.getCurrSessionId() !== null) {
+ headers = headers.append(Globals.SESSION_TOKEN_TAG, sessionContext.getCurrSessionId());
+ }
+
+ return headers;
+ }
+
+ protected extractData(res: HttpResponse<any>, _sessContext: SessionContext) {
+ // let the interested 'people' know about our result
+ _sessContext.centralHttpResultCode$.emit(res.status);
+
+ if (res.status !== 302 && (res.status < 200 || res.status >= 300)) {
+ throw new Error('Bad response status: ' + res.status);
+ }
+
+ console.log(res.type);
+ const data = res;
+ return data || {};
+ }
+
+ protected extractSessionId(headers: HttpHeaders, sessionContext: SessionContext) {
+ if (headers != null) {
+ if (headers.has(Globals.SESSION_TOKEN_TAG)) {
+ sessionContext.setCurrSessionId(headers.get(Globals.SESSION_TOKEN_TAG));
+ }
+ }
+ }
+
+ protected handleErrorPromise(error: any) {
+ let errMsg: string;
+ let body = null;
+
+ if (error instanceof HttpErrorResponse) {
+ if (error.status === 401 && this.messageService) {
+ const bannerMessage = new BannerMessage();
+ bannerMessage.errorType = ErrorType.authentication;
+ this.messageService.errorOccured$.emit(bannerMessage);
+ return Observable.throw(ErrorType.authentication);
+ }
+ body = error || '';
+ const err = body.error || JSON.stringify(body);
+ errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
+ } else {
+ errMsg = error.message ? error.message : error.toString();
+ }
+
+ console.error(errMsg);
+ return Observable.throw(errMsg);
+ }
+}
diff --git a/src/app/services/cim-cache.service.spec.ts b/src/app/services/cim-cache.service.spec.ts
new file mode 100644
index 0000000..fd4356e
--- /dev/null
+++ b/src/app/services/cim-cache.service.spec.ts
@@ -0,0 +1,60 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { TestBed, inject } from '@angular/core/testing';
+import { BaseHttpService, HttpMethodEn } from './base-http.service';
+import { CimCacheService } from './cim-cache.service';
+import { MockBaseHttpService } from '../testing/mock-base-http.service';
+import { SessionContext } from '../common/session-context';
+import { Globals } from '../common/globals';
+
+describe('CimCacheService', () => {
+ let sessionContext: SessionContext;
+ let mockedBaseHttpService: MockBaseHttpService;
+
+ beforeEach(() => {
+ sessionContext = new SessionContext();
+ mockedBaseHttpService = new MockBaseHttpService();
+
+ TestBed.configureTestingModule({
+ providers: [
+ CimCacheService,
+ { provide: SessionContext, useValue: sessionContext },
+ { provide: BaseHttpService, useValue: mockedBaseHttpService },
+ SessionContext
+ ]
+ });
+ });
+
+ it('should be created', inject([CimCacheService], (service: CimCacheService) => {
+ expect(service).toBeTruthy();
+ }));
+
+ it('should call get getRessourceTypes', inject([CimCacheService], (service: CimCacheService) => {
+ mockedBaseHttpService.content = [];
+ service.getRessourceTypes();
+
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toBe('/electricity/dynamic-topology/power-system-resource-types?revision=1');
+ expect(mockedBaseHttpService.lastHttpCallInfo.serviceName).toBe(Globals.CIM_CACHE_SERVICE);
+ }));
+
+ it('should call get getRessourcesWithType', inject([CimCacheService], (service: CimCacheService) => {
+ mockedBaseHttpService.content = [];
+ const ressourceType = '';
+ service.getRessourcesWithType(ressourceType);
+
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment)
+ .toBe('/electricity/dynamic-topology/power-system-resources?revision=1&power-system-resource-types=' + ressourceType);
+ expect(mockedBaseHttpService.lastHttpCallInfo.serviceName).toBe(Globals.CIM_CACHE_SERVICE);
+ }));
+});
diff --git a/src/app/services/cim-cache.service.ts b/src/app/services/cim-cache.service.ts
new file mode 100644
index 0000000..a5c300d
--- /dev/null
+++ b/src/app/services/cim-cache.service.ts
@@ -0,0 +1,64 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { SessionContext } from './../common/session-context';
+import { BaseHttpService, HttpCallInfo, HttpMethodEn } from './base-http.service';
+import { Injectable } from '@angular/core';
+import { GridMeasure } from '../model/grid-measure';
+import { Observable } from 'rxjs/Observable';
+import { Globals } from '../common/globals';
+import { HttpHeaders } from '@angular/common/http';
+
+@Injectable()
+export class CimCacheService {
+
+ constructor(
+ private baseHttpService: BaseHttpService,
+ private sessionContext: SessionContext) {
+ }
+
+ getRessourceTypes(): Observable<any> {
+
+ let headers = new HttpHeaders();
+ headers = headers.append('Accept', 'application/xml');
+ headers = headers.append('content-Type', 'application/json');
+
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.CIM_CACHE_SERVICE, HttpMethodEn.get,
+ '/electricity/dynamic-topology/power-system-resource-types?revision=1', null),
+ this.sessionContext, headers);
+ }
+
+ getRessourcesWithType(ressourceType: string): Observable<any> {
+
+ let headers = new HttpHeaders();
+ headers = headers.append('Accept', 'application/xml');
+ headers = headers.append('content-Type', 'application/json');
+
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.CIM_CACHE_SERVICE, HttpMethodEn.get,
+ '/electricity/dynamic-topology/power-system-resources?revision=1&power-system-resource-types=' + ressourceType, null),
+ this.sessionContext, headers);
+ }
+
+ getTopology(): Observable<any> {
+
+ let headers = new HttpHeaders();
+ headers = headers.append('Accept', 'application/xml');
+ headers = headers.append('content-Type', 'application/json');
+
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.CIM_CACHE_SERVICE, HttpMethodEn.get,
+ '/electricity/dynamic-topology/topology?revision=1', null),
+ this.sessionContext, headers);
+ }
+
+}
diff --git a/src/app/services/document.service.spec.ts b/src/app/services/document.service.spec.ts
new file mode 100644
index 0000000..2f175dd
--- /dev/null
+++ b/src/app/services/document.service.spec.ts
@@ -0,0 +1,72 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { BaseHttpService, HttpMethodEn } from './base-http.service';
+import { TestBed, inject } from '@angular/core/testing';
+import { MockBaseHttpService } from '../testing/mock-base-http.service';
+import { SessionContext } from '../common/session-context';
+import { DocumentService } from './document.service';
+
+describe('Service: Document', () => {
+ let sessionContext: SessionContext;
+ let mockedBaseHttpService: MockBaseHttpService;
+
+ beforeEach(() => {
+ sessionContext = new SessionContext();
+ mockedBaseHttpService = new MockBaseHttpService();
+
+ TestBed.configureTestingModule({
+ providers: [
+ DocumentService,
+ { provide: SessionContext, useValue: sessionContext },
+ { provide: BaseHttpService, useValue: mockedBaseHttpService },
+ SessionContext
+ ]
+ });
+ });
+
+ it('should call uploadGridMeasureAttachments', inject([DocumentService], (service: DocumentService) => {
+ expect(service).toBeTruthy();
+
+ service.uploadGridMeasureAttachments(this.gmId, this.document);
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.post);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('uploadGridMeasureAttachments');
+
+ }));
+
+ it('should call getGridMeasureAttachments', inject([DocumentService], (service: DocumentService) => {
+ expect(service).toBeTruthy();
+
+ service.getGridMeasureAttachments(this.gmId);
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('getGridMeasureAttachments');
+
+ }));
+
+ it('should call downloadGridMeasureAttachment', inject([DocumentService], (service: DocumentService) => {
+ expect(service).toBeTruthy();
+
+ service.downloadGridMeasureAttachment(this.docId);
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('downloadGridMeasureAttachment');
+
+ }));
+
+ it('should call deleteGridMeasureAttachment', inject([DocumentService], (service: DocumentService) => {
+ expect(service).toBeTruthy();
+
+ service.deleteGridMeasureAttachment(this.docId);
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.delete);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('deleteGridMeasureAttachment');
+
+ }));
+
+});
diff --git a/src/app/services/document.service.ts b/src/app/services/document.service.ts
new file mode 100644
index 0000000..c1158fa
--- /dev/null
+++ b/src/app/services/document.service.ts
@@ -0,0 +1,51 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import { Globals } from '../common/globals';
+import { SessionContext } from '../common/session-context';
+import { Document } from './../model/document';
+import { BaseHttpService, HttpCallInfo, HttpMethodEn } from './base-http.service';
+
+@Injectable()
+export class DocumentService {
+
+
+ constructor(
+ private baseHttpService: BaseHttpService,
+ private sessionContext: SessionContext) { }
+
+ public uploadGridMeasureAttachments(gridmeasuereId: number, documentToUpload: Document): Observable<Document> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.post, '/uploadGridMeasureAttachments/'
+ + gridmeasuereId, documentToUpload), this.sessionContext);
+ }
+
+ public getGridMeasureAttachments(gridmeasuereId: number): Observable<Document[]> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/getGridMeasureAttachments/'
+ + gridmeasuereId, null), this.sessionContext);
+ }
+
+ public downloadGridMeasureAttachment(documentId: number): Observable<Document> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/downloadGridMeasureAttachment/'
+ + documentId, null), this.sessionContext);
+ }
+
+ public deleteGridMeasureAttachment(documentId: number) {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.delete, '/deleteGridMeasureAttachment/'
+ + documentId, null), this.sessionContext);
+ }
+
+}
diff --git a/src/app/services/grid-measure.service.spec.ts b/src/app/services/grid-measure.service.spec.ts
new file mode 100644
index 0000000..4517846
--- /dev/null
+++ b/src/app/services/grid-measure.service.spec.ts
@@ -0,0 +1,75 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { TestBed, inject } from '@angular/core/testing';
+import { BaseHttpService, HttpMethodEn } from './base-http.service';
+import { GridMeasureService } from './grid-measure.service';
+import { MockBaseHttpService } from '../testing/mock-base-http.service';
+import { SessionContext } from '../common/session-context';
+import { GridMeasure } from '../model/grid-measure';
+import { Globals } from '../common/globals';
+
+describe('Service: GridMeasure', () => {
+ let sessionContext: SessionContext;
+ let mockedBaseHttpService: MockBaseHttpService;
+
+ beforeEach(() => {
+ sessionContext = new SessionContext();
+ mockedBaseHttpService = new MockBaseHttpService();
+
+ TestBed.configureTestingModule({
+ providers: [
+ GridMeasureService,
+ { provide: SessionContext, useValue: sessionContext },
+ { provide: BaseHttpService, useValue: mockedBaseHttpService },
+ SessionContext
+ ]
+ });
+ });
+
+ it('should call createGridMeasure', inject([GridMeasureService], (service: GridMeasureService) => {
+ expect(service).toBeTruthy();
+ const gridmeasure = new GridMeasure();
+ gridmeasure.id = 555;
+
+ service.storeGridMeasure(gridmeasure);
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.put);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toBe('/storeGridMeasure');
+
+ }));
+ it('should call getGridMeasures', inject([GridMeasureService], (service: GridMeasureService) => {
+ mockedBaseHttpService.content = [{ id: 444 }, { id: 555 }];
+ service.getGridMeasures();
+
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.post);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toBe('/getGridMeasures');
+ expect(mockedBaseHttpService.lastHttpCallInfo.serviceName).toBe(Globals.GRID_MEASURES_SERVICE_NAME);
+
+ }));
+ it('should call getGridMeasure', inject([GridMeasureService], (service: GridMeasureService) => {
+ expect(service).toBeTruthy();
+
+ service.getGridMeasure(2);
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('getGridMeasure');
+
+ }));
+
+ it('should call getHistoricalStatusChanges', inject([GridMeasureService], (service: GridMeasureService) => {
+ expect(service).toBeTruthy();
+
+ service.getHistoricalStatusChanges(2);
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('getHistoricalStatusChanges');
+
+ }));
+
+});
diff --git a/src/app/services/grid-measure.service.ts b/src/app/services/grid-measure.service.ts
new file mode 100644
index 0000000..9c46bf4
--- /dev/null
+++ b/src/app/services/grid-measure.service.ts
@@ -0,0 +1,61 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import 'rxjs/add/observable/throw';
+import 'rxjs/add/operator/catch';
+import 'rxjs/add/operator/map';
+import { Globals } from '../common/globals';
+import { SessionContext } from '../common/session-context';
+import { GridMeasure } from '../model/grid-measure';
+import { StatusChange } from '../model/status-change';
+import { BaseHttpService, HttpCallInfo, HttpMethodEn } from './base-http.service';
+import { StatusMainFilter } from '../model/status-main-filter';
+
+@Injectable()
+export class GridMeasureService {
+
+
+ constructor(
+ private baseHttpService: BaseHttpService,
+ private sessionContext: SessionContext) {
+ }
+ storeGridMeasure(gridMeasure: GridMeasure): Observable<GridMeasure> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.put, '/storeGridMeasure', gridMeasure),
+ this.sessionContext);
+ }
+
+ getGridMeasures(statusMainFilter?: StatusMainFilter): Observable<GridMeasure[]> {
+ const isMainFilterSetAndActive = statusMainFilter;
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.post, '/getGridMeasures',
+ isMainFilterSetAndActive ? statusMainFilter.item : null),
+ this.sessionContext);
+ }
+ getGridMeasure(id: number): Observable<GridMeasure> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/getGridMeasure/' + id, null),
+ this.sessionContext);
+ }
+ getHistoricalStatusChanges(id: number): Observable<StatusChange[]> {
+
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/getHistoricalStatusChanges/' + id, null),
+ this.sessionContext);
+ }
+ getAffectedResourcesDistinct(): Observable<string[]> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/getAffectedResourcesDistinct/', null),
+ this.sessionContext);
+ }
+}
diff --git a/src/app/services/http-response-interceptor.service.spec.ts b/src/app/services/http-response-interceptor.service.spec.ts
new file mode 100644
index 0000000..c466e15
--- /dev/null
+++ b/src/app/services/http-response-interceptor.service.spec.ts
@@ -0,0 +1,24 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { TestBed } from '@angular/core/testing';
+
+import { HttpResponseInterceptorService } from './http-response-interceptor.service';
+
+describe('HttpResponseInterceptorService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [HttpResponseInterceptorService]
+ });
+ });
+
+
+});
diff --git a/src/app/services/http-response-interceptor.service.ts b/src/app/services/http-response-interceptor.service.ts
new file mode 100644
index 0000000..0ea81fb
--- /dev/null
+++ b/src/app/services/http-response-interceptor.service.ts
@@ -0,0 +1,61 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Injectable, OnInit } from '@angular/core';
+import { Router, ActivatedRoute } from '@angular/router';
+import { MessageServiceCustom, MessageDefines } from '../services/message.service';
+import { BannerMessage } from '../common/banner-message';
+import { SessionContext } from '../common/session-context';
+import { ErrorType } from '../common/enums';
+
+@Injectable()
+export class HttpResponseInterceptorService {
+
+ constructor(
+ private messageService: MessageServiceCustom,
+ private router: Router,
+ private sessionContext: SessionContext,
+ private activatedRoute: ActivatedRoute) { this.init(); }
+
+ private init(): void {
+ this.subscribeToMessageService();
+ }
+
+ private subscribeToMessageService() {
+ this.messageService.errorOccured$.subscribe((errorMessage: BannerMessage) => {
+ if ((errorMessage.errorType !== ErrorType.authentication)) {
+ const errorTypeIsLocked = errorMessage.errorType === ErrorType.locked;
+
+ errorMessage.showButton = errorTypeIsLocked;
+
+
+ if (errorTypeIsLocked) {
+ errorMessage.isSetTimeout = false;
+ }
+
+ this.sessionContext.setBannerMessage(errorMessage);
+
+ } else {
+ if (this.router.url.includes('loggedout')) {
+ return;
+ }
+
+ const fwdUrl = this.activatedRoute.snapshot.queryParams['fwdUrl'];
+ if (fwdUrl) {
+ window.location.href = fwdUrl;
+ } else {
+ this.router.navigate(['/loggedout']);
+ }
+ this.messageService.loginLogoff$.emit(MessageDefines.MSG_LOG_OFF);
+ }
+ });
+ }
+}
diff --git a/src/app/services/jobs/base-data-loader.service.spec.ts b/src/app/services/jobs/base-data-loader.service.spec.ts
new file mode 100644
index 0000000..2c65741
--- /dev/null
+++ b/src/app/services/jobs/base-data-loader.service.spec.ts
@@ -0,0 +1,295 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { async, fakeAsync, tick } from '@angular/core/testing';
+import { Observable } from 'rxjs/Observable';
+import { SessionContext } from '../../common/session-context';
+import { Branch } from '../../model/branch';
+import { CostCenter } from '../../model/cost-center';
+import { Status } from '../../model/status';
+import { User } from '../../model/user';
+import { MessageDefines, MessageServiceCustom } from '../../services/message.service';
+import { USERS } from '../../test-data/users';
+import { AbstractMockObservableService } from '../../testing/abstract-mock-observable.service';
+import { RoleAccess } from './../../model/role-access';
+import { UserDepartment } from './../../model/user-department';
+import { BaseDataLoaderService } from './base-data-loader.service';
+import { RoleAccessHelperService } from './role-access-helper.service';
+import { BackendSettings } from './../../model/backend-settings';
+import { HttpClient } from '@angular/common/http';
+import { TERRITORY } from '../../test-data/territories';
+import { Territory } from '../../model/territory';
+
+describe('BaseDataLoaderService', () => {
+ class MockDataService extends AbstractMockObservableService {
+ public branchList: Branch[];
+ public statusList: Status[];
+ public departmentList: UserDepartment[];
+ public costCenters: CostCenter[];
+ public emailAddresses: string[];
+ public responsibleOnSiteNameList: string[];
+ public NetworkControlsNameList: string[];
+ public areaOfSwitchingList: string[];
+ public statusError;
+ public branchError;
+ public costCenterError;
+ public departmentError;
+ public responsibleOnSiteNameError;
+ public NetworkControlsNameError;
+ public emailAddressesError;
+ public areaOfSwitchingError;
+
+ getStatuses() {
+ const newService = new MockDataService();
+ newService.content = this.statusList;
+ newService.error = this.statusError;
+ return newService;
+ }
+
+ getResponsiblesOnSiteFromGridmeasures() {
+ const newService = new MockDataService();
+ newService.content = this.responsibleOnSiteNameList;
+ newService.error = this.responsibleOnSiteNameError;
+ return newService;
+ }
+
+ getNetworkControlsFromSingleGridmeasures() {
+ const newService = new MockDataService();
+ newService.content = this.NetworkControlsNameList;
+ newService.error = this.NetworkControlsNameError;
+ return newService;
+ }
+
+ getTerritories() {
+ const newService = new MockDataService();
+ newService.content = this.areaOfSwitchingList;
+ newService.error = this.areaOfSwitchingError;
+ return newService;
+ }
+
+ getBranches() {
+ const newService = new MockDataService();
+ newService.content = this.branchList;
+ newService.error = this.branchError;
+ return newService;
+ }
+ getCostCenters() {
+ const newService = new MockDataService();
+ newService.content = this.costCenters;
+ newService.error = this.costCenterError;
+ return newService;
+ }
+ getUserDepartments() {
+ const newService = new MockDataService();
+ newService.content = this.departmentList;
+ newService.error = this.departmentError;
+ return newService;
+ }
+ getEmailAddressesFromTemplates() {
+ const newService = new MockDataService();
+ newService.content = this.emailAddresses;
+ newService.error = this.emailAddressesError;
+ return newService;
+ }
+ }
+
+ class MockUserService extends AbstractMockObservableService {
+ public userList: User[];
+ public userError;
+ public http: HttpClient;
+ getUsers() {
+ const newService = new MockDataService();
+ newService.content = this.userList;
+ newService.error = this.userError;
+ return newService;
+ }
+ }
+ class MockRoleAccessService extends AbstractMockObservableService {
+ public roleAccess: RoleAccess;
+ public roleAccessError;
+ public http: HttpClient;
+ getRoleAccessDefinition(): Observable<RoleAccess> {
+ const newService = new MockDataService();
+ newService.content = this.roleAccess;
+ newService.error = this.roleAccessError;
+ return newService as any as Observable<RoleAccess>;
+ }
+ }
+
+ class MockBackendSettingsService extends AbstractMockObservableService {
+ public backendSettings: BackendSettings;
+ public backendSettingsError;
+ public http: HttpClient;
+ getBackendSettings() {
+ const newService = new MockDataService();
+ newService.content = this.backendSettings;
+ newService.error = this.backendSettingsError;
+ return newService;
+ }
+ }
+
+
+ let sessionContext: SessionContext;
+ let messageService: MessageServiceCustom;
+ let mockRoleAccessService;
+ let mockDataService;
+ let loaderServer: BaseDataLoaderService;
+ let mockUserService;
+ let roleAccessHelper: RoleAccessHelperService;
+ let mockBackendSettings;
+
+
+ beforeEach(async(() => {
+ mockDataService = new MockDataService();
+ mockUserService = new MockUserService();
+ mockRoleAccessService = new MockRoleAccessService();
+ mockBackendSettings = new MockBackendSettingsService();
+
+ sessionContext = new SessionContext();
+ sessionContext.clearStorage();
+ messageService = new MessageServiceCustom(sessionContext);
+ roleAccessHelper = new RoleAccessHelperService();
+
+ loaderServer = new BaseDataLoaderService(mockDataService, mockUserService, messageService,
+ sessionContext, mockRoleAccessService, roleAccessHelper, mockBackendSettings);
+
+ }));
+
+
+ it('should be load the base-data after MSG_LOG_IN_SUCCEEDED-Message "', fakeAsync(() => {
+ mockDataService.statusList = [{ id: 1, name: 'neu' }];
+ mockDataService.branchList = [{ id: 1, name: 'W', description: 'Wasser' }];
+ mockDataService.departmentList = [{ id: 1, name: 'Abteilung 1' }, { id: 2, name: 'Abteilung 2' }];
+ mockDataService.costCenters = [{ id: 1, name: 'cc1' }, { id: 2, name: 'cc2' }];
+ mockDataService.emailAddresses = ['testpreconfmail@test.de', 'testpreconfmail2@test.de'];
+ mockDataService.responsibleOnSiteNameList = ['harald', 'simon'];
+ mockDataService.NetworkControlsNameList = ['test', 'mich'];
+ mockDataService.areaOfSwitchingList = JSON.parse(JSON.stringify(TERRITORY));
+ mockUserService.userList = USERS;
+ mockBackendSettings.backendSettings = [{ reminderPeriod: 48 }];
+ const roleAccessDef: RoleAccess = {
+ editRoles: [
+ {
+ name: 'planned-policies-measureapplicant',
+ gridMeasureStatusIds: [
+ 0
+ ]
+ },
+ {
+ name: 'planned-policies-measureplanner',
+ gridMeasureStatusIds: [
+ 1
+ ]
+ },
+ {
+ name: 'planned-policies-measureapprover',
+ gridMeasureStatusIds: [
+ 2
+ ]
+ }
+ ],
+ controls: [
+ {
+ gridMeasureStatusId: 0,
+ activeButtons: [
+ 'save',
+ 'apply'
+ ],
+ inactiveFields: [
+ 'titleControl'
+ ]
+ }
+ ],
+ stornoSection:
+ {
+ 'stornoRoles': [
+ 'planned-policies-measureapplicant',
+ 'planned-policies-measureplanner',
+ 'planned-policies-measureapprover',
+ 'planned-policies-requester',
+ 'planned-policies-clearance'
+ ]
+ },
+ duplicateSection:
+ {
+ 'duplicateRoles': [
+ 'planned-policies-measureapplicant'
+ ]
+ }
+ };
+
+ (mockRoleAccessService as any).roleAccess = roleAccessDef;
+ roleAccessHelper.init(roleAccessDef);
+
+ expect(sessionContext.getStatuses()).toBeNull();
+ expect(sessionContext.getBranches()).toBeNull();
+ expect(sessionContext.getAllUserDepartments()).toBeNull();
+ expect(sessionContext.getAllUsers()).toBeNull();
+ expect(sessionContext.getBackendsettings()).toBeNull();
+ expect(sessionContext.getEmailAddressesFromTemplates()).toBeNull();
+
+ expect(sessionContext.getResponsiblesOnSiteFromGridmeasures()).toBeNull();
+ expect(sessionContext.getTerritories()).toBeNull();
+ messageService.loginLogoff$.emit(MessageDefines.MSG_LOG_IN_SUCCEEDED);
+
+ tick();
+
+ expect(sessionContext.getAllUserDepartments().length).toBe(2);
+ expect(sessionContext.getAllUserDepartments()[0].name).toBe('Abteilung 1');
+
+ expect(sessionContext.getStatuses().length).toBe(1);
+ expect(sessionContext.getStatuses()[0].name).toBe('neu');
+
+ expect(sessionContext.getBranches().length).toBe(1);
+ expect(sessionContext.getBranches()[0].description).toBe('Wasser');
+
+ expect(sessionContext.getEmailAddressesFromTemplates().length).toBe(2);
+ expect(sessionContext.getEmailAddressesFromTemplates()[0]).toBe('testpreconfmail@test.de');
+
+ expect(sessionContext.getResponsiblesOnSiteFromGridmeasures().length).toBe(2);
+ expect(sessionContext.getResponsiblesOnSiteFromGridmeasures()[0]).toBe('harald');
+
+ expect(sessionContext.getNetworkControlsFromSingleGridmeasures().length).toBe(2);
+ expect(sessionContext.getNetworkControlsFromSingleGridmeasures()[0]).toBe('test');
+
+ expect(sessionContext.getTerritories().length).toBe(3);
+ expect(sessionContext.getTerritories()[0].name).toBe('h1');
+
+ expect(sessionContext.getEmailAddressesFromTemplates().length).toBe(2);
+
+ expect(sessionContext.getAllUsers().length).toBe(3);
+
+ expect(sessionContext.getBackendsettings()[0].reminderPeriod).toBe(48);
+ expect(roleAccessHelper.getRoleAccessDefinitions().editRoles.length > 0).toBeTruthy();
+
+ }));
+
+ it('should log to the console if errors occur while loading', fakeAsync(() => {
+ spyOn(console, 'log');
+ mockDataService.costCenterError = 'Error in costCenter service';
+ mockDataService.responsibleOnSiteNameError = 'Error in responsibleOnSiteName service';
+ mockDataService.branchError = 'Error in branch service';
+ mockDataService.statusError = 'Error in status service';
+ mockUserService.userError = 'Error in user service';
+ mockDataService.departmentError = 'Error in department service';
+ mockDataService.emailAddressError = 'Error in emailAddress service';
+ mockDataService.areaOfSwitchingError = 'Error in areaOfSwitching service';
+ mockBackendSettings.backendSettingsError = 'Error in backendsettings service';
+ (mockRoleAccessService as MockRoleAccessService).roleAccessError = 'Error in roleAcess service';
+ messageService.loginLogoff$.emit(MessageDefines.MSG_LOG_IN_SUCCEEDED);
+ messageService.loginLogoff$.emit(MessageDefines.MSG_LOG_OFF);
+
+ tick();
+
+ expect(console.log).toHaveBeenCalledTimes(9);
+
+ }));
+});
diff --git a/src/app/services/jobs/base-data-loader.service.ts b/src/app/services/jobs/base-data-loader.service.ts
new file mode 100644
index 0000000..90a51d4
--- /dev/null
+++ b/src/app/services/jobs/base-data-loader.service.ts
@@ -0,0 +1,126 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Injectable } from '@angular/core';
+import { MessageServiceCustom, MessageDefines } from '../message.service';
+import { UserService } from '../user.service';
+import { SessionContext } from '../../common/session-context';
+import { BaseDataService } from '../base-data.service';
+import { RoleAccessService } from '../role-access.service';
+import { RoleAccessHelperService } from './role-access-helper.service';
+import { BackendSettingsService } from '../backend-settings.service';
+
+
+@Injectable()
+export class BaseDataLoaderService {
+
+ constructor(
+ private baseDataService: BaseDataService,
+ private userService: UserService,
+ private msgService: MessageServiceCustom,
+ private sessionContext: SessionContext,
+ private roleAccessService: RoleAccessService,
+ private roleAccessHelper: RoleAccessHelperService,
+ private backendSettingsService: BackendSettingsService
+ ) {
+ this.msgService.loginLogoff$.subscribe(msg => this.onLoginLogoff(msg));
+ }
+
+ onLoginLogoff(msg: string): void {
+ this.sessionContext.initBannerMessage();
+ if (msg === MessageDefines.MSG_LOG_IN_SUCCEEDED) {
+ this.sessionContext.setUserAuthenticated(true);
+ this.loadBaseData();
+ }
+
+ if (msg === MessageDefines.MSG_LOG_OFF) {
+ this.sessionContext.setUserAuthenticated(false);
+ this.sessionContext.clearStorage();
+ }
+
+ }
+
+ loadBaseData() {
+ // load base data (stammdaten) here!
+ this.baseDataService.getCostCenters()
+ .subscribe(center => this.sessionContext.setCostCenters(center),
+ error => {
+ console.log(error);
+ });
+
+ this.baseDataService.getNetworkControlsFromSingleGridmeasures()
+ .subscribe(res => this.sessionContext.setNetworkControlsFromSingleGridmeasures(res),
+ error => {
+ console.log(error);
+ });
+
+ this.baseDataService.getResponsiblesOnSiteFromGridmeasures()
+ .subscribe(res => this.sessionContext.setResponsiblesOnSiteFromGridmeasures(res),
+ error => {
+ console.log(error);
+ });
+
+ this.baseDataService.getBranches()
+ .subscribe(branch => this.sessionContext.setBranches(branch),
+ error => {
+ console.log(error);
+ });
+
+ this.baseDataService.getStatuses()
+ .subscribe(status => this.sessionContext.setStatuses(status),
+ error => {
+ console.log(error);
+ });
+
+ this.userService.getUsers()
+ .subscribe(users => {
+ this.sessionContext.setAllUsers(users);
+ },
+ error => {
+ console.log(error);
+ });
+
+ this.loadRoleAccessdefinitions();
+
+ this.baseDataService.getUserDepartments()
+ .subscribe(departments => this.sessionContext.setAllUserDepartments(departments),
+ error => {
+ console.log(error);
+ });
+
+ this.baseDataService.getEmailAddressesFromTemplates()
+ .subscribe(emails => this.sessionContext.setEmailAddressesFromTemplates(emails),
+ error => {
+ console.log(error);
+ });
+
+ this.backendSettingsService.getBackendSettings()
+ .subscribe(settings => this.sessionContext.setBackendsettings(settings),
+ error => {
+ console.log(error);
+ });
+
+ this.baseDataService.getTerritories()
+ .subscribe(ter => this.sessionContext.setTerritories(ter),
+ error => {
+ console.log(error);
+ });
+ }
+
+ loadRoleAccessdefinitions(): void {
+ this.roleAccessService.getRoleAccessDefinition().subscribe(
+ res => this.roleAccessHelper.init(res),
+ error => {
+ console.log(error);
+ });
+ }
+
+}
diff --git a/src/app/services/jobs/reminder-caller-job.service.spec.ts b/src/app/services/jobs/reminder-caller-job.service.spec.ts
new file mode 100644
index 0000000..e82617c
--- /dev/null
+++ b/src/app/services/jobs/reminder-caller-job.service.spec.ts
@@ -0,0 +1,141 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { async, fakeAsync, tick, TestBed, ComponentFixture, inject, discardPeriodicTasks } from '@angular/core/testing';
+
+import { MessageServiceCustom, MessageDefines } from '../../services/message.service';
+import { AbstractMockObservableService } from '../../testing/abstract-mock-observable.service';
+import { SessionContext } from '../../common/session-context';
+import { ReminderCallerJobService } from './reminder-caller-job.service';
+import { Globals } from '../../common/globals';
+import { ReminderService } from '../reminder.service';
+
+
+
+describe('ReminderCallerJobService', () => {
+
+ class MockReminderService extends AbstractMockObservableService {
+
+ getCurrentReminders() {
+ return this;
+ }
+ getExpiredReminders() {
+ return this;
+ }
+ }
+
+
+ let sessionContext: SessionContext;
+ let messageService: MessageServiceCustom;
+ let mockReminderService;
+ let injectedService;
+
+ beforeEach(async(() => {
+
+ mockReminderService = new MockReminderService();
+ sessionContext = new SessionContext();
+ sessionContext.clearStorage();
+ messageService = new MessageServiceCustom(sessionContext);
+ injectedService = new ReminderCallerJobService(sessionContext, mockReminderService, messageService);
+
+ TestBed.configureTestingModule({
+ providers: [
+ { provide: ReminderService, useValue: mockReminderService },
+ { provide: SessionContext, useValue: sessionContext },
+ MessageServiceCustom
+ ]
+ }).compileComponents();
+
+ }));
+
+ it('should init the timer after successfully login', fakeAsync(() => {
+ spyOn(injectedService, 'init').and.callThrough();
+ expect(injectedService.init).toHaveBeenCalledTimes(0);
+
+ messageService.loginLogoff$.emit(MessageDefines.MSG_LOG_IN_SUCCEEDED);
+ tick();
+
+ expect(injectedService.init).toHaveBeenCalledTimes(1);
+ discardPeriodicTasks();
+ }));
+
+ it('should destroy the timer after logout', fakeAsync(() => {
+ spyOn(injectedService, 'destroy').and.callThrough();
+ messageService.loginLogoff$.emit(MessageDefines.MSG_LOG_IN_SUCCEEDED);
+ tick();
+
+ expect(injectedService.destroy).toHaveBeenCalledTimes(0);
+
+ messageService.loginLogoff$.emit(MessageDefines.MSG_LOG_OFF);
+ tick();
+
+ expect(injectedService.destroy).toHaveBeenCalledTimes(1);
+ discardPeriodicTasks();
+ }));
+
+ it('should set setUpcomingReminder and setOverdueReminder to true when remindernotifications exist', fakeAsync(() => {
+ spyOn(injectedService, 'init').and.callThrough();
+ sessionContext.setUpcomingReminder(false);
+ sessionContext.setOverdueReminder(false);
+
+ mockReminderService.content = [111];
+ Globals.REMINDER_JOB_POLLING_START_DELAY = 10;
+ expect(sessionContext.getUpcomingReminder()).toBeFalsy();
+ expect(sessionContext.getOverdueReminder()).toBeFalsy();
+
+ messageService.loginLogoff$.emit(MessageDefines.MSG_LOG_IN_SUCCEEDED);
+ tick(15);
+
+ expect(injectedService.init).toHaveBeenCalledTimes(1);
+ expect(sessionContext.getUpcomingReminder()).toBeTruthy();
+ expect(sessionContext.getOverdueReminder()).toBeTruthy();
+ discardPeriodicTasks();
+ }));
+
+ it('should set setUpcomingReminder and setOverdueReminder to false when the are no remindernotifications', fakeAsync(() => {
+ spyOn(injectedService, 'init').and.callThrough();
+ sessionContext.setUpcomingReminder(false);
+ sessionContext.setOverdueReminder(false);
+
+ mockReminderService.content = [];
+ Globals.REMINDER_JOB_POLLING_START_DELAY = 10;
+
+ expect(sessionContext.getUpcomingReminder()).toBeFalsy();
+ expect(sessionContext.getOverdueReminder()).toBeFalsy();
+
+ messageService.loginLogoff$.emit(MessageDefines.MSG_LOG_IN_SUCCEEDED);
+ tick(15);
+
+ expect(injectedService.init).toHaveBeenCalledTimes(1);
+ expect(sessionContext.getUpcomingReminder()).toBeFalsy();
+ expect(sessionContext.getOverdueReminder()).toBeFalsy();
+ discardPeriodicTasks();
+ }));
+
+ it('should call "setError" when an error occurs while running "doJob"', fakeAsync(() => {
+ spyOn(injectedService, 'setError').and.callThrough();
+ sessionContext.setUpcomingReminder(false);
+ sessionContext.setOverdueReminder(false);
+
+ Globals.REMINDER_JOB_POLLING_START_DELAY = 10;
+ mockReminderService.error = 'REMINDER_MOCK_ERROR';
+ expect(sessionContext.getUpcomingReminder()).toBe(false);
+ expect(sessionContext.getOverdueReminder()).toBeFalsy();
+
+ messageService.loginLogoff$.emit(MessageDefines.MSG_LOG_IN_SUCCEEDED);
+ tick(15);
+
+ expect(injectedService.setError).toHaveBeenCalledTimes(2);
+ discardPeriodicTasks();
+ }));
+
+
+});
diff --git a/src/app/services/jobs/reminder-caller-job.service.ts b/src/app/services/jobs/reminder-caller-job.service.ts
new file mode 100644
index 0000000..ea95390
--- /dev/null
+++ b/src/app/services/jobs/reminder-caller-job.service.ts
@@ -0,0 +1,97 @@
+/**
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Injectable } from '@angular/core';
+import { Subscription } from 'rxjs/Subscription';
+import { TimerObservable } from 'rxjs/observable/TimerObservable';
+import { ReminderService } from '../../services/reminder.service';
+
+import { SessionContext } from '../../common/session-context';
+
+import { MessageServiceCustom, MessageDefines } from '../../services/message.service';
+import { Globals } from '../../common/globals';
+
+
+
+@Injectable()
+export class ReminderCallerJobService {
+
+ private timer;
+ private subscription: Subscription;
+
+
+ constructor(
+ protected _sessionContext: SessionContext,
+ protected reminderService: ReminderService,
+ protected msgService: MessageServiceCustom
+ ) {
+ this.msgService.loginLogoff$.subscribe(msg => this.onLoginLogoff(msg));
+ }
+
+ onLoginLogoff(msg: string) {
+ if (msg === MessageDefines.MSG_LOG_IN_SUCCEEDED) {
+ this.init();
+ }
+ if (msg === MessageDefines.MSG_LOG_OFF) {
+ this.destroy();
+ }
+ }
+
+ init() {
+
+ this.timer = TimerObservable.create(Globals.REMINDER_JOB_POLLING_START_DELAY, Globals.REMINDER_JOB_POLLING_INTERVALL);
+ this.subscription = this.timer.subscribe(t => this.doJob(t));
+
+ }
+
+ destroy() {
+ if (this.subscription) {
+ this.subscription.unsubscribe();
+ }
+ }
+
+ doJob(tick: number) {
+
+ this.reminderService.getCurrentReminders()
+ .subscribe(currentReminders => {
+ this._sessionContext.setCurrentReminders(currentReminders);
+
+ if (currentReminders && currentReminders.length) {
+ this._sessionContext.setUpcomingReminder(true);
+ } else {
+ this._sessionContext.setUpcomingReminder(false);
+ }
+ }, error => {
+ console.log(error);
+ this.setError(error);
+ });
+
+ this.reminderService.getExpiredReminders()
+ .subscribe(expiredReminders => {
+ this._sessionContext.setExpiredReminders(expiredReminders);
+
+ if (expiredReminders && expiredReminders.length) {
+ this._sessionContext.setOverdueReminder(true);
+ } else {
+ this._sessionContext.setOverdueReminder(false);
+ }
+ }, error => {
+ console.log(error);
+ this.setError(error);
+ });
+
+ }
+
+ protected setError(showErr: boolean) {
+ }
+
+
+}
diff --git a/src/app/services/jobs/role-access-helper.service.spec.ts b/src/app/services/jobs/role-access-helper.service.spec.ts
new file mode 100644
index 0000000..774aa66
--- /dev/null
+++ b/src/app/services/jobs/role-access-helper.service.spec.ts
@@ -0,0 +1,87 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { TestBed, inject } from '@angular/core/testing';
+import { RoleAccessHelperService } from './role-access-helper.service';
+import { RoleAccess } from '../../model/role-access';
+
+describe('RoleAccessHelperService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [RoleAccessHelperService]
+ });
+ });
+
+ it('should be created', inject([RoleAccessHelperService], (service: RoleAccessHelperService) => {
+ expect(service).toBeTruthy();
+ }));
+
+ it('should behave correctly', inject([RoleAccessHelperService], (service: RoleAccessHelperService) => {
+ const roleAccessDef: RoleAccess = {
+ editRoles: [
+ {
+ name: 'planned-policies-measureapplicant',
+ gridMeasureStatusIds: [
+ 0
+ ]
+ },
+ {
+ name: 'planned-policies-measureplanner',
+ gridMeasureStatusIds: [
+ 1
+ ]
+ },
+ {
+ name: 'planned-policies-measureapprover',
+ gridMeasureStatusIds: [
+ 2
+ ]
+ }
+ ],
+ controls: [
+ {
+ gridMeasureStatusId: 0,
+ activeButtons: [
+ 'save',
+ 'apply'
+ ],
+ inactiveFields: [
+ 'titleControl'
+ ]
+ }
+ ],
+ stornoSection:
+ {
+ 'stornoRoles': [
+ 'planned-policies-measureapplicant',
+ 'planned-policies-measureplanner',
+ 'planned-policies-measureapprover',
+ 'planned-policies-requester',
+ 'planned-policies-clearance'
+ ]
+ },
+ duplicateSection:
+ {
+ 'duplicateRoles': [
+ 'planned-policies-measureapplicant'
+ ]
+ }
+ };
+
+ service.init(roleAccessDef);
+ expect(service.getRoleAccessDefinitions().controls[0].inactiveFields[0]).toBe('titleControl');
+ expect(service.editPossibleForRoles(['bruno', 'bertil', 'planned-policies-measureplanner'], 1)).toBeTruthy();
+ expect(service.editPossibleForRoles([], 1)).toBeFalsy();
+ expect(service.editPossibleForRoles(['bruno', 'bertil', 'nope'], 1)).toBeFalsy();
+ expect(service.editPossibleForRoles(['bruno', 'bertil', 'nope'], 666)).toBeFalsy();
+
+ }));
+});
diff --git a/src/app/services/jobs/role-access-helper.service.ts b/src/app/services/jobs/role-access-helper.service.ts
new file mode 100644
index 0000000..bfa0889
--- /dev/null
+++ b/src/app/services/jobs/role-access-helper.service.ts
@@ -0,0 +1,58 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+/* tslint:disable:no-unused-variable */
+import { Injectable } from '@angular/core';
+import { RoleAccess, EditRoleItems } from '../../model/role-access';
+
+@Injectable()
+export class RoleAccessHelperService {
+ roleAccess: RoleAccess;
+ status2RoleMap: string[][];
+
+ constructor() { }
+
+ public init(newRoleAccess: RoleAccess) {
+ if (!this.roleAccess) {
+ this.roleAccess = newRoleAccess;
+
+ this.status2RoleMap = [];
+ this.roleAccess.editRoles.forEach(item => this.addEditRoleItem(item));
+ }
+ }
+
+ public editPossibleForRoles(userRoles: string[], statusToBeChecked: number): boolean {
+ const rolesForStatus = this.status2RoleMap[statusToBeChecked];
+ if (rolesForStatus && userRoles) {
+ for (let i = 0; i < userRoles.length; i++) {
+ if (rolesForStatus.find(s => s === userRoles[i])) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public getRoleAccessDefinitions(): RoleAccess {
+ return this.roleAccess;
+ }
+
+ private addEditRoleItem(item: EditRoleItems) {
+ item.gridMeasureStatusIds.forEach(status => {
+ if (!this.status2RoleMap[status]) {
+ this.status2RoleMap[status] = [];
+ }
+ this.status2RoleMap[status].push(item.name);
+ });
+ }
+
+
+}
diff --git a/src/app/services/lock-helper.service.spec.ts b/src/app/services/lock-helper.service.spec.ts
new file mode 100644
index 0000000..047e848
--- /dev/null
+++ b/src/app/services/lock-helper.service.spec.ts
@@ -0,0 +1,217 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+******************************************************************************
+ */
+import { TestBed, inject, async, fakeAsync, tick } from '@angular/core/testing';
+import { AbstractMockObservableService } from '..//testing/abstract-mock-observable.service';
+import { Lock } from '../model/lock';
+import { LockHelperService } from './lock-helper.service';
+import { LockService } from './lock.service';
+import { SessionContext } from '../common/session-context';
+import { EventEmitter } from '@angular/core';
+import { USERS } from '../test-data/users';
+import { UserMap } from '../common/user-map';
+import { ToasterMessageService } from './toaster-message.service';
+import { MessageService } from 'primeng/api';
+
+describe('LockHelperService', () => {
+
+ class MockLockService extends AbstractMockObservableService {
+ checkLock(key: number, info: string) {
+ return this;
+ }
+ storeLock(lock: Lock) {
+ return this;
+ }
+
+ deleteLock(key: number, info: string) {
+ return this;
+ }
+ }
+
+ let lockHelperService: LockHelperService;
+ let lockService: MockLockService;
+ let sessionContext: SessionContext;
+ let toasterMessageService: ToasterMessageService;
+ let messageService: MessageService;
+
+ beforeEach(async(() => {
+
+ messageService = new MessageService();
+ lockService = new MockLockService();
+ sessionContext = new SessionContext();
+ sessionContext.userMap = new UserMap(USERS);
+ toasterMessageService = new ToasterMessageService(sessionContext, messageService);
+
+ TestBed.configureTestingModule({
+ providers: [
+ LockHelperService,
+ { provide: LockService, useValue: lockService },
+ { provide: SessionContext, useValue: sessionContext },
+ { provide: ToasterMessageService, useValue: toasterMessageService },
+ { provide: MessageService, useValue: messageService }
+ ]
+ });
+ }));
+
+
+ beforeEach(inject([LockHelperService], (service: LockHelperService) => {
+ lockHelperService = service;
+ }));
+
+ it('should be created', fakeAsync(() => {
+ expect(lockHelperService).toBeTruthy();
+ }));
+
+ it('should fail if service does respond empty', fakeAsync(() => {
+ lockService.content = {};
+ let serviceResponded = false;
+ const lockedByOtherEmitter$ = new EventEmitter<boolean>();
+
+ lockedByOtherEmitter$.subscribe(isLocked => {
+ expect(isLocked).toBe(false);
+ serviceResponded = true;
+ });
+
+ tick();
+
+ lockHelperService.checkLockedByUser(5, 'testType', lockedByOtherEmitter$);
+
+ expect(serviceResponded).toBeTruthy();
+ }));
+
+
+ it('should fail if service responds with error', fakeAsync(() => {
+ spyOn(toasterMessageService, 'showError').and.callThrough();
+ lockService.error = 'error';
+ let serviceResponded = false;
+ const lockedByOtherEmitter$ = new EventEmitter<boolean>();
+
+ lockedByOtherEmitter$.subscribe(isLocked => {
+ expect(isLocked).toBe(true);
+ serviceResponded = true;
+ });
+
+ tick();
+
+ lockHelperService.checkLockedByUser(5, 'testType', lockedByOtherEmitter$);
+
+ expect(toasterMessageService.showError).toHaveBeenCalled();
+ expect(serviceResponded).toBeTruthy();
+ }));
+
+ it('should return locked if service responds with different user', fakeAsync(() => {
+ sessionContext.setCurrUser({ username: 'tom' });
+ lockService.content = { id: 8888, key: 'testType', username: 'paule', info: 'testType' };
+ let serviceResponded = false;
+ const lockedByOtherEmitter$ = new EventEmitter<boolean>();
+
+ lockedByOtherEmitter$.subscribe(isLocked => {
+ expect(isLocked).toBe(true);
+ serviceResponded = true;
+ });
+
+ tick();
+
+ lockHelperService.checkLockedByUser(5, 'testType', lockedByOtherEmitter$);
+
+ expect(serviceResponded).toBeTruthy();
+ }));
+
+ it('should return correct value when calling checkLock', fakeAsync(() => {
+ spyOn(toasterMessageService, 'showUnlockConfirm').and.callThrough();
+
+ sessionContext.setCurrUser({ username: 'tom' });
+ const lockedByOtherEmitter$ = new EventEmitter<boolean>();
+
+ expect((lockHelperService as any).checkLock({ id: 8888, key: 'testType', username: 'paule', info: 'testType' }))
+ .toBe(true);
+
+ expect((lockHelperService as any).checkLock({ id: 8888, key: 'testType', username: 'jakub', info: 'testType' }))
+ .toBe(true);
+ expect((lockHelperService as any).checkLock(null)).toBe(false);
+ expect((lockHelperService as any).checkLock({})).toBe(false);
+ expect((lockHelperService as any).checkLock({ id: 8888, key: 'testType', username: 'No lock', info: 'testType' }))
+ .toBe(false);
+
+ expect((lockHelperService as any).checkLock({ id: 8888, key: 'testType', username: 'tom', info: 'testType' }))
+ .toBe(false);
+
+ expect(toasterMessageService.showUnlockConfirm).toHaveBeenCalledTimes(2);
+
+ }));
+
+
+ it('should create a lock correctly', fakeAsync(() => {
+ sessionContext.setCurrUser({ username: 'tom' });
+ lockService.content = {};
+
+ spyOn(lockService, 'storeLock').and.callThrough();
+
+ tick();
+
+ lockHelperService.createLock(5, 'testType', null);
+
+ expect(lockService.storeLock).toHaveBeenCalled();
+ }));
+
+
+ it('should behave correctly when create a lock fails', fakeAsync(() => {
+ lockService.error = 'error';
+
+ spyOn(lockService, 'storeLock').and.callThrough();
+ spyOn(toasterMessageService, 'showError').and.callThrough();
+
+ tick();
+
+ lockHelperService.createLock(5, 'testType', null);
+
+ expect(lockService.storeLock).toHaveBeenCalled();
+ expect(toasterMessageService.showError).toHaveBeenCalled();
+
+ }));
+
+ it('should delete a lock correctly', fakeAsync(() => {
+ lockService.content = {};
+
+ spyOn(lockService, 'deleteLock').and.callThrough();
+
+ tick();
+
+ lockHelperService.deleteLock(5, 'testType', false, null);
+
+ expect(lockService.deleteLock).toHaveBeenCalled();
+ }));
+
+
+ it('should behave correctly when delete lock fails', fakeAsync(() => {
+ lockService.error = 'error';
+
+ spyOn(lockService, 'deleteLock').and.callThrough();
+
+ tick();
+
+ lockHelperService.deleteLock(5, 'testType', false, null);
+
+ expect(lockService.deleteLock).toHaveBeenCalled();
+ }));
+
+ it('should not call lockService delete lock when given id is undefined', fakeAsync(() => {
+ lockService.error = 'error';
+
+ spyOn(lockService, 'deleteLock').and.callThrough();
+
+ tick();
+
+ lockHelperService.deleteLock(undefined, 'testType', false, null);
+
+ expect(lockService.deleteLock).toHaveBeenCalledTimes(0);
+ }));
+
+});
diff --git a/src/app/services/lock-helper.service.ts b/src/app/services/lock-helper.service.ts
new file mode 100644
index 0000000..659a902
--- /dev/null
+++ b/src/app/services/lock-helper.service.ts
@@ -0,0 +1,87 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+******************************************************************************
+ */
+import { Injectable, EventEmitter } from '@angular/core';
+import { Lock } from '../model/lock';
+import { LockService } from './lock.service';
+import { SessionContext } from '../common/session-context';
+import { ErrorType } from '../common/enums';
+import { isNullOrUndefined } from 'util';
+import { Globals } from '../common/globals';
+import { ToasterMessageService } from './toaster-message.service';
+
+@Injectable()
+export class LockHelperService {
+
+ constructor(private lockService: LockService,
+ private sessionContext: SessionContext,
+ private toasterMessageService: ToasterMessageService) { }
+
+ checkLockedByUser(measureId: number, lockType: string, lockedByOtherEmitter$: EventEmitter<boolean>) {
+ this.lockService.checkLock(measureId, lockType).subscribe((lock: Lock) => {
+ const isLockedByOther = this.checkLock(lock);
+ lockedByOtherEmitter$.emit(isLockedByOther);
+ },
+ error => {
+ this.toasterMessageService.showError(ErrorType.locked, 'lockService checkLock', error);
+ lockedByOtherEmitter$.emit(true);
+ });
+ }
+
+ private checkLock(lock: Lock): boolean {
+ let isLockedByOther = true;
+ if (!lock || !lock.username || lock.username === '' || lock.username === 'No lock') {
+ isLockedByOther = false;
+ } else if (lock.username !== '' && lock.username !== this.sessionContext.getCurrUser().username) {
+ this.toasterMessageService.showUnlockConfirm('Eintrag ist gesperrt von ' +
+ this.sessionContext.getUserMap().findAndRenderUser(lock.username) + '.');
+ isLockedByOther = true;
+ } else if (lock.username === this.sessionContext.getCurrUser().username) {
+ isLockedByOther = false;
+ }
+
+ return isLockedByOther;
+ }
+
+ createLock(gridmeasureId: number, lockType: string, createLockEmitter$: EventEmitter<boolean>) {
+ const lock = new Lock();
+ lock.key = gridmeasureId;
+ lock.username = this.sessionContext.getCurrUser().username;
+ lock.info = lockType;
+ this.lockService.storeLock(lock).subscribe(resp => {
+ this.emitIfSet(true, createLockEmitter$);
+ },
+ error => {
+ this.emitIfSet(false, createLockEmitter$);
+ this.toasterMessageService.showError(ErrorType.create, 'Sperre (Maßnahme {' + gridmeasureId + '})');
+ console.log(error);
+ });
+ }
+
+ deleteLock(measureId: number, lockType: string, force: boolean, deleteLockEmitter$: EventEmitter<boolean>) {
+ if (!isNullOrUndefined(measureId)) {
+ const forceText = force ? Globals.FORCE_UNLOCK : Globals.NO_FORCE_UNLOCK;
+ this.lockService.deleteLock(measureId, lockType, forceText).subscribe(resp => {
+ this.emitIfSet(true, deleteLockEmitter$);
+ },
+ error => {
+ this.emitIfSet(false, deleteLockEmitter$);
+ console.log('Fehler beim Aufheben der Sperre (Maßnahme {' + measureId + '})');
+ });
+ }
+ }
+
+ emitIfSet(msg: boolean, emitter: EventEmitter<boolean>) {
+ if (emitter) {
+ emitter.emit(msg);
+ }
+ }
+
+}
diff --git a/src/app/services/lock.service.spec.ts b/src/app/services/lock.service.spec.ts
new file mode 100644
index 0000000..8a712e6
--- /dev/null
+++ b/src/app/services/lock.service.spec.ts
@@ -0,0 +1,70 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { BaseHttpService, HttpMethodEn } from './base-http.service';
+import { TestBed, inject } from '@angular/core/testing';
+import { MockBaseHttpService } from '../testing/mock-base-http.service';
+import { SessionContext } from '../common/session-context';
+import { LOCKS } from './../test-data/locks';
+import { LockService } from './lock.service';
+import { Lock } from './../model/lock';
+import { Globals } from '../common/globals';
+
+describe('Service: Lock', () => {
+ let sessionContext: SessionContext;
+ let mockedBaseHttpService: MockBaseHttpService;
+
+ beforeEach(() => {
+ sessionContext = new SessionContext();
+ mockedBaseHttpService = new MockBaseHttpService();
+ const lock: Lock = LOCKS[0];
+ const gmId = 2;
+ const lockId = 1;
+
+ TestBed.configureTestingModule({
+ providers: [
+ LockService,
+ { provide: SessionContext, useValue: sessionContext },
+ { provide: BaseHttpService, useValue: mockedBaseHttpService },
+ SessionContext
+ ]
+ });
+ });
+
+ it('should call storeLock', inject([LockService], (service: LockService) => {
+ expect(service).toBeTruthy();
+
+ service.storeLock(this.lock);
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.put);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('createLock');
+
+ }));
+
+ it('should call checkLock', inject([LockService], (service: LockService) => {
+ expect(service).toBeTruthy();
+
+ const returnValue = service.checkLock(this.gmId, 'gridmeasure');
+
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('checkLock');
+
+ }));
+
+ it('should call deleteLock', inject([LockService], (service: LockService) => {
+ expect(service).toBeTruthy();
+
+ service.deleteLock(this.gmId, 'gridmeasure', Globals.NO_FORCE_UNLOCK);
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.delete);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('deleteLock');
+
+ }));
+
+});
diff --git a/src/app/services/lock.service.ts b/src/app/services/lock.service.ts
new file mode 100644
index 0000000..4f2f100
--- /dev/null
+++ b/src/app/services/lock.service.ts
@@ -0,0 +1,44 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import { Globals } from '../common/globals';
+import { SessionContext } from '../common/session-context';
+import { Lock } from './../model/lock';
+import { BaseHttpService, HttpCallInfo, HttpMethodEn } from './base-http.service';
+
+@Injectable()
+export class LockService {
+
+ constructor(
+ private baseHttpService: BaseHttpService,
+ private sessionContext: SessionContext) {
+ }
+
+ storeLock(lock: Lock): Observable<Lock> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.put, '/createLock', lock),
+ this.sessionContext);
+ }
+ deleteLock(key: number, info: string, force: string) {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.delete, '/deleteLock/'
+ + key + '/' + info + '/' + force, null),
+ this.sessionContext);
+ }
+ checkLock(key: number, info: string): Observable<Lock> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/checkLock/'
+ + key + '/' + info, null),
+ this.sessionContext);
+ }
+}
diff --git a/src/app/services/message.service.spec.ts b/src/app/services/message.service.spec.ts
new file mode 100644
index 0000000..824a0f3
--- /dev/null
+++ b/src/app/services/message.service.spec.ts
@@ -0,0 +1,57 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { TestBed, inject, fakeAsync, tick } from '@angular/core/testing';
+import { SessionContext } from './../common/session-context';
+import { MessageServiceCustom } from './message.service';
+import { BannerMessage } from '../common/banner-message';
+import { ErrorType, MessageScopeEn } from '../common/enums';
+import { USERS } from '../test-data/users';
+import { UserMap } from '../common/user-map';
+
+describe('MessageService', () => {
+ let sessionContext: SessionContext;
+ beforeEach(fakeAsync(() => {
+ sessionContext = new SessionContext();
+ sessionContext.userMap = new UserMap(USERS);
+ TestBed.configureTestingModule({
+ providers: [MessageServiceCustom,
+ { provide: SessionContext, useValue: sessionContext }
+ ]
+ }).compileComponents();
+ }));
+
+
+ it('should be created', inject([MessageServiceCustom], (service: MessageServiceCustom) => {
+ expect(service).toBeTruthy();
+ }));
+
+ it('should emit errors correctly', fakeAsync(inject([MessageServiceCustom], (service: MessageServiceCustom) => {
+ sessionContext.setUserAuthenticated(true);
+ const messages: BannerMessage[] = [];
+ service.errorOccured$.subscribe(bannerMessage => messages.push(bannerMessage));
+
+ // service.emitError('testLoc', ErrorType.authentication);
+ // service.emitError('testLoc', ErrorType.create);
+ // service.emitError('testLoc', ErrorType.delete);
+ // service.emitError('testLoc', ErrorType.retrieve);
+ // service.emitError('testLoc', ErrorType.update);
+ // service.emitError('testUser', ErrorType.locked);
+ // service.emitError('testUser', ErrorType.stornoLocked);
+ // service.emitInfo('info1', MessageScopeEn.local);
+ // service.emitWarning('warning1', MessageScopeEn.global);
+
+ tick();
+
+ expect(messages.length).toBe(0);
+
+ })));
+});
diff --git a/src/app/services/message.service.ts b/src/app/services/message.service.ts
new file mode 100644
index 0000000..0708492
--- /dev/null
+++ b/src/app/services/message.service.ts
@@ -0,0 +1,124 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Injectable, EventEmitter } from '@angular/core';
+import { BannerMessageStatusEn, ErrorType, ToasterButtonEventEn, MessageScopeEn } from '../common/enums';
+import { SessionContext } from './../common/session-context';
+import { BannerMessage } from '../common/banner-message';
+
+export class MessageDefines {
+ static MSG_LOG_IN_SUCCEEDED = 'LOG_IN_SUCCEEDED';
+ static MSG_LOG_OFF = 'LOG_OFF';
+}
+
+@Injectable()
+export class MessageServiceCustom {
+
+ private messageServiceActive: boolean;
+
+ public loginLogoff$: EventEmitter<string> = new EventEmitter<string>();
+ // public bannerButtonEvent$: EventEmitter<ToasterButtonEventEn> = new EventEmitter<ToasterButtonEventEn>();
+ public errorOccured$: EventEmitter<BannerMessage> = new EventEmitter<BannerMessage>();
+ // public clearBannerLocalEvent$: EventEmitter<boolean> = new EventEmitter<boolean>();
+ public clearDecisionBannerLocalEvent$: EventEmitter<boolean> = new EventEmitter<boolean>();
+
+ constructor(private sessionContext: SessionContext) { }
+
+ // public emitError(location: string, errorType?: ErrorType, msgScope?: MessageScopeEn, error?: any) {
+ // if (error === ErrorType.authentication) {
+ // return;
+ // }
+
+ // let message = '';
+
+ // switch (errorType) {
+ // case ErrorType.create:
+ // message = 'Fehler beim Erstellen des Objekts ' + location + '.';
+ // break;
+ // case ErrorType.update:
+ // message = 'Fehler beim Aktualiseren des Objekts ' + location + '.';
+ // break;
+ // case ErrorType.delete:
+ // message = 'Fehler beim Löschen des Objekts ' + location + '.';
+ // break;
+ // case ErrorType.retrieve:
+ // message = 'Fehler beim Zugriff auf ' + location + '.';
+ // break;
+ // case ErrorType.upload:
+ // message = 'Fehler beim Hochladen der Datei in ' + location + '.';
+ // break;
+ // case ErrorType.locked:
+ // message = 'Eintrag ist gesperrt von ' + this.sessionContext.getUserMap().findAndRenderUser(location) + '.';
+ // break;
+ // case ErrorType.datedependency:
+ // message = 'Beginn der ersten geplanten Schrittsequenz muss vor dem Enddatum der geplanten Netzmaßnahme liegen.';
+ // break;
+ // case ErrorType.stornoLocked:
+ // message = 'Eintrag ist gesperrt. Bitte versuchen Sie es später noch einmal.';
+ // break;
+ // default:
+ // message = 'Es ist ein unbekannter Fehler aufgetreten.';
+ // break;
+ // }
+
+ // this.emitMessage(message, BannerMessageStatusEn.error, msgScope, errorType);
+ // }
+
+ // public emitInfo(message: string, msgScope: MessageScopeEn) {
+ // this.emitMessage(message, BannerMessageStatusEn.info, msgScope);
+ // }
+
+ // public emitWarning(message: string, msgScope: MessageScopeEn) {
+ // this.emitMessage(message, BannerMessageStatusEn.warning, msgScope);
+ // }
+
+ // public deactivateMessage() {
+ // const bannerMessage: BannerMessage = new BannerMessage();
+ // bannerMessage.isActive = false;
+ // this.errorOccured$.emit(bannerMessage);
+ // }
+
+ // public emitDecisionMessage(message: string, msgScope: MessageScopeEn) {
+ // const bannerMessage: BannerMessage = new BannerMessage();
+ // bannerMessage.showDecisionButtons = true;
+ // // bannerMessage.showButton = true;
+ // bannerMessage.isSetTimeout = false;
+ // this.emitAdjustedMessage(bannerMessage, message, BannerMessageStatusEn.info, msgScope);
+ // }
+
+ // TODO Unlock muss hierüber emittiert werden
+ /* public emitUnlockMessage(message: string, msgScope: MessageScopeEn) {
+ const bannerMessage: BannerMessage = new BannerMessage();
+ bannerMessage.showButton = true;
+ this.emitAdjustedMessage(bannerMessage);
+ } */
+
+ // private emitAdjustedMessage(bannerMessage: BannerMessage, message: string, status: BannerMessageStatusEn, msgScope: MessageScopeEn) {
+ // bannerMessage.text = message;
+ // bannerMessage.isActive = true;
+ // bannerMessage.scope = msgScope;
+ // bannerMessage.status = status;
+ // if (!this.sessionContext.isUserAuthenticated()) { return; }
+ // this.errorOccured$.emit(bannerMessage);
+ // }
+
+ // private emitMessage(message: string, status: BannerMessageStatusEn, msgScope: MessageScopeEn, errorType?: ErrorType) {
+ // if (!this.sessionContext.isUserAuthenticated()) { return; }
+ // const bannerMessage: BannerMessage = new BannerMessage();
+ // bannerMessage.isActive = true;
+ // bannerMessage.status = status;
+ // bannerMessage.text = message;
+ // bannerMessage.errorType = errorType;
+ // bannerMessage.scope = msgScope;
+ // this.errorOccured$.emit(bannerMessage);
+ // console.log(message);
+ // }
+}
diff --git a/src/app/services/modify-grid-config.service.spec.ts b/src/app/services/modify-grid-config.service.spec.ts
new file mode 100644
index 0000000..c5d038e
--- /dev/null
+++ b/src/app/services/modify-grid-config.service.spec.ts
@@ -0,0 +1,37 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { TestBed, inject } from '@angular/core/testing';
+import { ModifyGridConfigService } from './modify-grid-config.service';
+import { SessionContext } from '../common/session-context';
+import { BaseHttpService } from './base-http.service';
+import { MockBaseHttpService } from '../testing/mock-base-http.service';
+
+describe('ModifyGridConfigService', () => {
+ let sessionContext: SessionContext;
+ let mockedBaseHttpService: MockBaseHttpService;
+ beforeEach(() => {
+ sessionContext = new SessionContext();
+ mockedBaseHttpService = new MockBaseHttpService();
+
+ TestBed.configureTestingModule({
+ providers: [ModifyGridConfigService,
+ { provide: SessionContext, useValue: sessionContext },
+ { provide: BaseHttpService, useValue: mockedBaseHttpService },
+ SessionContext
+ ]
+ });
+ });
+
+ it('should be created', inject([ModifyGridConfigService], (service: ModifyGridConfigService) => {
+ expect(service).toBeTruthy();
+ }));
+});
diff --git a/src/app/services/modify-grid-config.service.ts b/src/app/services/modify-grid-config.service.ts
new file mode 100644
index 0000000..ea6b329
--- /dev/null
+++ b/src/app/services/modify-grid-config.service.ts
@@ -0,0 +1,38 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Injectable } from '@angular/core';
+import { GridConfig } from '../model/grid-config';
+import { Observable } from 'rxjs';
+import { BaseHttpService, HttpCallInfo, HttpMethodEn } from './base-http.service';
+import { SessionContext } from '../common/session-context';
+import { Globals } from '../common/globals';
+
+@Injectable()
+export class ModifyGridConfigService {
+
+ constructor(
+ private baseHttpService: BaseHttpService,
+ private sessionContext: SessionContext) {
+ }
+
+ setGridConfig(modifyGridConfig: GridConfig): Observable<GridConfig> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/setGridConfig/?' +
+ 'skipForApproval=' + modifyGridConfig.skipForApproval + '&' +
+ 'endAfterApproved=' + modifyGridConfig.endAfterApproved + '&' +
+ 'skipRequesting=' + modifyGridConfig.skipRequesting + '&' +
+ 'endAfterReleased=' + modifyGridConfig.endAfterReleased + '&' +
+ 'skipInWork=' + modifyGridConfig.skipInWork
+ , null),
+ this.sessionContext);
+ }
+}
diff --git a/src/app/services/reminder.service.spec.ts b/src/app/services/reminder.service.spec.ts
new file mode 100644
index 0000000..359bf7d
--- /dev/null
+++ b/src/app/services/reminder.service.spec.ts
@@ -0,0 +1,63 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { TestBed, async, inject } from '@angular/core/testing';
+
+import 'rxjs/add/observable/of';
+import 'rxjs/add/operator/catch';
+import 'rxjs/add/operator/do';
+import 'rxjs/add/operator/toPromise';
+
+import { ReminderService } from './reminder.service';
+import { SessionContext } from '../common/session-context';
+import { BaseHttpService, HttpMethodEn } from './base-http.service';
+import { MockBaseHttpService } from '../testing/mock-base-http.service';
+
+describe('ReminderService', () => {
+
+ let sessionContext: SessionContext;
+ let mockedBaseHttpService: MockBaseHttpService;
+ mockedBaseHttpService = new MockBaseHttpService();
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ ReminderService,
+ SessionContext,
+ { provide: BaseHttpService, useValue: mockedBaseHttpService },
+ ]
+ })
+ .compileComponents();
+
+ sessionContext = new SessionContext();
+ }));
+
+ it('can instantiate service when inject service',
+ inject([ReminderService], (service: ReminderService) => {
+ expect(service instanceof ReminderService).toBe(true);
+ }));
+
+ it('should call getCurrentReminders', inject([ReminderService], (service: ReminderService) => {
+ expect(service).toBeTruthy();
+
+ service.getCurrentReminders();
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('getCurrentReminders');
+ }));
+
+ it('should call getExpiredReminders', inject([ReminderService], (service: ReminderService) => {
+ expect(service).toBeTruthy();
+
+ service.getExpiredReminders();
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('getExpiredReminders');
+ }));
+});
diff --git a/src/app/services/reminder.service.ts b/src/app/services/reminder.service.ts
new file mode 100644
index 0000000..6da0581
--- /dev/null
+++ b/src/app/services/reminder.service.ts
@@ -0,0 +1,41 @@
+/**
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import 'rxjs/add/operator/catch';
+import 'rxjs/add/operator/map';
+import 'rxjs/add/observable/throw';
+import { SessionContext } from '../common/session-context';
+import { BaseHttpService, HttpCallInfo, HttpMethodEn } from './base-http.service';
+import { Globals } from '../common/globals';
+
+@Injectable()
+export class ReminderService {
+
+ constructor(
+ private baseHttpService: BaseHttpService,
+ private sessionContext: SessionContext,
+ ) {
+ }
+
+ getCurrentReminders(): Observable<number[]> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/getCurrentReminders', null),
+ this.sessionContext);
+ }
+
+ getExpiredReminders(): Observable<number[]> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/getExpiredReminders', null),
+ this.sessionContext);
+ }
+}
diff --git a/src/app/services/request-interceptor.service.ts b/src/app/services/request-interceptor.service.ts
new file mode 100644
index 0000000..7d3d0d3
--- /dev/null
+++ b/src/app/services/request-interceptor.service.ts
@@ -0,0 +1,67 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+******************************************************************************
+ */
+import { SessionContext } from './../common/session-context';
+import { AuthenticationService } from './authentication.service';
+import { MessageServiceCustom, MessageDefines } from './message.service';
+import { Router } from '@angular/router';
+import { Injectable } from '@angular/core';
+import {
+ HttpInterceptor,
+ HttpRequest,
+ HttpErrorResponse,
+ HttpHandler,
+ HttpEvent
+} from '@angular/common/http';
+
+import { Observable } from 'rxjs/Observable';
+import 'rxjs/add/operator/do';
+
+@Injectable()
+export class RequestInterceptor implements HttpInterceptor {
+
+
+ constructor(private msgService: MessageServiceCustom,
+ private authService: AuthenticationService,
+ private router: Router,
+ private sessionContext: SessionContext) { }
+
+ intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
+
+ return next
+ .handle(request)
+ .do((ev: HttpEvent<any>) => {
+ // could do something with the resonse
+ /* if (ev instanceof HttpResponse) {
+ console.log('Response:', ev);
+ } */
+ }).catch(error => {
+ if (error instanceof HttpErrorResponse) {
+
+ // this.sessionContext.centralHttpResultCode$.emit(error.status);
+
+ if (error.status === 401) {
+ // redirect to the login route
+ // or show a modal
+ if (this.sessionContext.isUserAuthenticated()) {
+ this.msgService.loginLogoff$.emit(MessageDefines.MSG_LOG_OFF);
+ this.router.navigate(['/loggedout']);
+ }
+ } else if (error.status !== 302 && (error.status < 200 || error.status >= 300)) {
+ throw new Error('Bad response status: ' + error.status);
+ }
+ }
+
+ return Observable.throw(error);
+ }
+
+ );
+ }
+}
diff --git a/src/app/services/role-access.service.spec.ts b/src/app/services/role-access.service.spec.ts
new file mode 100644
index 0000000..0376f7c
--- /dev/null
+++ b/src/app/services/role-access.service.spec.ts
@@ -0,0 +1,45 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+******************************************************************************
+ */
+import { HttpClientModule } from '@angular/common/http';
+import { TestBed, async, inject } from '@angular/core/testing';
+import { RoleAccessService } from './role-access.service';
+import { SessionContext } from '../common/session-context';
+import { MockBaseHttpService } from '../testing/mock-base-http.service';
+import { BaseHttpService, HttpMethodEn } from './base-http.service';
+
+describe('Service: RoleAccess', () => {
+
+ let sessionContext: SessionContext;
+ let mockedBaseHttpService: MockBaseHttpService;
+
+ beforeEach(async(() => {
+ sessionContext = new SessionContext();
+ mockedBaseHttpService = new MockBaseHttpService();
+ TestBed.configureTestingModule({
+ imports: [HttpClientModule],
+ providers: [
+ RoleAccessService,
+ { provide: SessionContext, useValue: sessionContext },
+ { provide: BaseHttpService, useValue: mockedBaseHttpService },
+ SessionContext
+ ]
+ });
+ }));
+
+ it('should call getRoleAccessDefinition', inject([RoleAccessService], (service: RoleAccessService) => {
+ expect(service).toBeTruthy();
+
+ service.getRoleAccessDefinition();
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('getRoleAccessDefinition');
+
+ }));
+});
diff --git a/src/app/services/role-access.service.ts b/src/app/services/role-access.service.ts
new file mode 100644
index 0000000..93c2911
--- /dev/null
+++ b/src/app/services/role-access.service.ts
@@ -0,0 +1,35 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+******************************************************************************
+ */
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import 'rxjs/add/observable/throw';
+import 'rxjs/add/operator/catch';
+import 'rxjs/add/operator/map';
+import { RoleAccess } from '../model/role-access';
+import { BaseHttpService, HttpCallInfo, HttpMethodEn } from './base-http.service';
+import { Globals } from '../common/globals';
+import { SessionContext } from '../common/session-context';
+
+@Injectable()
+export class RoleAccessService {
+
+ constructor(public http: HttpClient,
+ private baseHttpService: BaseHttpService,
+ private sessionContext: SessionContext) {
+ }
+
+ getRoleAccessDefinition(): Observable<RoleAccess> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/getRoleAccessDefinition', null),
+ this.sessionContext);
+ }
+}
diff --git a/src/app/services/toaster-message.service.spec.ts b/src/app/services/toaster-message.service.spec.ts
new file mode 100644
index 0000000..47661f0
--- /dev/null
+++ b/src/app/services/toaster-message.service.spec.ts
@@ -0,0 +1,149 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { TestBed, inject } from '@angular/core/testing';
+
+import { ToasterMessageService } from './toaster-message.service';
+import { SessionContext } from '../common/session-context';
+import { MessageService } from 'primeng/api';
+import { ErrorType } from '../common/enums';
+import { UserMap } from '../common/user-map';
+import { USERS } from '../test-data/users';
+
+describe('ToasterMessageService', () => {
+ let sessionContext: SessionContext;
+ beforeEach(() => {
+ sessionContext = new SessionContext();
+ sessionContext.userMap = new UserMap(USERS);
+ TestBed.configureTestingModule({
+ providers: [ToasterMessageService,
+ { provide: SessionContext, useValue: sessionContext },
+ MessageService]
+ });
+ });
+
+ it('should be created', inject([ToasterMessageService], (service: ToasterMessageService) => {
+ expect(service).toBeTruthy();
+ }));
+
+ it('should can call showSuccess', inject([ToasterMessageService], (service: ToasterMessageService) => {
+ spyOn(service, 'showSuccess').and.callThrough();
+ service.showSuccess('testLoc', 'details');
+ expect(service.showSuccess).toHaveBeenCalled();
+ }));
+
+ it('should can call showInfo', inject([ToasterMessageService], (service: ToasterMessageService) => {
+ spyOn(service, 'showInfo').and.callThrough();
+ service.showInfo('testLoc', 'details');
+ expect(service.showInfo).toHaveBeenCalled();
+ }));
+
+ it('should can call showWarn', inject([ToasterMessageService], (service: ToasterMessageService) => {
+ spyOn(service, 'showWarn').and.callThrough();
+ service.showWarn('testLoc', 'details');
+ expect(service.showWarn).toHaveBeenCalled();
+ }));
+
+ it('should can call showTopLeft', inject([ToasterMessageService], (service: ToasterMessageService) => {
+ spyOn(service, 'showTopLeft').and.callThrough();
+ service.showTopLeft('testLoc', 'details');
+ expect(service.showTopLeft).toHaveBeenCalled();
+ }));
+
+ it('should can call showTopCenter', inject([ToasterMessageService], (service: ToasterMessageService) => {
+ spyOn(service, 'showTopCenter').and.callThrough();
+ service.showTopCenter('testLoc', 'details');
+ expect(service.showTopCenter).toHaveBeenCalled();
+ }));
+
+ it('should can call showSingleGMDeleteConfirm', inject([ToasterMessageService], (service: ToasterMessageService) => {
+ spyOn(service, 'showSingleGMDeleteConfirm').and.callThrough();
+ service.showSingleGMDeleteConfirm('testLoc', 'details');
+ expect(service.showSingleGMDeleteConfirm).toHaveBeenCalled();
+ }));
+
+ it('should can call showUnlockConfirm', inject([ToasterMessageService], (service: ToasterMessageService) => {
+ spyOn(service, 'showUnlockConfirm').and.callThrough();
+ service.showUnlockConfirm('testLoc', 'details');
+ expect(service.showUnlockConfirm).toHaveBeenCalled();
+ }));
+
+ it('should can call clear without key input', inject([ToasterMessageService], (service: ToasterMessageService) => {
+ spyOn(service, 'clear').and.callThrough();
+ service.clear();
+ expect(service.clear).toHaveBeenCalled();
+ }));
+
+ it('should can call clear with key input', inject([ToasterMessageService], (service: ToasterMessageService) => {
+ spyOn(service, 'clear').and.callThrough();
+ service.clear('c');
+ expect(service.clear).toHaveBeenCalledWith('c');
+ }));
+
+ it('should can call showError for delete', inject([ToasterMessageService], (service: ToasterMessageService) => {
+ spyOn(service, 'showError').and.callThrough();
+ service.showError(ErrorType.delete, 'testLoc');
+ expect(service.showError).toHaveBeenCalled();
+ }));
+
+ it('should can call showError for authentication', inject([ToasterMessageService], (service: ToasterMessageService) => {
+ spyOn(service, 'showError').and.callThrough();
+ service.showError(ErrorType.authentication, 'testLoc');
+ expect(service.showError).toHaveBeenCalled();
+ }));
+
+
+ it('should can call showError for create', inject([ToasterMessageService], (service: ToasterMessageService) => {
+ spyOn(service, 'showError').and.callThrough();
+ service.showError(ErrorType.create, 'testLoc');
+ expect(service.showError).toHaveBeenCalled();
+ }));
+
+ it('should can call showError for datedependency', inject([ToasterMessageService], (service: ToasterMessageService) => {
+ spyOn(service, 'showError').and.callThrough();
+ service.showError(ErrorType.datedependency, 'testLoc');
+ expect(service.showError).toHaveBeenCalled();
+ }));
+
+
+ it('should can call showError for locked', inject([ToasterMessageService], (service: ToasterMessageService) => {
+ sessionContext.setUserAuthenticated(true);
+ spyOn(service, 'showError').and.callThrough();
+ service.showError(ErrorType.locked, 'testLoc');
+ expect(service.showError).toHaveBeenCalled();
+ }));
+
+ it('should can call showError for retrieve', inject([ToasterMessageService], (service: ToasterMessageService) => {
+ spyOn(service, 'showError').and.callThrough();
+ service.showError(ErrorType.retrieve, 'testLoc');
+ expect(service.showError).toHaveBeenCalled();
+ }));
+
+
+ it('should can call showError for stornoLocked', inject([ToasterMessageService], (service: ToasterMessageService) => {
+ spyOn(service, 'showError').and.callThrough();
+ service.showError(ErrorType.stornoLocked, 'testLoc');
+ expect(service.showError).toHaveBeenCalled();
+ }));
+
+ it('should can call showError for update', inject([ToasterMessageService], (service: ToasterMessageService) => {
+ spyOn(service, 'showError').and.callThrough();
+ service.showError(ErrorType.update, 'testLoc');
+ expect(service.showError).toHaveBeenCalled();
+ }));
+
+ it('should can call showError for upload', inject([ToasterMessageService], (service: ToasterMessageService) => {
+ spyOn(service, 'showError').and.callThrough();
+ service.showError(ErrorType.upload, 'testLoc');
+ expect(service.showError).toHaveBeenCalled();
+ }));
+
+});
diff --git a/src/app/services/toaster-message.service.ts b/src/app/services/toaster-message.service.ts
new file mode 100644
index 0000000..96508cb
--- /dev/null
+++ b/src/app/services/toaster-message.service.ts
@@ -0,0 +1,102 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Injectable, EventEmitter } from '@angular/core';
+import { MessageService } from 'primeng/api';
+import { ErrorType, ToasterButtonEventEn } from '../common/enums';
+import { SessionContext } from '../common/session-context';
+
+@Injectable()
+export class ToasterMessageService {
+
+ public toasterEmitter$: EventEmitter<ToasterButtonEventEn> = new EventEmitter<ToasterButtonEventEn>();
+
+ constructor(private sessionContext: SessionContext,
+ private messageService: MessageService) {
+
+ }
+
+ showSuccess(summaryText: string, detailText?: string) {
+ this.messageService.add({ life: 6000, severity: 'success', summary: summaryText, detail: detailText });
+ }
+
+ showInfo(summaryText: string, detailText?: string) {
+ this.messageService.add({ life: 6000, severity: 'info', summary: summaryText, detail: detailText });
+ }
+
+ showWarn(summaryText: string, detailText?: string) {
+ this.messageService.add({ life: 6000, severity: 'warn', summary: summaryText, detail: detailText });
+ }
+
+
+
+ showError(errorType: ErrorType, location: string, detailText?: string) {
+ let message = '';
+ switch (errorType) {
+ case ErrorType.create:
+ message = 'Fehler beim Erstellen des Objekts ' + location + '.';
+ break;
+ case ErrorType.update:
+ message = 'Fehler beim Aktualiseren des Objekts ' + location + '.';
+ break;
+ case ErrorType.delete:
+ message = 'Fehler beim Löschen des Objekts ' + location + '.';
+ break;
+ case ErrorType.retrieve:
+ message = 'Fehler beim Zugriff auf ' + location + '.';
+ break;
+ case ErrorType.upload:
+ message = 'Fehler beim Hochladen der Datei in ' + location + '.';
+ break;
+ case ErrorType.locked:
+ message = 'Eintrag ist gesperrt von ' + this.sessionContext.getUserMap().findAndRenderUser(location) + '.';
+ break;
+ case ErrorType.datedependency:
+ message = 'Beginn der ersten geplanten Schrittsequenz muss vor dem Enddatum der geplanten Netzmaßnahme liegen.';
+ break;
+ case ErrorType.stornoLocked:
+ message = 'Eintrag ist gesperrt. Bitte versuchen Sie es später noch einmal.';
+ break;
+ default:
+ message = 'Es ist ein unbekannter Fehler aufgetreten.';
+ break;
+ }
+
+ this.messageService.add({ severity: 'error', sticky: true, summary: message, detail: detailText });
+ }
+
+ showTopLeft(summaryText: string, detailText?: string) {
+ this.messageService.add({ life: 6000, key: 'tl', severity: 'info', summary: summaryText, detail: detailText });
+ }
+
+ showTopCenter(summaryText: string, detailText?: string) {
+ this.messageService.add({ life: 6000, key: 'tc', severity: 'warn', summary: summaryText, detail: detailText });
+ }
+
+ showSingleGMDeleteConfirm(summaryText: string, detailText?: string) {
+ this.messageService.clear();
+ this.messageService.add({ key: 'deletec', sticky: true, severity: 'warn', summary: summaryText, detail: detailText });
+ }
+
+ showUnlockConfirm(summaryText: string, detailText?: string) {
+ this.messageService.clear();
+ this.messageService.add({ key: 'unlockc', sticky: true, severity: 'warn', summary: summaryText, detail: detailText });
+ }
+
+ clear(tmp?: string) {
+ if (tmp) {
+ this.messageService.clear(tmp);
+ } else {
+ this.messageService.clear();
+ }
+ }
+
+}
diff --git a/src/app/services/user-settings.service.spec.ts b/src/app/services/user-settings.service.spec.ts
new file mode 100644
index 0000000..d0fc1af
--- /dev/null
+++ b/src/app/services/user-settings.service.spec.ts
@@ -0,0 +1,57 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { BaseHttpService, HttpMethodEn } from './base-http.service';
+import { TestBed, async, inject } from '@angular/core/testing';
+import { UserSettingsService } from './user-settings.service';
+import { AbstractMockObservableService } from '../testing/abstract-mock-observable.service';
+import { MockBaseHttpService } from '../testing/mock-base-http.service';
+import { SessionContext } from '../common/session-context';
+import { UserSettings } from '../model/user-settings';
+
+describe('Service: UserSettings', () => {
+ class MockService extends AbstractMockObservableService {
+
+ getUserSettings() {
+ return this;
+ }
+ }
+ let sessionContext: SessionContext;
+ let mockService;
+ let mockedBaseHttpService;
+
+ beforeEach(async(() => {
+ sessionContext = new SessionContext();
+ mockedBaseHttpService = new MockBaseHttpService();
+ mockService = new MockService();
+ TestBed.configureTestingModule({
+ providers: [
+ UserSettingsService,
+ SessionContext,
+ { provide: SessionContext, useValue: sessionContext },
+ { provide: BaseHttpService, useValue: mockedBaseHttpService }
+ ]
+ });
+ }));
+
+ it('should ...', inject([UserSettingsService], (service: UserSettingsService) => {
+ expect(service).toBeTruthy();
+ }));
+
+ it('should call setUserSettings', inject([UserSettingsService], (service: UserSettingsService) => {
+ expect(service).toBeTruthy();
+
+ service.setUserSettings(new UserSettings);
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.put);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toContain('storeUserSettings');
+
+ }));
+});
diff --git a/src/app/services/user-settings.service.ts b/src/app/services/user-settings.service.ts
new file mode 100644
index 0000000..d80afa1
--- /dev/null
+++ b/src/app/services/user-settings.service.ts
@@ -0,0 +1,41 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Injectable } from '@angular/core';
+import { UserSettings } from '../model/user-settings';
+import { SessionContext } from '../common/session-context';
+import { BaseHttpService, HttpMethodEn, HttpCallInfo } from './base-http.service';
+import { Observable } from 'rxjs/Observable';
+import { Globals } from '../common/globals';
+
+@Injectable()
+export class UserSettingsService {
+
+ constructor(private baseHttpService: BaseHttpService,
+ private sessionContext: SessionContext) { }
+ setUserSettings(userSettings: UserSettings): any {
+ const settingsPayload = { ...userSettings, value: JSON.stringify(userSettings.value) };
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.put, '/storeUserSettings', settingsPayload)
+ , this.sessionContext);
+ }
+ getUserSettings(settingType: string): Observable<UserSettings> {
+ return this.baseHttpService.callService(
+
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/getUserSettings/' + settingType, null)
+ , this.sessionContext)
+ .map((res) => {
+ const userSettings = <UserSettings>res;
+
+ return { ...userSettings, value: userSettings.value ? JSON.parse(userSettings.value.toString()) : userSettings.value };
+ });
+ }
+}
diff --git a/src/app/services/user.service.spec.ts b/src/app/services/user.service.spec.ts
new file mode 100644
index 0000000..65fc761
--- /dev/null
+++ b/src/app/services/user.service.spec.ts
@@ -0,0 +1,52 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { TestBed, async } from '@angular/core/testing';
+import { Globals } from '../common/globals';
+import { SessionContext } from '../common/session-context';
+import { USERS } from '../test-data/users';
+import { MockBaseHttpService } from '../testing/mock-base-http.service';
+import { HttpMethodEn } from './base-http.service';
+import { UserService } from './user.service';
+
+
+
+describe('Http-UserService (mockBackend)', () => {
+ let mockedBaseService: any;
+
+ let sessionContext: SessionContext;
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ SessionContext
+ ]
+ })
+ .compileComponents();
+ sessionContext = new SessionContext();
+ mockedBaseService = new MockBaseHttpService();
+ }));
+
+ it('create a correct envelope when called', () => {
+ const userService: UserService = new UserService(mockedBaseService, sessionContext);
+ const userResult = USERS;
+ mockedBaseService.content = userResult;
+
+ userService.getUsers().subscribe(users => {
+ expect(users.length).toBe(userResult.length);
+ expect(mockedBaseService.lastHttpCallInfo).toBeTruthy();
+ expect(mockedBaseService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseService.lastHttpCallInfo.serviceName).toBe(Globals.AUTH_AND_AUTH_SERVICE_NAME);
+ expect(mockedBaseService.lastHttpCallInfo.uriFragment).toBe('/users');
+ },
+ error => expect('No Error').toBe('Where an Error occured')
+ );
+ });
+});
diff --git a/src/app/services/user.service.ts b/src/app/services/user.service.ts
new file mode 100644
index 0000000..7c0af53
--- /dev/null
+++ b/src/app/services/user.service.ts
@@ -0,0 +1,33 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import { BaseHttpService, HttpCallInfo, HttpMethodEn } from './base-http.service';
+import { SessionContext } from '../common/session-context';
+import { Globals } from '../common/globals';
+import { User } from '../model/user';
+
+
+@Injectable()
+export class UserService {
+
+
+ constructor(
+ private baseHttpService: BaseHttpService,
+ private sessionContext: SessionContext) { }
+
+ public getUsers(): Observable<User[]> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.AUTH_AND_AUTH_SERVICE_NAME, HttpMethodEn.get, '/users', null), this.sessionContext);
+ }
+
+}
diff --git a/src/app/services/version-info.service.spec.ts b/src/app/services/version-info.service.spec.ts
new file mode 100644
index 0000000..5f576ec
--- /dev/null
+++ b/src/app/services/version-info.service.spec.ts
@@ -0,0 +1,46 @@
+/*
+ ******************************************************************************
+ * Copyright © 2018 PTA GmbH.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ *
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ ******************************************************************************
+ */
+import { TestBed, inject } from '@angular/core/testing';
+import { BaseHttpService, HttpMethodEn } from './base-http.service';
+import { VersionInfoService } from './version-info.service';
+import { MockBaseHttpService } from '../testing/mock-base-http.service';
+import { SessionContext } from '../common/session-context';
+
+
+describe('Service: VersionInfo', () => {
+ let sessionContext: SessionContext;
+ let mockedBaseHttpService: MockBaseHttpService;
+
+ beforeEach(() => {
+ sessionContext = new SessionContext();
+ mockedBaseHttpService = new MockBaseHttpService();
+
+ TestBed.configureTestingModule({
+ providers: [
+ VersionInfoService,
+ { provide: SessionContext, useValue: sessionContext },
+ { provide: BaseHttpService, useValue: mockedBaseHttpService },
+ SessionContext
+ ]
+ });
+ });
+
+ it('should call getVersionInfo', inject([VersionInfoService], (service: VersionInfoService) => {
+ expect(service).toBeTruthy();
+
+ service.loadBackendServerInfo();
+ expect(mockedBaseHttpService.lastHttpCallInfo.method).toBe(HttpMethodEn.get);
+ expect(mockedBaseHttpService.lastHttpCallInfo.uriFragment).toBe('/versionInfo');
+
+ }));
+});
+
diff --git a/src/app/services/version-info.service.ts b/src/app/services/version-info.service.ts
new file mode 100644
index 0000000..cc449e0
--- /dev/null
+++ b/src/app/services/version-info.service.ts
@@ -0,0 +1,32 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import { SessionContext } from '../common/session-context';
+import { BaseHttpService, HttpCallInfo, HttpMethodEn } from './base-http.service';
+import { VersionInfo } from '../model/version-info';
+import { Globals } from '../common/globals';
+
+@Injectable()
+export class VersionInfoService {
+
+ constructor(
+ private baseHttpService: BaseHttpService,
+ private sessionContext: SessionContext
+ ) {
+ }
+
+ public loadBackendServerInfo(): Observable<VersionInfo> {
+ return this.baseHttpService.callService(
+ new HttpCallInfo(Globals.GRID_MEASURES_SERVICE_NAME, HttpMethodEn.get, '/versionInfo', null), this.sessionContext);
+ }
+}
diff --git a/src/app/test-data/backend-settings.ts b/src/app/test-data/backend-settings.ts
new file mode 100644
index 0000000..88ce394
--- /dev/null
+++ b/src/app/test-data/backend-settings.ts
@@ -0,0 +1,34 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { BackendSettings } from '../model/backend-settings';
+
+export const BACKENDSETTINGS: BackendSettings = {
+ reminderPeriod: 48,
+ appointmentRepetition: [
+ {
+ id: 1,
+ name: 'täglich'
+ },
+ {
+ id: 2,
+ name: 'wöchentlich'
+ },
+ {
+ id: 3,
+ name: 'monatlich'
+ },
+ {
+ id: 4,
+ name: 'jährlich'
+ }
+ ]
+};
diff --git a/src/app/test-data/branches.ts b/src/app/test-data/branches.ts
new file mode 100644
index 0000000..731f14a
--- /dev/null
+++ b/src/app/test-data/branches.ts
@@ -0,0 +1,19 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Branch } from '../model/branch';
+
+export const BRANCHES: Branch[] = [
+ { 'id': 1, 'name': 'S', 'description': 'Strom', 'colorCode': '' },
+ { 'id': 2, 'name': 'G', 'description': 'Gas', 'colorCode': '' },
+ { 'id': 4, 'name': 'W', 'description': 'Wasser', 'colorCode': '' },
+ { 'id': 3, 'name': 'F', 'description': 'Fernwärme', 'colorCode': '' }
+];
diff --git a/src/app/test-data/cim-cache-responses.ts b/src/app/test-data/cim-cache-responses.ts
new file mode 100644
index 0000000..44c34ad
--- /dev/null
+++ b/src/app/test-data/cim-cache-responses.ts
@@ -0,0 +1,364 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+export const RESSOURCEWITHTYPERESPONSEEMPTYLIST = `
+<ResponseMessage>
+ <Header>
+ <Verb>reply</Verb>
+ <Noun>PowerSystemResources</Noun>
+ <Revision>1</Revision>
+ <Timestamp>2018-07-09T07:35:02.144Z</Timestamp>
+ <Source>DynamicTopologyService</Source>
+ <User>
+ <UserID></UserID>
+ </User>
+ <MessageID>3e3d3b6f-e765-4398-aa03-68a4dcc0fffb</MessageID>
+ </Header>
+ <Reply>
+ <Result>OK</Result>
+ </Reply>
+ <Payload>
+ <PowerSystemResources>
+ </PowerSystemResources>
+ </Payload>
+</ResponseMessage>
+`;
+
+export const RESSOURCEWITHTYPERESPONSE = `
+<ResponseMessage>
+ <Header>
+ <Verb>reply</Verb>
+ <Noun>PowerSystemResources</Noun>
+ <Revision>1</Revision>
+ <Timestamp>2018-07-09T07:35:02.144Z</Timestamp>
+ <Source>DynamicTopologyService</Source>
+ <User>
+ <UserID></UserID>
+ </User>
+ <MessageID>3e3d3b6f-e765-4398-aa03-68a4dcc0fffb</MessageID>
+ </Header>
+ <Reply>
+ <Result>OK</Result>
+ </Reply>
+ <Payload>
+ <PowerSystemResources>
+ <PowerTransformer>
+ <description>PowerTransformer (abddaf44-13e6-46a3-8f87-0d675ea78659)</description>
+ <mRID>abddaf44-13e6-46a3-8f87-0d675ea78659</mRID>
+ <name>PowerTransformer</name>
+ <Terminals>
+ <Terminal>
+ <mRID>22648ddb-9ef9-471c-86e9-9e653354b6b9</mRID>
+ <name>Terminal 1</name>
+ <ConnectivityNode>
+ <ConnectivityNode>
+ <mRID>547146dd-d4aa-48d0-bf1c-d41b3048a372</mRID>
+ </ConnectivityNode>
+ </ConnectivityNode>
+ </Terminal>
+ <Terminal>
+ <mRID>cae79c4c-b877-4382-becb-1438595dbe1a</mRID>
+ <name>Terminal 2</name>
+ <ConnectivityNode>
+ <ConnectivityNode>
+ <mRID>d461607f-f4bb-407a-9be8-1575eefacfb6</mRID>
+ </ConnectivityNode>
+ </ConnectivityNode>
+ </Terminal>
+ </Terminals>
+ <vectorGroup>Dyn1</vectorGroup>
+ <PowerTransformerEnd>
+ <PowerTransformerEnd>
+ <BaseVoltage>
+ <BaseVoltage>
+ <mRID>ea4cf040-cb88-4978-a3a4-630a8f144e4b</mRID>
+ </BaseVoltage>
+ </BaseVoltage>
+ <Terminal>
+ <Terminal>
+ <mRID>22648ddb-9ef9-471c-86e9-9e653354b6b9</mRID>
+ </Terminal>
+ </Terminal>
+ <ratedS>
+ <multiplier>none</multiplier>
+ <unit>VA</unit>
+ <value>20000.0</value>
+ </ratedS>
+ <ratedU>
+ <multiplier>none</multiplier>
+ <unit>V</unit>
+ <value>10000.0</value>
+ </ratedU>
+ </PowerTransformerEnd>
+ <PowerTransformerEnd>
+ <RatioTapChanger>
+ <RatioTapChanger>
+ <highStep>40</highStep>
+ <lowStep>5</lowStep>
+ <normalStep>5</normalStep>
+ <step>20.0</step>
+ <stepVoltageIncrement>
+ <multiplier>none</multiplier>
+ <unit>none</unit>
+ <value>30.0</value>
+ </stepVoltageIncrement>
+ </RatioTapChanger>
+ </RatioTapChanger>
+ <BaseVoltage>
+ <BaseVoltage>
+ <mRID>9542f9ef-9a5f-47de-aff5-33f04f362668</mRID>
+ </BaseVoltage>
+ </BaseVoltage>
+ <Terminal>
+ <Terminal>
+ <mRID>cae79c4c-b877-4382-becb-1438595dbe1a</mRID>
+ </Terminal>
+ </Terminal>
+ <ratedS>
+ <multiplier>none</multiplier>
+ <unit>VA</unit>
+ <value>20000.0</value>
+ </ratedS>
+ <ratedU>
+ <multiplier>none</multiplier>
+ <unit>V</unit>
+ <value>400.0</value>
+ </ratedU>
+ </PowerTransformerEnd>
+ </PowerTransformerEnd>
+ </PowerTransformer>
+ </PowerSystemResources>
+ </Payload>
+</ResponseMessage>
+`;
+
+export const RESSOURCEWITHTYPERESPONSE_2 = `
+<ResponseMessage>
+<Header>
+ <Verb>reply</Verb>
+ <Noun>PowerSystemResources</Noun>
+ <Revision>1</Revision>
+ <Timestamp>2018-07-09T09:15:56.986Z</Timestamp>
+ <Source>DynamicTopologyService</Source>
+ <User>
+ <UserID></UserID>
+ </User>
+ <MessageID>d0383f7e-e89f-4291-847c-6a5007aac5ae</MessageID>
+</Header>
+<Reply>
+ <Result>OK</Result>
+</Reply>
+<Payload>
+ <PowerSystemResources>
+ <ACLineSegment>
+ <mRID>ebb8f129-3e4f-4200-bb96-ba384c911c83</mRID>
+ <name>South ACLineSegment</name>
+ <BaseVoltage>
+ <BaseVoltage>
+ <mRID>9542f9ef-9a5f-47de-aff5-33f04f362668</mRID>
+ </BaseVoltage>
+ </BaseVoltage>
+ <Terminals>
+ <Terminal>
+ <mRID>ae19e738-59ce-466d-840d-cafb06b4ad16</mRID>
+ <name>Terminal 1</name>
+ <ConnectivityNode>
+ <ConnectivityNode>
+ <mRID>49079d8e-9478-4560-a958-9dae258254e2</mRID>
+ </ConnectivityNode>
+ </ConnectivityNode>
+ </Terminal>
+ <Terminal>
+ <mRID>78b3a4ad-154d-4ace-9fa9-d881b38debb2</mRID>
+ <name>Terminal 2</name>
+ <ConnectivityNode>
+ <ConnectivityNode>
+ <mRID>ed79abaf-10f9-4bc7-93d9-64ab205fcf5d</mRID>
+ </ConnectivityNode>
+ </ConnectivityNode>
+ </Terminal>
+ </Terminals>
+ <length>
+ <multiplier>none</multiplier>
+ <unit>m</unit>
+ <value>500000.0</value>
+ </length>
+ <PerLengthImpedance>
+ <PerLengthSequenceImpedance>
+ <WireInfos>
+ <WireInfo>
+ <ratedCurrent>
+ <multiplier>none</multiplier>
+ <unit>A</unit>
+ <value>2.0</value>
+ </ratedCurrent>
+ </WireInfo>
+ </WireInfos>
+ <r>
+ <multiplier>none</multiplier>
+ <unit>ohm</unit>
+ <value>0.5</value>
+ </r>
+ <x>
+ <multiplier>none</multiplier>
+ <unit>ohm</unit>
+ <value>0.3</value>
+ </x>
+ </PerLengthSequenceImpedance>
+ </PerLengthImpedance>
+ </ACLineSegment>
+ <ACLineSegment>
+ <mRID>1efdad09-d26f-4f1b-be82-7f63ef474c42</mRID>
+ <name>North ACLineSegment</name>
+ <BaseVoltage>
+ <BaseVoltage>
+ <mRID>9542f9ef-9a5f-47de-aff5-33f04f362668</mRID>
+ </BaseVoltage>
+ </BaseVoltage>
+ <Terminals>
+ <Terminal>
+ <mRID>d4b3980f-8cf5-4840-aed3-e23c5dd2689a</mRID>
+ <name>Terminal 1</name>
+ <ConnectivityNode>
+ <ConnectivityNode>
+ <mRID>23fe5dd0-7969-4d23-9b91-05db9598186e</mRID>
+ </ConnectivityNode>
+ </ConnectivityNode>
+ </Terminal>
+ <Terminal>
+ <mRID>ed4cbd93-fefb-48c9-abf4-e94e2f3b8ac4</mRID>
+ <name>Terminal 2</name>
+ <ConnectivityNode>
+ <ConnectivityNode>
+ <mRID>49079d8e-9478-4560-a958-9dae258254e2</mRID>
+ </ConnectivityNode>
+ </ConnectivityNode>
+ </Terminal>
+ </Terminals>
+ <length>
+ <multiplier>none</multiplier>
+ <unit>m</unit>
+ <value>500000.0</value>
+ </length>
+ <PerLengthImpedance>
+ <PerLengthSequenceImpedance>
+ <WireInfos>
+ <WireInfo>
+ <ratedCurrent>
+ <multiplier>none</multiplier>
+ <unit>A</unit>
+ <value>2.0</value>
+ </ratedCurrent>
+ </WireInfo>
+ </WireInfos>
+ <r>
+ <multiplier>none</multiplier>
+ <unit>ohm</unit>
+ <value>0.5</value>
+ </r>
+ <x>
+ <multiplier>none</multiplier>
+ <unit>ohm</unit>
+ <value>0.3</value>
+ </x>
+ </PerLengthSequenceImpedance>
+ </PerLengthImpedance>
+ </ACLineSegment>
+ </PowerSystemResources>
+</Payload>
+</ResponseMessage>
+`;
+
+export const RESSOURCETYPESRESPONSE = `
+<ResponseMessage>
+ <Header>
+ <Verb>reply</Verb>
+ <Noun>PowerSystemResourceTypes</Noun>
+ <Revision>1</Revision>
+ <Timestamp>2018-07-09T07:54:24.275Z</Timestamp>
+ <Source>DynamicTopologyService</Source>
+ <User>
+ <UserID></UserID>
+ </User>
+ <MessageID>c99f30dd-62a9-4461-91fa-9d654fd6ef91</MessageID>
+ </Header>
+ <Reply>
+ <Result>OK</Result>
+ </Reply>
+ <Payload>
+ <PowerSystemResourceTypes>
+ <PSRType>
+ <name>ac-line-segment</name>
+ </PSRType>
+ <PSRType>
+ <name>base-voltage</name>
+ </PSRType>
+ <PSRType>
+ <name>bay</name>
+ </PSRType>
+ <PSRType>
+ <name>breaker</name>
+ </PSRType>
+ <PSRType>
+ <name>busbar-section</name>
+ </PSRType>
+ <PSRType>
+ <name>disconnector</name>
+ </PSRType>
+ <PSRType>
+ <name>earth-fault-compensator</name>
+ </PSRType>
+ <PSRType>
+ <name>energy-consumer</name>
+ </PSRType>
+ <PSRType>
+ <name>energy-source</name>
+ </PSRType>
+ <PSRType>
+ <name>geographical-region</name>
+ </PSRType>
+ <PSRType>
+ <name>ground</name>
+ </PSRType>
+ <PSRType>
+ <name>grounding-impedance</name>
+ </PSRType>
+ <PSRType>
+ <name>junction</name>
+ </PSRType>
+ <PSRType>
+ <name>line-type</name>
+ </PSRType>
+ <PSRType>
+ <name>load-break-switch</name>
+ </PSRType>
+ <PSRType>
+ <name>petersen-coil</name>
+ </PSRType>
+ <PSRType>
+ <name>plant</name>
+ </PSRType>
+ <PSRType>
+ <name>power-transformer</name>
+ </PSRType>
+ <PSRType>
+ <name>sub-geographical-region</name>
+ </PSRType>
+ <PSRType>
+ <name>substation-type</name>
+ </PSRType>
+ <PSRType>
+ <name>voltage-level</name>
+ </PSRType>
+ </PowerSystemResourceTypes>
+ </Payload>
+</ResponseMessage>
+`;
diff --git a/src/app/test-data/datestringarrays.ts b/src/app/test-data/datestringarrays.ts
new file mode 100644
index 0000000..0e5318a
--- /dev/null
+++ b/src/app/test-data/datestringarrays.ts
@@ -0,0 +1,36 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+export const INPUTDATESTRINGARRAY0: string[] = [];
+
+export const INPUTDATESTRINGARRAY1: string[] = ['', ''];
+
+export const INPUTDATESTRINGARRAY2: string[] = ['', 'q'];
+
+export const INPUTDATESTRINGARRAY3: string[] = ['', '2017-01-15T11:11:00z'];
+
+export const INPUTDATESTRINGARRAY4: string[] = ['2017-01-16T11:11:00z', ''];
+
+export const INPUTDATESTRINGARRAY5: string[] = [
+ '2017-01-15T11:11:00z',
+ '2017-01-15T12:11:00z',
+ '2017-02-15T11:11:00z',
+ '2017-01-01T11:11:00z'];
+
+export const INPUTDATESTRINGARRAY6: string[] = [
+ '2017-01-15T11:11:00z',
+ '2017-01-15T12:11:00z',
+ '2016-02-15T11:11:00z',
+ '2017-01-01T11:11:00z',
+ '2017-01-01T11:11:00z',
+ '2018-01-01T11:11:00z'];
+
diff --git a/src/app/test-data/documents.ts b/src/app/test-data/documents.ts
new file mode 100644
index 0000000..87d1d4b
--- /dev/null
+++ b/src/app/test-data/documents.ts
@@ -0,0 +1,18 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Document } from '../model/document';
+
+export const DOCUMENTS: Document[] = [
+ { 'id': 1, 'documentName': 'test.txt', data: '' },
+ { 'id': 2, 'documentName': 'test6.txt', data: '' }
+
+];
diff --git a/src/app/test-data/filtering-search-text.ts b/src/app/test-data/filtering-search-text.ts
new file mode 100644
index 0000000..e7a4bd8
--- /dev/null
+++ b/src/app/test-data/filtering-search-text.ts
@@ -0,0 +1,14 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+
+export const FILTERINGSEARCHTEXT = { branchId: 'G', title: '', statusId: '' };
diff --git a/src/app/test-data/grid-measures.ts b/src/app/test-data/grid-measures.ts
new file mode 100644
index 0000000..ed79f4e
--- /dev/null
+++ b/src/app/test-data/grid-measures.ts
@@ -0,0 +1,124 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { GridMeasure } from '../model/grid-measure';
+
+export const GRIDMEASURE: GridMeasure[] = [
+
+ {
+ id: 1111,
+ descriptiveId: 'descriptiveId1',
+ branchId: 1,
+ plannedStarttimeFirstSinglemeasure: '2017-01-15T11:11:00z',
+ plannedStarttimeFirstSequence: '2017-01-15T11:11:00z',
+ plannedEndtimeLastSinglemeasure: '2017-01-15T11:11:00z',
+ plannedEndtimeGridmeasure: '2017-01-16T11:11:00z',
+ title: 'T1',
+ affectedResource: 'TESTRESOURCE1',
+ remark: 'TESTREMARK1',
+ createUser: 'Tim Fischer',
+ createUserDepartment: 'Abteilung A',
+ statusId: 1,
+ costCenter: 'Kostenstelle',
+ approvalBy: 'Freigabe durch',
+ areaOfSwitching: 'Gebiet (Region) der Schaltung',
+ appointmentRepetition: 'Terminwiederholung täglich',
+ appointmentStartdate: '2017-01-15T11:11:00z',
+ appointmentNumberOf: 3,
+ timeOfReallocation: '2017-01-15T11:11:00z',
+ modUserDepartment: 'Meisterabteilung',
+ description: 'BeschreibungText',
+ listSingleGridmeasures: [
+ {
+ id: 3, title: 'title 1', sortorder: 11, gridmeasureId: 1111, listSteps: [
+ { id: 1, sortorder: 1, switchingObject: 'Schalter 1', targetState: 'aus', singleGridmeasureId: 3, delete: false },
+ { id: 2, sortorder: 2, switchingObject: 'Schalter 2', targetState: 'aus', singleGridmeasureId: 3, delete: false },
+ { id: 3, sortorder: 3, switchingObject: 'Schalter 3', targetState: 'aus', singleGridmeasureId: 3, delete: false }
+ ]
+ },
+ {
+ id: 5, title: 'title 2', sortorder: 35, gridmeasureId: 1111, listSteps: [
+ { id: 4, sortorder: 1, switchingObject: 'Schalter 42', targetState: 'aus', singleGridmeasureId: 5, delete: false },
+ { id: 5, sortorder: 2, switchingObject: 'Schalter 43', targetState: 'aus', singleGridmeasureId: 5, delete: false },
+ { id: 6, sortorder: 3, switchingObject: 'Schalter 44', targetState: 'aus', singleGridmeasureId: 5, delete: false },
+ { id: 7, sortorder: 4, switchingObject: 'Schalter 45', targetState: 'aus', singleGridmeasureId: 5, delete: false }
+ ]
+ }
+ ],
+ emailAddresses: 'test@test.de;test2@test.de',
+ listEmailDistribution: [
+ { id: 1, emailAddress: 'test@test.de', _isValide: true, delete: false },
+ { id: 2, emailAddress: 'test2@test.de', _isValide: true, delete: false }
+ ]
+
+ },
+ {
+ id: 2222,
+ branchId: 2,
+ descriptiveId: 'descriptiveId2',
+ plannedStarttimeFirstSinglemeasure: '2017-01-15T11:11:00z',
+ title: 'T2',
+ affectedResource: 'TESTRESOURCE2',
+ remark: 'TESTREMARK2',
+ createUser: 'Max Maye',
+ createUserDepartment: '',
+ statusId: 2,
+ listSingleGridmeasures: [{ id: 3, title: 'title' }, { id: 5, title: 'title' }],
+ emailAddresses: 'test@test.de;test2@test.de',
+ listEmailDistribution: [
+ { id: 1, emailAddress: 'test@test.de', _isValide: true, delete: false },
+ { id: 2, emailAddress: 'test2@test.de', _isValide: true, delete: false }
+ ]
+ },
+ {
+ id: 3333,
+ descriptiveId: 'descriptiveId3',
+ branchId: 3,
+ plannedStarttimeFirstSinglemeasure: '2017-01-15T11:11:00z',
+ title: 'Rohr warten',
+ affectedResource: 'Rohr',
+ remark: '',
+ createUser: 'Tim Tester',
+ createUserDepartment: 'Wasser',
+ statusId: 3,
+ listSingleGridmeasures: [{ id: 3, title: 'title' }, { id: 5, title: 'title' }],
+ emailAddresses: 'test@test.de;test2@test.de',
+ listEmailDistribution: [
+ { id: 1, emailAddress: 'test@test.de', _isValide: true, delete: false },
+ { id: 2, emailAddress: 'test2@test.de', _isValide: true, delete: false }
+ ]
+ },
+ {
+ id: 4444,
+ descriptiveId: 'descriptiveId4',
+ branchId: 3,
+ title: 'Keine Zeit zum Warten',
+ affectedResource: 'Zeit',
+ remark: 'Teste die Zeiten',
+ createUser: 'Tom Tester',
+ createUserDepartment: 'Wasser',
+ statusId: 1,
+ costCenter: 'Kostenstelle',
+ approvalBy: 'Freigabe durch',
+ areaOfSwitching: 'Gebiet (Region) der Schaltung',
+ appointmentRepetition: 'Terminwiederholung täglich',
+ appointmentNumberOf: 3,
+ modUserDepartment: 'Meisterabteilung',
+ description: 'BeschreibungText',
+ listSingleGridmeasures: [{ id: 3, title: 'title' }, { id: 5, title: 'title' }],
+ emailAddresses: 'test@test.de;test2@test.de',
+ listEmailDistribution: [
+ { id: 1, emailAddress: 'test@test.de', _isValide: true, delete: false },
+ { id: 2, emailAddress: 'test2@test.de', _isValide: true, delete: false }
+ ]
+ }
+
+];
diff --git a/src/app/test-data/locks.ts b/src/app/test-data/locks.ts
new file mode 100644
index 0000000..b3a6e2c
--- /dev/null
+++ b/src/app/test-data/locks.ts
@@ -0,0 +1,17 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Lock } from '../model/lock';
+
+export const LOCKS: Lock[] = [
+ { id: 1, key: 1, username: 'hugo', info: 'gridmeasure' },
+ { id: 2, key: 2, username: 'otto', info: 'gridmeasure' }
+];
diff --git a/src/app/test-data/power-system-resources.ts b/src/app/test-data/power-system-resources.ts
new file mode 100644
index 0000000..9c50818
--- /dev/null
+++ b/src/app/test-data/power-system-resources.ts
@@ -0,0 +1,30 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { PowerSystemResource } from '../model/power-system-resource';
+
+export const POWERSYSTEMRESOURCES: PowerSystemResource[] = [
+ {
+ cimId: '"abddaf44-13e6-46a3-8f87-0d675ea78659"',
+ cimName: 'PowerTransformer',
+ cimDescription: 'desc 1'
+ },
+ {
+ cimId: '"abddaf44-13e6-46a3-8f87-0d675ea78659"',
+ cimName: 'PowerTransformer',
+ cimDescription: 'desc 2'
+ },
+ {
+ cimId: '"abddaf44-13e6-46a3-8f87-0d675ea78659"',
+ cimName: 'PowerTransformer',
+ cimDescription: 'desc 3'
+ }
+];
diff --git a/src/app/test-data/role-access-definition.ts b/src/app/test-data/role-access-definition.ts
new file mode 100644
index 0000000..9d76379
--- /dev/null
+++ b/src/app/test-data/role-access-definition.ts
@@ -0,0 +1,177 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+******************************************************************************
+ */
+
+import { RoleAccess } from '../model/role-access';
+
+export const ROLE_ACCESS: RoleAccess = {
+ editRoles: [{
+ name: 'planned-policies-measureplanner',
+ gridMeasureStatusIds: [
+ 0,
+ 1
+ ]
+ }],
+ controls: [
+ {
+ 'gridMeasureStatusId': 0,
+ 'activeButtons': [
+ 'apply',
+ 'save'
+ ],
+ 'inactiveFields': [
+ 'titeldermassnahme'
+ ]
+ },
+ {
+ 'gridMeasureStatusId': 1,
+ 'activeButtons': [
+ 'cancel',
+ 'forapproval',
+ 'save'
+ ],
+ 'inactiveFields': [
+ 'titeldermassnahme'
+ ]
+ },
+ {
+ 'gridMeasureStatusId': 2,
+ 'activeButtons': [],
+ 'inactiveFields': [
+ 'titeldermassnahme'
+ ]
+ },
+ {
+ 'gridMeasureStatusId': 3,
+ 'activeButtons': [
+ 'save',
+ 'cancel',
+ 'reject',
+ 'approve'
+ ],
+ 'inactiveFields': [
+ 'titeldermassnahme'
+ ]
+ },
+ {
+ 'gridMeasureStatusId': 4,
+ 'activeButtons': [
+ 'save',
+ 'request'
+ ],
+ 'inactiveFields': [
+ 'titeldermassnahme'
+ ]
+ },
+ {
+ 'gridMeasureStatusId': 5,
+ 'activeButtons': [
+ 'save',
+ 'release'
+ ],
+ 'inactiveFields': [
+ 'titeldermassnahme'
+ ]
+ },
+ {
+ 'gridMeasureStatusId': 6,
+ 'activeButtons': [
+ 'save',
+ 'activate'
+ ],
+ 'inactiveFields': [
+ 'titeldermassnahme'
+ ]
+ },
+ {
+ 'gridMeasureStatusId': 7,
+ 'activeButtons': [
+ 'save',
+ 'inwork'
+ ],
+ 'inactiveFields': [
+ 'titeldermassnahme'
+ ]
+ },
+ {
+ 'gridMeasureStatusId': 8,
+ 'activeButtons': [
+ 'save',
+ 'workfinish'
+ ],
+ 'inactiveFields': [
+ 'titeldermassnahme'
+ ]
+ },
+ {
+ 'gridMeasureStatusId': 9,
+ 'activeButtons': [
+ 'save',
+ 'finish'
+ ],
+ 'inactiveFields': [
+ 'titeldermassnahme'
+ ]
+ },
+ {
+ 'gridMeasureStatusId': 10,
+ 'activeButtons': [
+ 'save',
+ 'close'
+ ],
+ 'inactiveFields': [
+ 'titeldermassnahme'
+ ]
+ },
+ {
+ 'gridMeasureStatusId': 11,
+ 'activeButtons': [
+ 'save'
+ ],
+ 'inactiveFields': [
+ 'titeldermassnahme'
+ ]
+ },
+ {
+ 'gridMeasureStatusId': 11,
+ 'activeButtons': [
+ 'save'
+ ],
+ 'inactiveFields': [
+ 'titeldermassnahme'
+ ]
+ },
+ {
+ 'gridMeasureStatusId': 20,
+ 'activeButtons': [
+ 'quit'
+ ],
+ 'inactiveFields': [
+ 'titeldermassnahme'
+ ]
+ }
+ ],
+ stornoSection:
+ {
+ 'stornoRoles': [
+ 'planned-policies-measureapplicant',
+ 'planned-policies-measureplanner',
+ 'planned-policies-measureapprover',
+ 'planned-policies-requester',
+ 'planned-policies-clearance'
+ ]
+ },
+ duplicateSection:
+ {
+ 'duplicateRoles': [
+ 'planned-policies-measureapplicant'
+ ]
+ }
+};
diff --git a/src/app/test-data/status-changes.ts b/src/app/test-data/status-changes.ts
new file mode 100644
index 0000000..d1fc06d
--- /dev/null
+++ b/src/app/test-data/status-changes.ts
@@ -0,0 +1,27 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { StatusChange } from '../model/status-change';
+
+export const STATUSCHANGES: StatusChange[] = [
+ {
+ statusId: 2,
+ modDate: '',
+ modUser: 'otto',
+ remark: 'erstellt'
+ },
+ {
+ statusId: 3,
+ modDate: '',
+ modUser: 'hugo',
+ remark: 'geprüft ok'
+ }
+];
diff --git a/src/app/test-data/statuses.ts b/src/app/test-data/statuses.ts
new file mode 100644
index 0000000..32fcb81
--- /dev/null
+++ b/src/app/test-data/statuses.ts
@@ -0,0 +1,27 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Status } from '../model/status';
+
+export const STATUSES: Status[] = [
+ {
+ id: 1,
+ name: 'offen',
+ },
+ {
+ id: 2,
+ name: 'in Bearbeitung',
+ },
+ {
+ id: 3,
+ name: 'beendet',
+ }
+];
diff --git a/src/app/test-data/territories.ts b/src/app/test-data/territories.ts
new file mode 100644
index 0000000..df985c6
--- /dev/null
+++ b/src/app/test-data/territories.ts
@@ -0,0 +1,18 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Territory } from '../model/territory';
+
+export const TERRITORY: Territory[] = [
+ { 'id': 1, 'name': 'h1', 'description': 'Strom' },
+ { 'id': 2, 'name': 'h2', 'description': 'Gas' },
+ { 'id': 4, 'name': 'h3', 'description': 'Wasser' }
+];
diff --git a/src/app/test-data/tree.ts b/src/app/test-data/tree.ts
new file mode 100644
index 0000000..77394dc
--- /dev/null
+++ b/src/app/test-data/tree.ts
@@ -0,0 +1,26 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { TreeModel, Tree } from 'ng2-tree';
+
+const TREE_MODEL: TreeModel = {
+ value: 'tranformator',
+ id: 2
+};
+
+export const TREE_STRUCTURE: Tree = new Tree(TREE_MODEL);
+TREE_STRUCTURE.id = 2;
+TREE_STRUCTURE.value = 'tranformator';
+TREE_STRUCTURE.node = TREE_MODEL;
+
+
+
+
diff --git a/src/app/test-data/users.ts b/src/app/test-data/users.ts
new file mode 100644
index 0000000..e1a15ac
--- /dev/null
+++ b/src/app/test-data/users.ts
@@ -0,0 +1,45 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { User } from '../model/user';
+
+export const USERS: User[] = [
+ {
+ id: '1',
+ itemName: 'max',
+ username: 'max',
+ password: 'max',
+ name: 'Max Mustermann',
+ roles: ['planned-policies-measureplanner', 'test', 'test2'],
+ firstName: 'Max',
+ lastName: 'Mustermann',
+ },
+ {
+ id: '2',
+ itemName: 'admin',
+ username: 'admin',
+ password: 'admin',
+ name: 'Administrator',
+ roles: ['test', 'planned-policies-superuser'],
+ firstName: 'Administrator',
+ lastName: '',
+ },
+ {
+ id: '3',
+ itemName: 'otto',
+ username: 'otto',
+ password: 'otto',
+ name: 'Otto Normalverbraucher',
+ roles: ['test', 'planned-policies-measureapplicant'],
+ firstName: 'Otto',
+ lastName: 'Normalverbraucher',
+ }
+];
diff --git a/src/app/testing/abstract-mock-observable.service.ts b/src/app/testing/abstract-mock-observable.service.ts
new file mode 100644
index 0000000..dd037ff
--- /dev/null
+++ b/src/app/testing/abstract-mock-observable.service.ts
@@ -0,0 +1,63 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Subscription } from 'rxjs/Subscription';
+
+export abstract class AbstractMockObservableService {
+ protected _subscription: Subscription;
+ protected _fakeContent: any;
+ protected _fakeError: any;
+
+ set error(err) {
+ this._fakeError = err;
+ }
+
+ set content(data) {
+ this._fakeContent = data;
+ }
+
+ get subscription(): Subscription {
+ return this._subscription;
+ }
+
+ subscribe(next: Function, error?: Function, complete?: Function): Subscription {
+ this._subscription = new Subscription();
+
+ if (next && this._fakeContent && !this._fakeError) {
+ next(this._fakeContent);
+ }
+ if (error && this._fakeError) {
+ error(this._fakeError);
+ }
+ if (complete) {
+ complete();
+ }
+ return this._subscription;
+ }
+
+ do( doFunc: Function, error?: Function, complete?: Function ): Subscription {
+ this._subscription = new Subscription();
+
+ if (this._fakeContent && !this._fakeError ) {
+ doFunc( this._fakeContent );
+ }
+
+ if (error && this._fakeError) {
+ error(this._fakeError);
+ }
+
+ if (complete) {
+ complete();
+ }
+
+ return this._subscription;
+ }
+}
diff --git a/src/app/testing/index.ts b/src/app/testing/index.ts
new file mode 100644
index 0000000..b709407
--- /dev/null
+++ b/src/app/testing/index.ts
@@ -0,0 +1,81 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+******************************************************************************
+ */
+import { DebugElement } from '@angular/core';
+import { tick, ComponentFixture } from '@angular/core/testing';
+
+// export * from './jasmine-matchers';
+export * from './router-stubs';
+
+///// Short utilities /////
+
+/** Wait a tick, then detect changes */
+export function advance(f: ComponentFixture<any>): void {
+ tick();
+ f.detectChanges();
+}
+
+/**
+ * Create custom DOM event the old fashioned way
+ *
+ * https://developer.mozilla.org/en-US/docs/Web/API/Event/initEvent
+ * Although officially deprecated, some browsers (phantom) don't accept the preferred "new Event(eventName)"
+ */
+export function newEventDepr(eventName: string, bubbles = false, cancelable = false) {
+ const evt = document.createEvent('CustomEvent'); // MUST be 'CustomEvent'
+ evt.initCustomEvent(eventName, bubbles, cancelable, null);
+ return evt;
+}
+
+export function newEvent(eventName: string, bubbles = false, cancelable = false) {
+ const evt = new CustomEvent(eventName, {
+ 'bubbles': bubbles,
+ 'cancelable': cancelable
+ });
+ return evt;
+}
+
+// See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
+/** Button events to pass to `DebugElement.triggerEventHandler` for RouterLink event handler */
+export const ButtonClickEvents = {
+ left: { button: 0 },
+ right: { button: 2 }
+};
+
+/** Simulate element click. Defaults to mouse left-button click event. */
+export function click(el: DebugElement | HTMLElement, eventObj: any = ButtonClickEvents.left): void {
+ if (el instanceof HTMLElement) {
+ el.click();
+ } else {
+ el.triggerEventHandler('click', eventObj);
+ }
+}
+
+/** Simulate element focus. */
+export function focus(el: DebugElement | HTMLElement, eventObj: any = null): void {
+ if (el instanceof HTMLElement) {
+ el.focus();
+ } else {
+ el.triggerEventHandler('focus', eventObj);
+ }
+}
+
+/** Simulate pressing a key with keyCode. */
+export function pressKey(de: DebugElement, event: string, keyCode: number): void {
+ const keyEventObj = new Event(event);
+ Object.defineProperty(keyEventObj, 'keyCode', { 'value': keyCode });
+ de.triggerEventHandler(event, keyEventObj);
+}
+
+/*
+Copyright 2017 Google Inc. All Rights Reserved.
+Use of this source code is governed by an MIT-style license that
+can be found in the LICENSE file at http://angular.io/license
+*/
diff --git a/src/app/testing/mock-base-http.service.ts b/src/app/testing/mock-base-http.service.ts
new file mode 100644
index 0000000..e3dd0fe
--- /dev/null
+++ b/src/app/testing/mock-base-http.service.ts
@@ -0,0 +1,32 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { SessionContext } from '../common/session-context';
+import { BaseHttpServiceInterface, HttpCallInfo } from '../services/base-http.service';
+import { AbstractMockObservableService } from './abstract-mock-observable.service';
+
+
+
+
+export class MockBaseHttpService extends AbstractMockObservableService implements BaseHttpServiceInterface {
+ public lastHttpCallInfo: HttpCallInfo;
+ public lastSessionContext: SessionContext;
+
+ constructor() {
+ super();
+ }
+
+ public callService(callInfo: HttpCallInfo, sessionContext: SessionContext) {
+ this.lastHttpCallInfo = callInfo;
+ this.lastSessionContext = sessionContext;
+ return this;
+ }
+}
diff --git a/src/app/testing/mock.component.ts b/src/app/testing/mock.component.ts
new file mode 100644
index 0000000..782d514
--- /dev/null
+++ b/src/app/testing/mock.component.ts
@@ -0,0 +1,22 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { Component } from '@angular/core';
+
+export function MockComponent(options: Component): Component {
+ const metadata: Component = {
+ selector: options.selector,
+ template: options.template || '',
+ inputs: options.inputs,
+ outputs: options.outputs,
+ };
+ return Component(metadata)(<any>class MockClass {});
+}
diff --git a/src/app/testing/router-stubs.ts b/src/app/testing/router-stubs.ts
new file mode 100644
index 0000000..d5fc789
--- /dev/null
+++ b/src/app/testing/router-stubs.ts
@@ -0,0 +1,69 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+ // export for convenience.
+export { ActivatedRoute, Router, RouterLink, RouterOutlet} from '@angular/router';
+
+import { Component, Directive, Injectable, Input, HostListener } from '@angular/core';
+import { NavigationExtras } from '@angular/router';
+
+@Directive({
+ selector: '[appRouterLink]'
+})
+export class RouterLinkStubDirective {
+ @Input('appRouterLink') linkParams: any;
+ navigatedTo: any = null;
+
+ @HostListener('click') onClick() {
+ this.navigatedTo = this.linkParams;
+ }
+}
+
+@Component({selector: 'app-router-outlet', template: ''})
+export class RouterOutletStubComponent { }
+
+@Injectable()
+export class RouterStub {
+ navigate(commands: any[], extras?: NavigationExtras) { }
+}
+
+
+// Only implements params and part of snapshot.params
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+
+@Injectable()
+export class ActivatedRouteStub {
+
+ // ActivatedRoute.params is Observable
+ private subject = new BehaviorSubject(this.testParams);
+ params = this.subject.asObservable();
+
+ // Test parameters
+ private _testParams: {};
+ get testParams() { return this._testParams; }
+ set testParams(params: {}) {
+ this._testParams = params;
+ this.subject.next(params);
+ }
+
+ // ActivatedRoute.snapshot.params
+ get snapshot() {
+ return { params: this.testParams };
+ }
+}
+
+
+/*
+Copyright 2017 Google Inc. All Rights Reserved.
+Use of this source code is governed by an MIT-style license that
+can be found in the LICENSE file at http://angular.io/license
+*/
diff --git a/plannedGridMeasures.frontend_Test.txt b/src/assets/.gitkeep
similarity index 100%
copy from plannedGridMeasures.frontend_Test.txt
copy to src/assets/.gitkeep
diff --git a/src/assets/css/custom.css b/src/assets/css/custom.css
new file mode 100644
index 0000000..09b65cb
--- /dev/null
+++ b/src/assets/css/custom.css
@@ -0,0 +1,417 @@
+/*******************************************************************************
+* Copyright (c) 2015 BTC AG.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+* http://www.eclipse.org/legal/epl-v10.html
+*
+* Contributors:
+* Martin Boehm, http://www.minnemedia.de - initial API and implementation
+*******************************************************************************/
+
+html,
+body {
+ overflow-x: hidden;
+ /* Prevent scroll on narrow devices */
+}
+
+html {
+ position: relative;
+ min-height: 100%;
+}
+
+body {
+ padding-top: 20px;
+ /* Margin bottom by footer height*/
+ margin-bottom: 80px;
+}
+
+.voffset {
+ padding-top: 15px;
+}
+
+.breadcrumb .divider {
+ display: none;
+}
+
+.breadcrumb>li+li:before {
+ content: "\00276D";
+}
+
+/* Beginn Kopfleiste */
+
+.masthead {
+ border: 0;
+ background: rgb(232, 238, 231);
+ background: -moz-linear-gradient(left, rgba(232, 238, 231, 1) 0%, rgba(229, 237, 242, 1) 75%);
+ background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(232, 238, 231, 1)), color-stop(75%, rgba(229, 237, 242, 1)));
+ background: -webkit-linear-gradient(left, rgba(232, 238, 231, 1) 0%, rgba(229, 237, 242, 1) 75%);
+ background: -o-linear-gradient(left, rgba(232, 238, 231, 1) 0%, rgba(229, 237, 242, 1) 75%);
+ background: -ms-linear-gradient(left, rgba(232, 238, 231, 1) 0%, rgba(229, 237, 242, 1) 75%);
+ background: linear-gradient(to right, rgba(232, 238, 231, 1) 0%, rgba(229, 237, 242, 1) 75%);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#e8eee7', endColorstr='#e5edf2', GradientType=1);
+}
+
+.masthead:after {
+ position: relative;
+ height: 4px;
+ width: 100%;
+ content: "";
+ background: rgb(121, 182, 28);
+ background: -moz-linear-gradient(left, rgba(121, 182, 28, 1) 0%, rgba(2, 129, 196, 1) 75%);
+ background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(121, 182, 28, 1)), color-stop(75%, rgba(2, 129, 196, 1)));
+ background: -webkit-linear-gradient(left, rgba(121, 182, 28, 1) 0%, rgba(2, 129, 196, 1) 75%);
+ background: -o-linear-gradient(left, rgba(121, 182, 28, 1) 0%, rgba(2, 129, 196, 1) 75%);
+ background: -ms-linear-gradient(left, rgba(121, 182, 28, 1) 0%, rgba(2, 129, 196, 1) 75%);
+ background: linear-gradient(to right, rgba(121, 182, 28, 1) 0%, rgba(2, 129, 196, 1) 75%);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#79b61c', endColorstr='#0281c4', GradientType=1);
+}
+
+.navbar-brand {
+ padding-top: 13px;
+ padding-bottom: 0;
+ padding-left: 70px;
+ background: url(../img/logo_openkonsequenz.png) no-repeat 5px center;
+ background-size: 63px 50px;
+}
+
+.navbar-brand .open {
+ font-size: 1.8em;
+ color: #000;
+ font-weight: 500;
+ text-transform: lowercase;
+}
+
+.navbar-brand .konsequenz {
+ margin-left: 3px;
+ font-size: 1.35em;
+ color: #000;
+ text-transform: uppercase;
+ font-weight: 300;
+}
+
+.masthead .navbar-toggle {
+ position: absolute;
+ right: 0;
+ background: rgba(0, 0, 0, 0.05);
+}
+
+/* Ende Kopfleiste */
+
+/* Beginn Sidebar */
+
+#sidebar .list-group-item:first-child {
+ border-top-right-radius: 0px !important;
+ border-top-left-radius: 0px !important;
+}
+
+.toggle-sidebar {
+ margin-top: 10px;
+}
+
+.row-offcanvas .toggle-sidebar .in,
+.row-offcanvas.active .toggle-sidebar .out {
+ display: block;
+}
+
+.row-offcanvas .toggle-sidebar .out,
+.row-offcanvas.active .toggle-sidebar .in {
+ display: none;
+}
+
+/* Ende Sidebar */
+
+.maincontent {
+ background: #fff;
+ margin: 0;
+ padding-top: 15px;
+ padding-bottom: 15px;
+}
+
+.nav-tabs-maincontent {
+ margin-left: 0;
+ padding-left: 15px;
+}
+
+.checkboxes label.checkbox,
+.checkboxes label.radio {
+ font-weight: 400;
+ margin-left: 20px;
+}
+
+.checkboxes .input-group-addon {
+ width: 50px
+}
+
+.form-group.sicherheit-aufschlag .input-group {
+ width: 82px;
+}
+
+.form-group:last-child {
+ margin-bottom: 0;
+}
+
+.zeit {
+ float: right;
+}
+
+.zeit .alert {
+ margin-top: -6px;
+ margin-bottom: 10px;
+ padding: 4px;
+}
+
+#netzgebiete tr.toggle {
+ cursor: pointer;
+}
+
+#netzgebiete tbody:nth-child(2n) tr.toggle {
+ background: #f5f8fc;
+}
+
+#netzgebiete tbody tr.toggle td span.glyphicon {
+ margin-right: 5px;
+}
+
+#netzgebiete tbody:nth-child(2n+1) tr.toggle {
+ background: #f5f8fc;
+}
+
+#netzgebiete tfoot tr {
+ background: #e9f0f9;
+}
+
+#netzgebiete tr.collapsing td:first-child,
+#netzgebiete tr.collapse td:first-child {
+ padding-left: 15px;
+}
+
+#regulationSteps {
+ height: 23px;
+ margin-top: -2px;
+ margin-bottom: 4px;
+}
+
+.table>tbody>tr>td,
+.table>tbody>tr>th,
+.table>tfoot>tr>td,
+.table>tfoot>tr>th,
+.table>thead>tr>td,
+.table>thead>tr>th {
+ border-top: 1px solid #ddd;
+ padding: 4px;
+ vertical-align: top;
+}
+
+.table>thead>tr>th {
+ padding: 6px 0px 35px 6px;
+}
+
+@media screen and (max-width: 1024px) {
+ .panel-group .panel {
+ width: 70em;
+ }
+}
+
+@media screen and (max-width: 767px) {
+ .row-offcanvas {
+ position: relative;
+ -webkit-transition: all .25s ease-out;
+ -o-transition: all .25s ease-out;
+ transition: all .25s ease-out;
+ }
+ .row-offcanvas-right {
+ right: 0;
+ }
+ .row-offcanvas-left {
+ left: 0;
+ }
+ .row-offcanvas-right .sidebar-offcanvas {
+ right: -50%;
+ /* 6 columns */
+ }
+ .row-offcanvas-left .sidebar-offcanvas {
+ left: -50%;
+ /* 6 columns */
+ }
+ .row-offcanvas-right.active {
+ right: 50%;
+ /* 6 columns */
+ }
+ .row-offcanvas-left.active {
+ left: 50%;
+ /* 6 columns */
+ }
+ .sidebar-offcanvas {
+ position: absolute;
+ top: 0;
+ width: 50%;
+ /* 6 columns */
+ }
+}
+
+.ui-grid-cell button.btn-sm.btn-default {
+ margin: 2px;
+ height: 26px;
+ width: 26px;
+ padding: 0px;
+}
+
+.ui-grid-pager-panel {
+ border-top: solid 1px #d4d4d4;
+ background-color: #efefef;
+}
+
+.mtdefault {
+ margin-top: 10px;
+}
+
+button.btn-sm.btn-default.active {
+ background-color: green ! important;
+}
+
+.ui-grid-row.active div {
+ background-color: #b4f371 ! important;
+ background-image: none ! important;
+}
+
+div.panel-default {
+ min-height: 120px;
+}
+
+.toolbar {
+ position: absolute;
+ right: 10px;
+ top: 20px;
+}
+
+div.panel.panel-default.panel-details,
+div.panel.panel-default.panel-activities {
+ min-height: 436px;
+}
+
+.panel-details table tr td {
+ padding: 4px 20px 4px 4px;
+}
+
+.panel-details table tr td:first-child {
+ font-weight: bold;
+}
+
+.feature-heading {
+ display: none;
+}
+
+.breadcrumb {
+ margin-bottom: 10px;
+}
+
+input.goto {
+ width: 40px;
+ height: 28px;
+ text-align: center;
+}
+
+span.paging {
+ display: inline-block;
+ margin: 0px 10px 0px 10px;
+}
+
+.cursor {
+ cursor: pointer;
+}
+
+.align-right {
+ text-align: right;
+}
+
+table.table.tree-grid th {
+ border: solid 1px #ddd;
+}
+
+table.table.tree-grid {
+ border-top: solid 1px #ddd;
+}
+
+.ui-grid-cell.ng-scope.ui-grid-disable-selection {
+ border-bottom: solid 1px #ddd;
+}
+
+a i.indented.tree-icon.glyphicon.glyphicon-arrow-right {
+ margin-left: 30px;
+}
+
+table.tree-grid tr.tree-grid-row.ng-scope.level-3.active td {
+ background-color: #ffffff ! important;
+}
+
+table.tree-grid tr.tree-grid-row.ng-scope.level-3:hover {
+ background-color: #eeeeee ! important;
+}
+
+#proposalTree {
+ height: 402px;
+}
+
+#timermessage {
+ position: fixed;
+ left: 0px;
+ top: 0px;
+ width: 100%;
+ height: 100%;
+ z-index: 20000;
+ display: none;
+}
+
+#timermessage .messagebox {
+ width: 420px;
+ height: 300px;
+ padding: 10px;
+ background-color: white;
+ margin: 100px auto;
+}
+
+.error {
+ color: red ! important;
+}
+
+form .row.navheader {
+ margin: 20px 0px 40px 0px ! important;
+}
+
+.row.navheader {
+ margin: 0px 0px 0px 0px;
+ border-top: solid 1px #cccccc;
+ border-bottom: solid 1px #cccccc;
+ padding: 5px 0px 5px 0px;
+ background-color: #f5f5f5;
+}
+
+.row.navfooter {
+ margin: 10px 0px 0px 0px;
+ border-top: solid 1px #cccccc;
+ border-bottom: solid 1px #cccccc;
+ padding: 5px 0px 5px 0px;
+ background-color: #f5f5f5;
+}
+
+.panel td {
+ vertical-align: top;
+}
+
+.ui-grid-a11y-ariascreenreader-speakable {
+ display: none ! important;
+}
+
+.ranges {
+ width: 100% ! important;
+}
+
+login {
+ font-size: 14px;
+ color: #537798;
+}
+
+login:hover {
+ color: #bac2cb;
+}
\ No newline at end of file
diff --git a/src/assets/css/main.css b/src/assets/css/main.css
new file mode 100644
index 0000000..68fc8ce
--- /dev/null
+++ b/src/assets/css/main.css
@@ -0,0 +1,11 @@
+/*
+******************************************************************************
+* Copyright (c) 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
diff --git a/src/assets/img/logo_openkonsequenz.png b/src/assets/img/logo_openkonsequenz.png
new file mode 100644
index 0000000..6506ea8
--- /dev/null
+++ b/src/assets/img/logo_openkonsequenz.png
Binary files differ
diff --git a/src/assets/js/xml2json.min.js b/src/assets/js/xml2json.min.js
new file mode 100644
index 0000000..e8f9d0a
--- /dev/null
+++ b/src/assets/js/xml2json.min.js
@@ -0,0 +1 @@
+(function(a,b){if(typeof define==="function"&&define.amd){define([],b);}else{if(typeof exports==="object"){module.exports=b();}else{a.X2JS=b();}}}(this,function(){return function(z){var t="1.2.0";z=z||{};i();u();function i(){if(z.escapeMode===undefined){z.escapeMode=true;}z.attributePrefix=z.attributePrefix||"_";z.arrayAccessForm=z.arrayAccessForm||"none";z.emptyNodeForm=z.emptyNodeForm||"text";if(z.enableToStringFunc===undefined){z.enableToStringFunc=true;}z.arrayAccessFormPaths=z.arrayAccessFormPaths||[];if(z.skipEmptyTextNodesForObj===undefined){z.skipEmptyTextNodesForObj=true;}if(z.stripWhitespaces===undefined){z.stripWhitespaces=true;}z.datetimeAccessFormPaths=z.datetimeAccessFormPaths||[];if(z.useDoubleQuotes===undefined){z.useDoubleQuotes=false;}z.xmlElementsFilter=z.xmlElementsFilter||[];z.jsonPropertiesFilter=z.jsonPropertiesFilter||[];if(z.keepCData===undefined){z.keepCData=false;}}var h={ELEMENT_NODE:1,TEXT_NODE:3,CDATA_SECTION_NODE:4,COMMENT_NODE:8,DOCUMENT_NODE:9};function u(){}function x(B){var C=B.localName;if(C==null){C=B.baseName;}if(C==null||C==""){C=B.nodeName;}return C;}function r(B){return B.prefix;}function s(B){if(typeof(B)=="string"){return B.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'");}else{return B;}}function k(B){return B.replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'/g,"'").replace(/&/g,"&");}function w(C,F,D,E){var B=0;for(;B<C.length;B++){var G=C[B];if(typeof G==="string"){if(G==E){break;}}else{if(G instanceof RegExp){if(G.test(E)){break;}}else{if(typeof G==="function"){if(G(F,D,E)){break;}}}}}return B!=C.length;}function n(D,B,C){switch(z.arrayAccessForm){case"property":if(!(D[B] instanceof Array)){D[B+"_asArray"]=[D[B]];}else{D[B+"_asArray"]=D[B];}break;}if(!(D[B] instanceof Array)&&z.arrayAccessFormPaths.length>0){if(w(z.arrayAccessFormPaths,D,B,C)){D[B]=[D[B]];}}}function a(G){var E=G.split(/[-T:+Z]/g);var F=new Date(E[0],E[1]-1,E[2]);var D=E[5].split(".");F.setHours(E[3],E[4],D[0]);if(D.length>1){F.setMilliseconds(D[1]);}if(E[6]&&E[7]){var C=E[6]*60+Number(E[7]);var B=/\d\d-\d\d:\d\d$/.test(G)?"-":"+";C=0+(B=="-"?-1*C:C);F.setMinutes(F.getMinutes()-C-F.getTimezoneOffset());}else{if(G.indexOf("Z",G.length-1)!==-1){F=new Date(Date.UTC(F.getFullYear(),F.getMonth(),F.getDate(),F.getHours(),F.getMinutes(),F.getSeconds(),F.getMilliseconds()));}}return F;}function q(D,B,C){if(z.datetimeAccessFormPaths.length>0){var E=C.split(".#")[0];if(w(z.datetimeAccessFormPaths,D,B,E)){return a(D);}else{return D;}}else{return D;}}function b(E,C,B,D){if(C==h.ELEMENT_NODE&&z.xmlElementsFilter.length>0){return w(z.xmlElementsFilter,E,B,D);}else{return true;}}function A(D,J){if(D.nodeType==h.DOCUMENT_NODE){var K=new Object;var B=D.childNodes;for(var L=0;L<B.length;L++){var C=B.item(L);if(C.nodeType==h.ELEMENT_NODE){var I=x(C);K[I]=A(C,I);}}return K;}else{if(D.nodeType==h.ELEMENT_NODE){var K=new Object;K.__cnt=0;var B=D.childNodes;for(var L=0;L<B.length;L++){var C=B.item(L);var I=x(C);if(C.nodeType!=h.COMMENT_NODE){var H=J+"."+I;if(b(K,C.nodeType,I,H)){K.__cnt++;if(K[I]==null){K[I]=A(C,H);n(K,I,H);}else{if(K[I]!=null){if(!(K[I] instanceof Array)){K[I]=[K[I]];n(K,I,H);}}(K[I])[K[I].length]=A(C,H);}}}}for(var E=0;E<D.attributes.length;E++){var F=D.attributes.item(E);K.__cnt++;K[z.attributePrefix+F.name]=F.value;}var G=r(D);if(G!=null&&G!=""){K.__cnt++;K.__prefix=G;}if(K["#text"]!=null){K.__text=K["#text"];if(K.__text instanceof Array){K.__text=K.__text.join("\n");}if(z.stripWhitespaces){K.__text=K.__text.trim();}delete K["#text"];if(z.arrayAccessForm=="property"){delete K["#text_asArray"];}K.__text=q(K.__text,I,J+"."+I);}if(K["#cdata-section"]!=null){K.__cdata=K["#cdata-section"];delete K["#cdata-section"];if(z.arrayAccessForm=="property"){delete K["#cdata-section_asArray"];}}if(K.__cnt==0&&z.emptyNodeForm=="text"){K="";}else{if(K.__cnt==1&&K.__text!=null){K=K.__text;}else{if(K.__cnt==1&&K.__cdata!=null&&!z.keepCData){K=K.__cdata;}else{if(K.__cnt>1&&K.__text!=null&&z.skipEmptyTextNodesForObj){if((z.stripWhitespaces&&K.__text=="")||(K.__text.trim()=="")){delete K.__text;}}}}}delete K.__cnt;if(z.enableToStringFunc&&(K.__text!=null||K.__cdata!=null)){K.toString=function(){return(this.__text!=null?this.__text:"")+(this.__cdata!=null?this.__cdata:"");};}return K;}else{if(D.nodeType==h.TEXT_NODE||D.nodeType==h.CDATA_SECTION_NODE){return D.nodeValue;}}}}function o(I,F,H,C){var E="<"+((I!=null&&I.__prefix!=null)?(I.__prefix+":"):"")+F;if(H!=null){for(var G=0;G<H.length;G++){var D=H[G];var B=I[D];if(z.escapeMode){B=s(B);}E+=" "+D.substr(z.attributePrefix.length)+"=";if(z.useDoubleQuotes){E+='"'+B+'"';}else{E+="'"+B+"'";}}}if(!C){E+=">";}else{E+="/>";}return E;}function j(C,B){return"</"+(C.__prefix!=null?(C.__prefix+":"):"")+B+">";}function v(C,B){return C.indexOf(B,C.length-B.length)!==-1;}function y(C,B){if((z.arrayAccessForm=="property"&&v(B.toString(),("_asArray")))||B.toString().indexOf(z.attributePrefix)==0||B.toString().indexOf("__")==0||(C[B] instanceof Function)){return true;}else{return false;}}function m(D){var C=0;if(D instanceof Object){for(var B in D){if(y(D,B)){continue;}C++;}}return C;}function l(D,B,C){return z.jsonPropertiesFilter.length==0||C==""||w(z.jsonPropertiesFilter,D,B,C);}function c(D){var C=[];if(D instanceof Object){for(var B in D){if(B.toString().indexOf("__")==-1&&B.toString().indexOf(z.attributePrefix)==0){C.push(B);}}}return C;}function g(C){var B="";if(C.__cdata!=null){B+="<![CDATA["+C.__cdata+"]]>";}if(C.__text!=null){if(z.escapeMode){B+=s(C.__text);}else{B+=C.__text;}}return B;}function d(C){var B="";if(C instanceof Object){B+=g(C);}else{if(C!=null){if(z.escapeMode){B+=s(C);}else{B+=C;}}}return B;}function p(C,B){if(C===""){return B;}else{return C+"."+B;}}function f(D,G,F,E){var B="";if(D.length==0){B+=o(D,G,F,true);}else{for(var C=0;C<D.length;C++){B+=o(D[C],G,c(D[C]),false);B+=e(D[C],p(E,G));B+=j(D[C],G);}}return B;}function e(I,H){var B="";var F=m(I);if(F>0){for(var E in I){if(y(I,E)||(H!=""&&!l(I,E,p(H,E)))){continue;}var D=I[E];var G=c(D);if(D==null||D==undefined){B+=o(D,E,G,true);}else{if(D instanceof Object){if(D instanceof Array){B+=f(D,E,G,H);}else{if(D instanceof Date){B+=o(D,E,G,false);B+=D.toISOString();B+=j(D,E);}else{var C=m(D);if(C>0||D.__text!=null||D.__cdata!=null){B+=o(D,E,G,false);B+=e(D,p(H,E));B+=j(D,E);}else{B+=o(D,E,G,true);}}}}else{B+=o(D,E,G,false);B+=d(D);B+=j(D,E);}}}}B+=d(I);return B;}this.parseXmlString=function(D){var F=window.ActiveXObject||"ActiveXObject" in window;if(D===undefined){return null;}var E;if(window.DOMParser){var G=new window.DOMParser();var B=null;if(!F){try{B=G.parseFromString("INVALID","text/xml").getElementsByTagName("parsererror")[0].namespaceURI;}catch(C){B=null;}}try{E=G.parseFromString(D,"text/xml");if(B!=null&&E.getElementsByTagNameNS(B,"parsererror").length>0){E=null;}}catch(C){E=null;}}else{if(D.indexOf("<?")==0){D=D.substr(D.indexOf("?>")+2);}E=new ActiveXObject("Microsoft.XMLDOM");E.async="false";E.loadXML(D);}return E;};this.asArray=function(B){if(B===undefined||B==null){return[];}else{if(B instanceof Array){return B;}else{return[B];}}};this.toXmlDateTime=function(B){if(B instanceof Date){return B.toISOString();}else{if(typeof(B)==="number"){return new Date(B).toISOString();}else{return null;}}};this.asDateTime=function(B){if(typeof(B)=="string"){return a(B);}else{return B;}};this.xml2json=function(B){return A(B);};this.xml_str2json=function(B){var C=this.parseXmlString(B);if(C!=null){return this.xml2json(C);}else{return null;}};this.json2xml_str=function(B){return e(B,"");};this.json2xml=function(C){var B=this.json2xml_str(C);return this.parseXmlString(B);};this.getVersion=function(){return t;};};}));
\ No newline at end of file
diff --git a/src/assets/settings.json b/src/assets/settings.json
new file mode 100644
index 0000000..acff65f
--- /dev/null
+++ b/src/assets/settings.json
@@ -0,0 +1,3 @@
+{
+ "HELPURL": "https://www.google.de"
+}
\ No newline at end of file
diff --git a/src/assets/source-sans-pro/EOT/SourceSansPro-Black.eot b/src/assets/source-sans-pro/EOT/SourceSansPro-Black.eot
new file mode 100644
index 0000000..747914e
--- /dev/null
+++ b/src/assets/source-sans-pro/EOT/SourceSansPro-Black.eot
Binary files differ
diff --git a/src/assets/source-sans-pro/EOT/SourceSansPro-BlackIt.eot b/src/assets/source-sans-pro/EOT/SourceSansPro-BlackIt.eot
new file mode 100644
index 0000000..a56b5c3
--- /dev/null
+++ b/src/assets/source-sans-pro/EOT/SourceSansPro-BlackIt.eot
Binary files differ
diff --git a/src/assets/source-sans-pro/EOT/SourceSansPro-Bold.eot b/src/assets/source-sans-pro/EOT/SourceSansPro-Bold.eot
new file mode 100644
index 0000000..e9f4234
--- /dev/null
+++ b/src/assets/source-sans-pro/EOT/SourceSansPro-Bold.eot
Binary files differ
diff --git a/src/assets/source-sans-pro/EOT/SourceSansPro-BoldIt.eot b/src/assets/source-sans-pro/EOT/SourceSansPro-BoldIt.eot
new file mode 100644
index 0000000..d375ef5
--- /dev/null
+++ b/src/assets/source-sans-pro/EOT/SourceSansPro-BoldIt.eot
Binary files differ
diff --git a/src/assets/source-sans-pro/EOT/SourceSansPro-ExtraLight.eot b/src/assets/source-sans-pro/EOT/SourceSansPro-ExtraLight.eot
new file mode 100644
index 0000000..f4dbef9
--- /dev/null
+++ b/src/assets/source-sans-pro/EOT/SourceSansPro-ExtraLight.eot
Binary files differ
diff --git a/src/assets/source-sans-pro/EOT/SourceSansPro-ExtraLightIt.eot b/src/assets/source-sans-pro/EOT/SourceSansPro-ExtraLightIt.eot
new file mode 100644
index 0000000..da735de
--- /dev/null
+++ b/src/assets/source-sans-pro/EOT/SourceSansPro-ExtraLightIt.eot
Binary files differ
diff --git a/src/assets/source-sans-pro/EOT/SourceSansPro-It.eot b/src/assets/source-sans-pro/EOT/SourceSansPro-It.eot
new file mode 100644
index 0000000..55ad856
--- /dev/null
+++ b/src/assets/source-sans-pro/EOT/SourceSansPro-It.eot
Binary files differ
diff --git a/src/assets/source-sans-pro/EOT/SourceSansPro-Light.eot b/src/assets/source-sans-pro/EOT/SourceSansPro-Light.eot
new file mode 100644
index 0000000..ea7f671
--- /dev/null
+++ b/src/assets/source-sans-pro/EOT/SourceSansPro-Light.eot
Binary files differ
diff --git a/src/assets/source-sans-pro/EOT/SourceSansPro-LightIt.eot b/src/assets/source-sans-pro/EOT/SourceSansPro-LightIt.eot
new file mode 100644
index 0000000..d7cb9a7
--- /dev/null
+++ b/src/assets/source-sans-pro/EOT/SourceSansPro-LightIt.eot
Binary files differ
diff --git a/src/assets/source-sans-pro/EOT/SourceSansPro-Regular.eot b/src/assets/source-sans-pro/EOT/SourceSansPro-Regular.eot
new file mode 100644
index 0000000..ba7f8d9
--- /dev/null
+++ b/src/assets/source-sans-pro/EOT/SourceSansPro-Regular.eot
Binary files differ
diff --git a/src/assets/source-sans-pro/EOT/SourceSansPro-Semibold.eot b/src/assets/source-sans-pro/EOT/SourceSansPro-Semibold.eot
new file mode 100644
index 0000000..2f7a52c
--- /dev/null
+++ b/src/assets/source-sans-pro/EOT/SourceSansPro-Semibold.eot
Binary files differ
diff --git a/src/assets/source-sans-pro/EOT/SourceSansPro-SemiboldIt.eot b/src/assets/source-sans-pro/EOT/SourceSansPro-SemiboldIt.eot
new file mode 100644
index 0000000..d8504fe
--- /dev/null
+++ b/src/assets/source-sans-pro/EOT/SourceSansPro-SemiboldIt.eot
Binary files differ
diff --git a/src/assets/source-sans-pro/LICENSE.txt b/src/assets/source-sans-pro/LICENSE.txt
new file mode 100644
index 0000000..df18763
--- /dev/null
+++ b/src/assets/source-sans-pro/LICENSE.txt
@@ -0,0 +1,93 @@
+Copyright 2010, 2012, 2014 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+
+This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/src/assets/source-sans-pro/OTF/SourceSansPro-Black.otf b/src/assets/source-sans-pro/OTF/SourceSansPro-Black.otf
new file mode 100644
index 0000000..0c25f3d
--- /dev/null
+++ b/src/assets/source-sans-pro/OTF/SourceSansPro-Black.otf
Binary files differ
diff --git a/src/assets/source-sans-pro/OTF/SourceSansPro-BlackIt.otf b/src/assets/source-sans-pro/OTF/SourceSansPro-BlackIt.otf
new file mode 100644
index 0000000..da3504c
--- /dev/null
+++ b/src/assets/source-sans-pro/OTF/SourceSansPro-BlackIt.otf
Binary files differ
diff --git a/src/assets/source-sans-pro/OTF/SourceSansPro-Bold.otf b/src/assets/source-sans-pro/OTF/SourceSansPro-Bold.otf
new file mode 100644
index 0000000..98dbee7
--- /dev/null
+++ b/src/assets/source-sans-pro/OTF/SourceSansPro-Bold.otf
Binary files differ
diff --git a/src/assets/source-sans-pro/OTF/SourceSansPro-BoldIt.otf b/src/assets/source-sans-pro/OTF/SourceSansPro-BoldIt.otf
new file mode 100644
index 0000000..6600c86
--- /dev/null
+++ b/src/assets/source-sans-pro/OTF/SourceSansPro-BoldIt.otf
Binary files differ
diff --git a/src/assets/source-sans-pro/OTF/SourceSansPro-ExtraLight.otf b/src/assets/source-sans-pro/OTF/SourceSansPro-ExtraLight.otf
new file mode 100644
index 0000000..f885ce7
--- /dev/null
+++ b/src/assets/source-sans-pro/OTF/SourceSansPro-ExtraLight.otf
Binary files differ
diff --git a/src/assets/source-sans-pro/OTF/SourceSansPro-ExtraLightIt.otf b/src/assets/source-sans-pro/OTF/SourceSansPro-ExtraLightIt.otf
new file mode 100644
index 0000000..f932024
--- /dev/null
+++ b/src/assets/source-sans-pro/OTF/SourceSansPro-ExtraLightIt.otf
Binary files differ
diff --git a/src/assets/source-sans-pro/OTF/SourceSansPro-It.otf b/src/assets/source-sans-pro/OTF/SourceSansPro-It.otf
new file mode 100644
index 0000000..2d627d9
--- /dev/null
+++ b/src/assets/source-sans-pro/OTF/SourceSansPro-It.otf
Binary files differ
diff --git a/src/assets/source-sans-pro/OTF/SourceSansPro-Light.otf b/src/assets/source-sans-pro/OTF/SourceSansPro-Light.otf
new file mode 100644
index 0000000..159979f
--- /dev/null
+++ b/src/assets/source-sans-pro/OTF/SourceSansPro-Light.otf
Binary files differ
diff --git a/src/assets/source-sans-pro/OTF/SourceSansPro-LightIt.otf b/src/assets/source-sans-pro/OTF/SourceSansPro-LightIt.otf
new file mode 100644
index 0000000..e3d49b5
--- /dev/null
+++ b/src/assets/source-sans-pro/OTF/SourceSansPro-LightIt.otf
Binary files differ
diff --git a/src/assets/source-sans-pro/OTF/SourceSansPro-Regular.otf b/src/assets/source-sans-pro/OTF/SourceSansPro-Regular.otf
new file mode 100644
index 0000000..bdcfb27
--- /dev/null
+++ b/src/assets/source-sans-pro/OTF/SourceSansPro-Regular.otf
Binary files differ
diff --git a/src/assets/source-sans-pro/OTF/SourceSansPro-Semibold.otf b/src/assets/source-sans-pro/OTF/SourceSansPro-Semibold.otf
new file mode 100644
index 0000000..fffdbaf
--- /dev/null
+++ b/src/assets/source-sans-pro/OTF/SourceSansPro-Semibold.otf
Binary files differ
diff --git a/src/assets/source-sans-pro/OTF/SourceSansPro-SemiboldIt.otf b/src/assets/source-sans-pro/OTF/SourceSansPro-SemiboldIt.otf
new file mode 100644
index 0000000..e90515b
--- /dev/null
+++ b/src/assets/source-sans-pro/OTF/SourceSansPro-SemiboldIt.otf
Binary files differ
diff --git a/src/assets/source-sans-pro/README.md b/src/assets/source-sans-pro/README.md
new file mode 100644
index 0000000..a3e1b9d
--- /dev/null
+++ b/src/assets/source-sans-pro/README.md
@@ -0,0 +1,20 @@
+# Source Sans Pro
+
+Source Sans Pro is a set of OpenType fonts that have been designed to work well
+in user interface (UI) environments. In addition to a functional OpenType font, this open
+source project provides all of the source files that were used to build this OpenType font
+by using the AFDKO makeotf tool.
+
+## Font installation instructions
+
+* [Mac OS X](http://support.apple.com/kb/HT2509)
+* [Windows](http://windows.microsoft.com/en-us/windows-vista/install-or-uninstall-fonts)
+* [Linux/Unix-based systems](https://github.com/adobe-fonts/source-code-pro/issues/17#issuecomment-8967116)
+
+## Getting Involved
+
+Send suggestions for changes to the Source Sans OpenType font project maintainer, [Paul D. Hunt](mailto:opensourcefonts@adobe.com?subject=[GitHub] Source Sans Pro), for consideration.
+
+## Further information
+
+For information about the design and background of Source Sans, please refer to the [official font readme file](http://www.adobe.com/products/type/font-information/source-sans-pro-readme.html).
diff --git a/src/assets/source-sans-pro/TTF/SourceSansPro-Black.ttf b/src/assets/source-sans-pro/TTF/SourceSansPro-Black.ttf
new file mode 100644
index 0000000..9c9b5cb
--- /dev/null
+++ b/src/assets/source-sans-pro/TTF/SourceSansPro-Black.ttf
Binary files differ
diff --git a/src/assets/source-sans-pro/TTF/SourceSansPro-BlackIt.ttf b/src/assets/source-sans-pro/TTF/SourceSansPro-BlackIt.ttf
new file mode 100644
index 0000000..294ce5a
--- /dev/null
+++ b/src/assets/source-sans-pro/TTF/SourceSansPro-BlackIt.ttf
Binary files differ
diff --git a/src/assets/source-sans-pro/TTF/SourceSansPro-Bold.ttf b/src/assets/source-sans-pro/TTF/SourceSansPro-Bold.ttf
new file mode 100644
index 0000000..5d65c93
--- /dev/null
+++ b/src/assets/source-sans-pro/TTF/SourceSansPro-Bold.ttf
Binary files differ
diff --git a/src/assets/source-sans-pro/TTF/SourceSansPro-BoldIt.ttf b/src/assets/source-sans-pro/TTF/SourceSansPro-BoldIt.ttf
new file mode 100644
index 0000000..3decd13
--- /dev/null
+++ b/src/assets/source-sans-pro/TTF/SourceSansPro-BoldIt.ttf
Binary files differ
diff --git a/src/assets/source-sans-pro/TTF/SourceSansPro-ExtraLight.ttf b/src/assets/source-sans-pro/TTF/SourceSansPro-ExtraLight.ttf
new file mode 100644
index 0000000..253eafa
--- /dev/null
+++ b/src/assets/source-sans-pro/TTF/SourceSansPro-ExtraLight.ttf
Binary files differ
diff --git a/src/assets/source-sans-pro/TTF/SourceSansPro-ExtraLightIt.ttf b/src/assets/source-sans-pro/TTF/SourceSansPro-ExtraLightIt.ttf
new file mode 100644
index 0000000..00d7e9a
--- /dev/null
+++ b/src/assets/source-sans-pro/TTF/SourceSansPro-ExtraLightIt.ttf
Binary files differ
diff --git a/src/assets/source-sans-pro/TTF/SourceSansPro-It.ttf b/src/assets/source-sans-pro/TTF/SourceSansPro-It.ttf
new file mode 100644
index 0000000..f7af537
--- /dev/null
+++ b/src/assets/source-sans-pro/TTF/SourceSansPro-It.ttf
Binary files differ
diff --git a/src/assets/source-sans-pro/TTF/SourceSansPro-Light.ttf b/src/assets/source-sans-pro/TTF/SourceSansPro-Light.ttf
new file mode 100644
index 0000000..83a0a33
--- /dev/null
+++ b/src/assets/source-sans-pro/TTF/SourceSansPro-Light.ttf
Binary files differ
diff --git a/src/assets/source-sans-pro/TTF/SourceSansPro-LightIt.ttf b/src/assets/source-sans-pro/TTF/SourceSansPro-LightIt.ttf
new file mode 100644
index 0000000..f188279
--- /dev/null
+++ b/src/assets/source-sans-pro/TTF/SourceSansPro-LightIt.ttf
Binary files differ
diff --git a/src/assets/source-sans-pro/TTF/SourceSansPro-Regular.ttf b/src/assets/source-sans-pro/TTF/SourceSansPro-Regular.ttf
new file mode 100644
index 0000000..44486cd
--- /dev/null
+++ b/src/assets/source-sans-pro/TTF/SourceSansPro-Regular.ttf
Binary files differ
diff --git a/src/assets/source-sans-pro/TTF/SourceSansPro-Semibold.ttf b/src/assets/source-sans-pro/TTF/SourceSansPro-Semibold.ttf
new file mode 100644
index 0000000..86b00c0
--- /dev/null
+++ b/src/assets/source-sans-pro/TTF/SourceSansPro-Semibold.ttf
Binary files differ
diff --git a/src/assets/source-sans-pro/TTF/SourceSansPro-SemiboldIt.ttf b/src/assets/source-sans-pro/TTF/SourceSansPro-SemiboldIt.ttf
new file mode 100644
index 0000000..13d66a1
--- /dev/null
+++ b/src/assets/source-sans-pro/TTF/SourceSansPro-SemiboldIt.ttf
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-Black.otf.woff b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-Black.otf.woff
new file mode 100644
index 0000000..f1a663a
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-Black.otf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-BlackIt.otf.woff b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-BlackIt.otf.woff
new file mode 100644
index 0000000..1d7dfbd
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-BlackIt.otf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-Bold.otf.woff b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-Bold.otf.woff
new file mode 100644
index 0000000..6700893
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-Bold.otf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-BoldIt.otf.woff b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-BoldIt.otf.woff
new file mode 100644
index 0000000..d5e4a0f
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-BoldIt.otf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-ExtraLight.otf.woff b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-ExtraLight.otf.woff
new file mode 100644
index 0000000..559b740
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-ExtraLight.otf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-ExtraLightIt.otf.woff b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-ExtraLightIt.otf.woff
new file mode 100644
index 0000000..e8fbeb8
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-ExtraLightIt.otf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-It.otf.woff b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-It.otf.woff
new file mode 100644
index 0000000..4b8af41
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-It.otf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-Light.otf.woff b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-Light.otf.woff
new file mode 100644
index 0000000..10490ec
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-Light.otf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-LightIt.otf.woff b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-LightIt.otf.woff
new file mode 100644
index 0000000..13532d7
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-LightIt.otf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-Regular.otf.woff b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-Regular.otf.woff
new file mode 100644
index 0000000..04739e7
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-Regular.otf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-Semibold.otf.woff b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-Semibold.otf.woff
new file mode 100644
index 0000000..17d744d
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-Semibold.otf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-SemiboldIt.otf.woff b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-SemiboldIt.otf.woff
new file mode 100644
index 0000000..a5b5e1e
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/OTF/SourceSansPro-SemiboldIt.otf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-Black.ttf.woff b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-Black.ttf.woff
new file mode 100644
index 0000000..b7e8620
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-Black.ttf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-BlackIt.ttf.woff b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-BlackIt.ttf.woff
new file mode 100644
index 0000000..c3314b1
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-BlackIt.ttf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-Bold.ttf.woff b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-Bold.ttf.woff
new file mode 100644
index 0000000..d1d40f8
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-Bold.ttf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-BoldIt.ttf.woff b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-BoldIt.ttf.woff
new file mode 100644
index 0000000..ef6ff51
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-BoldIt.ttf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-ExtraLight.ttf.woff b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-ExtraLight.ttf.woff
new file mode 100644
index 0000000..1e6c94d
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-ExtraLight.ttf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-ExtraLightIt.ttf.woff b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-ExtraLightIt.ttf.woff
new file mode 100644
index 0000000..7a408b1
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-ExtraLightIt.ttf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-It.ttf.woff b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-It.ttf.woff
new file mode 100644
index 0000000..4d54bc9
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-It.ttf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-Light.ttf.woff b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-Light.ttf.woff
new file mode 100644
index 0000000..1706d57
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-Light.ttf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-LightIt.ttf.woff b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-LightIt.ttf.woff
new file mode 100644
index 0000000..87378d6
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-LightIt.ttf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-Regular.ttf.woff b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-Regular.ttf.woff
new file mode 100644
index 0000000..460ab12
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-Regular.ttf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-Semibold.ttf.woff b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-Semibold.ttf.woff
new file mode 100644
index 0000000..4337963
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-Semibold.ttf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-SemiboldIt.ttf.woff b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-SemiboldIt.ttf.woff
new file mode 100644
index 0000000..232c204
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF/TTF/SourceSansPro-SemiboldIt.ttf.woff
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-Black.otf.woff2 b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-Black.otf.woff2
new file mode 100644
index 0000000..d6f4b60
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-Black.otf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-BlackIt.otf.woff2 b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-BlackIt.otf.woff2
new file mode 100644
index 0000000..c52b3c8
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-BlackIt.otf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-Bold.otf.woff2 b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-Bold.otf.woff2
new file mode 100644
index 0000000..7d7f34b
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-Bold.otf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-BoldIt.otf.woff2 b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-BoldIt.otf.woff2
new file mode 100644
index 0000000..4d14ef7
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-BoldIt.otf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-ExtraLight.otf.woff2 b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-ExtraLight.otf.woff2
new file mode 100644
index 0000000..bdb21cb
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-ExtraLight.otf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-ExtraLightIt.otf.woff2 b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-ExtraLightIt.otf.woff2
new file mode 100644
index 0000000..aee47e3
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-ExtraLightIt.otf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-It.otf.woff2 b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-It.otf.woff2
new file mode 100644
index 0000000..00e212c
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-It.otf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-Light.otf.woff2 b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-Light.otf.woff2
new file mode 100644
index 0000000..2dd7ca3
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-Light.otf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-LightIt.otf.woff2 b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-LightIt.otf.woff2
new file mode 100644
index 0000000..5ee08be
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-LightIt.otf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-Regular.otf.woff2 b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-Regular.otf.woff2
new file mode 100644
index 0000000..d76e390
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-Regular.otf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-Semibold.otf.woff2 b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-Semibold.otf.woff2
new file mode 100644
index 0000000..8c27c18
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-Semibold.otf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-SemiboldIt.otf.woff2 b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-SemiboldIt.otf.woff2
new file mode 100644
index 0000000..f963d78
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/OTF/SourceSansPro-SemiboldIt.otf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-Black.ttf.woff2 b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-Black.ttf.woff2
new file mode 100644
index 0000000..c90d078
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-Black.ttf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-BlackIt.ttf.woff2 b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-BlackIt.ttf.woff2
new file mode 100644
index 0000000..b87e22c
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-BlackIt.ttf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-Bold.ttf.woff2 b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-Bold.ttf.woff2
new file mode 100644
index 0000000..0f46f3e
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-Bold.ttf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-BoldIt.ttf.woff2 b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-BoldIt.ttf.woff2
new file mode 100644
index 0000000..8007df6
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-BoldIt.ttf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-ExtraLight.ttf.woff2 b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-ExtraLight.ttf.woff2
new file mode 100644
index 0000000..b715f27
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-ExtraLight.ttf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-ExtraLightIt.ttf.woff2 b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-ExtraLightIt.ttf.woff2
new file mode 100644
index 0000000..d8f9d29
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-ExtraLightIt.ttf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-It.ttf.woff2 b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-It.ttf.woff2
new file mode 100644
index 0000000..a008526
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-It.ttf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-Light.ttf.woff2 b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-Light.ttf.woff2
new file mode 100644
index 0000000..d8b610a
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-Light.ttf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-LightIt.ttf.woff2 b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-LightIt.ttf.woff2
new file mode 100644
index 0000000..e0eebac
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-LightIt.ttf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-Regular.ttf.woff2 b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-Regular.ttf.woff2
new file mode 100644
index 0000000..0dd3464
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-Regular.ttf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-Semibold.ttf.woff2 b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-Semibold.ttf.woff2
new file mode 100644
index 0000000..2526d2e
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-Semibold.ttf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-SemiboldIt.ttf.woff2 b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-SemiboldIt.ttf.woff2
new file mode 100644
index 0000000..606935a
--- /dev/null
+++ b/src/assets/source-sans-pro/WOFF2/TTF/SourceSansPro-SemiboldIt.ttf.woff2
Binary files differ
diff --git a/src/assets/source-sans-pro/bower.json b/src/assets/source-sans-pro/bower.json
new file mode 100644
index 0000000..dfe14f2
--- /dev/null
+++ b/src/assets/source-sans-pro/bower.json
@@ -0,0 +1,17 @@
+{
+ "name": "source-sans-pro",
+ "version": "2.020R-ro/1.075R-it",
+ "main": "source-sans-pro.css",
+ "homepage": "https://github.com/adobe-fonts/source-sans-pro",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/adobe-fonts/source-sans-pro.git"
+ },
+ "authors": [
+ { "name": "Paul D. Hunt" }
+ ],
+ "description": "Source Sans Pro font family by Adobe",
+ "license": "SIL OFL 1.1",
+ "keywords": ["font", "sourcesans", "sourcesanspro", "source sans", "source sans pro"],
+ "ignore": ["**/.*"]
+}
diff --git a/src/assets/source-sans-pro/source-sans-pro.css b/src/assets/source-sans-pro/source-sans-pro.css
new file mode 100644
index 0000000..85aa101
--- /dev/null
+++ b/src/assets/source-sans-pro/source-sans-pro.css
@@ -0,0 +1,154 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+@font-face{
+ font-family: 'Source Sans Pro';
+ font-weight: 200;
+ font-style: normal;
+ font-stretch: normal;
+ src: url('EOT/SourceSansPro-ExtraLight.eot') format('embedded-opentype'),
+ url('WOFF2/TTF/SourceSansPro-ExtraLight.ttf.woff2') format('woff2'),
+ url('WOFF/OTF/SourceSansPro-ExtraLight.otf.woff') format('woff'),
+ url('OTF/SourceSansPro-ExtraLight.otf') format('opentype'),
+ url('TTF/SourceSansPro-ExtraLight.ttf') format('truetype');
+}
+
+@font-face{
+ font-family: 'Source Sans Pro';
+ font-weight: 200;
+ font-style: italic;
+ font-stretch: normal;
+ src: url('EOT/SourceSansPro-ExtraLightIt.eot') format('embedded-opentype'),
+ url('WOFF2/TTF/SourceSansPro-ExtraLightIt.ttf.woff2') format('woff2'),
+ url('WOFF/OTF/SourceSansPro-ExtraLightIt.otf.woff') format('woff'),
+ url('OTF/SourceSansPro-ExtraLightIt.otf') format('opentype'),
+ url('TTF/SourceSansPro-ExtraLightIt.ttf') format('truetype');
+}
+
+@font-face{
+ font-family: 'Source Sans Pro';
+ font-weight: 300;
+ font-style: normal;
+ font-stretch: normal;
+ src: url('EOT/SourceSansPro-Light.eot') format('embedded-opentype'),
+ url('WOFF2/TTF/SourceSansPro-Light.ttf.woff2') format('woff2'),
+ url('WOFF/OTF/SourceSansPro-Light.otf.woff') format('woff'),
+ url('OTF/SourceSansPro-Light.otf') format('opentype'),
+ url('TTF/SourceSansPro-Light.ttf') format('truetype');
+}
+
+@font-face{
+ font-family: 'Source Sans Pro';
+ font-weight: 300;
+ font-style: italic;
+ font-stretch: normal;
+ src: url('EOT/SourceSansPro-LightIt.eot') format('embedded-opentype'),
+ url('WOFF2/TTF/SourceSansPro-LightIt.ttf.woff2') format('woff2'),
+ url('WOFF/OTF/SourceSansPro-LightIt.otf.woff') format('woff'),
+ url('OTF/SourceSansPro-LightIt.otf') format('opentype'),
+ url('TTF/SourceSansPro-LightIt.ttf') format('truetype');
+}
+
+@font-face{
+ font-family: 'Source Sans Pro';
+ font-weight: 400;
+ font-style: normal;
+ font-stretch: normal;
+ src: url('EOT/SourceSansPro-Regular.eot') format('embedded-opentype'),
+ url('WOFF2/TTF/SourceSansPro-Regular.ttf.woff2') format('woff2'),
+ url('WOFF/OTF/SourceSansPro-Regular.otf.woff') format('woff'),
+ url('OTF/SourceSansPro-Regular.otf') format('opentype'),
+ url('TTF/SourceSansPro-Regular.ttf') format('truetype');
+}
+
+@font-face{
+ font-family: 'Source Sans Pro';
+ font-weight: 400;
+ font-style: italic;
+ font-stretch: normal;
+ src: url('EOT/SourceSansPro-It.eot') format('embedded-opentype'),
+ url('WOFF2/TTF/SourceSansPro-It.ttf.woff2') format('woff2'),
+ url('WOFF/OTF/SourceSansPro-It.otf.woff') format('woff'),
+ url('OTF/SourceSansPro-It.otf') format('opentype'),
+ url('TTF/SourceSansPro-It.ttf') format('truetype');
+}
+
+@font-face{
+ font-family: 'Source Sans Pro';
+ font-weight: 600;
+ font-style: normal;
+ font-stretch: normal;
+ src: url('EOT/SourceSansPro-Semibold.eot') format('embedded-opentype'),
+ url('WOFF2/TTF/SourceSansPro-Semibold.ttf.woff2') format('woff2'),
+ url('WOFF/OTF/SourceSansPro-Semibold.otf.woff') format('woff'),
+ url('OTF/SourceSansPro-Semibold.otf') format('opentype'),
+ url('TTF/SourceSansPro-Semibold.ttf') format('truetype');
+}
+
+@font-face{
+ font-family: 'Source Sans Pro';
+ font-weight: 600;
+ font-style: italic;
+ font-stretch: normal;
+ src: url('EOT/SourceSansPro-SemiboldIt.eot') format('embedded-opentype'),
+ url('WOFF2/TTF/SourceSansPro-SemiboldIt.ttf.woff2') format('woff2'),
+ url('WOFF/OTF/SourceSansPro-SemiboldIt.otf.woff') format('woff'),
+ url('OTF/SourceSansPro-SemiboldIt.otf') format('opentype'),
+ url('TTF/SourceSansPro-SemiboldIt.ttf') format('truetype');
+}
+
+@font-face{
+ font-family: 'Source Sans Pro';
+ font-weight: 700;
+ font-style: normal;
+ font-stretch: normal;
+ src: url('EOT/SourceSansPro-Bold.eot') format('embedded-opentype'),
+ url('WOFF2/TTF/SourceSansPro-Bold.ttf.woff2') format('woff2'),
+ url('WOFF/OTF/SourceSansPro-Bold.otf.woff') format('woff'),
+ url('OTF/SourceSansPro-Bold.otf') format('opentype'),
+ url('TTF/SourceSansPro-Bold.ttf') format('truetype');
+}
+
+@font-face{
+ font-family: 'Source Sans Pro';
+ font-weight: 700;
+ font-style: italic;
+ font-stretch: normal;
+ src: url('EOT/SourceSansPro-BoldIt.eot') format('embedded-opentype'),
+ url('WOFF2/TTF/SourceSansPro-BoldIt.ttf.woff2') format('woff2'),
+ url('WOFF/OTF/SourceSansPro-BoldIt.otf.woff') format('woff'),
+ url('OTF/SourceSansPro-BoldIt.otf') format('opentype'),
+ url('TTF/SourceSansPro-BoldIt.ttf') format('truetype');
+}
+
+@font-face{
+ font-family: 'Source Sans Pro';
+ font-weight: 900;
+ font-style: normal;
+ font-stretch: normal;
+ src: url('EOT/SourceSansPro-Black.eot') format('embedded-opentype'),
+ url('WOFF2/TTF/SourceSansPro-Black.ttf.woff2') format('woff2'),
+ url('WOFF/OTF/SourceSansPro-Black.otf.woff') format('woff'),
+ url('OTF/SourceSansPro-Black.otf') format('opentype'),
+ url('TTF/SourceSansPro-Black.ttf') format('truetype');
+}
+
+@font-face{
+ font-family: 'Source Sans Pro';
+ font-weight: 900;
+ font-style: italic;
+ font-stretch: normal;
+ src: url('EOT/SourceSansPro-BlackIt.eot') format('embedded-opentype'),
+ url('WOFF2/TTF/SourceSansPro-BlackIt.ttf.woff2') format('woff2'),
+ url('WOFF/OTF/SourceSansPro-BlackIt.otf.woff') format('woff'),
+ url('OTF/SourceSansPro-BlackIt.otf') format('opentype'),
+ url('TTF/SourceSansPro-BlackIt.ttf') format('truetype');
+}
diff --git a/src/environments/environment.prod.spec.ts b/src/environments/environment.prod.spec.ts
new file mode 100644
index 0000000..3214e5e
--- /dev/null
+++ b/src/environments/environment.prod.spec.ts
@@ -0,0 +1,30 @@
+/*
+ ******************************************************************************
+ * Copyright © 2018 PTA GmbH.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ *
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ ******************************************************************************
+ */
+import { TestBed } from '@angular/core/testing';
+import { environment } from './environment.prod';
+
+
+describe('Enviroment: Production', () => {
+
+
+ beforeEach(() => {
+
+ TestBed.configureTestingModule({
+ declarations: [environment]
+ });
+ });
+
+ it('should create an instance', () => {
+ expect(environment.production).toBeTruthy();
+ });
+});
+
diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts
new file mode 100644
index 0000000..d28c44b
--- /dev/null
+++ b/src/environments/environment.prod.ts
@@ -0,0 +1,14 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+export const environment = {
+ production: true
+};
diff --git a/src/environments/environment.spec.ts b/src/environments/environment.spec.ts
new file mode 100644
index 0000000..cc512d0
--- /dev/null
+++ b/src/environments/environment.spec.ts
@@ -0,0 +1,30 @@
+/*
+ ******************************************************************************
+ * Copyright © 2018 PTA GmbH.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ *
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ ******************************************************************************
+ */
+import { TestBed } from '@angular/core/testing';
+import { environment } from './environment';
+
+
+describe('Enviroment: Dev', () => {
+
+
+ beforeEach(() => {
+
+ TestBed.configureTestingModule({
+ declarations: [environment]
+ });
+ });
+
+ it('should create an instance', () => {
+ expect(environment.production).toBeFalsy();
+ });
+});
+
diff --git a/src/environments/environment.ts b/src/environments/environment.ts
new file mode 100644
index 0000000..a114969
--- /dev/null
+++ b/src/environments/environment.ts
@@ -0,0 +1,19 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+// The file contents for the current environment will overwrite these during build.
+// The build system defaults to the dev environment which uses `environment.ts`, but if you do
+// `ng build --env=prod` then `environment.prod.ts` will be used instead.
+// The list of which env maps to which file can be found in `.angular-cli.json`.
+
+export const environment = {
+ production: false
+};
diff --git a/src/favicon.ico b/src/favicon.ico
new file mode 100644
index 0000000..8081c7c
--- /dev/null
+++ b/src/favicon.ico
Binary files differ
diff --git a/src/index.html b/src/index.html
new file mode 100644
index 0000000..0ce3dc7
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<!--
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+-->
+<html>
+
+<head>
+ <meta http-equiv="X-UA-Compatible" content="IE=11" />
+ <meta charset="utf-8">
+ <title>openKONSEQUENZ</title>
+ <base href="./">
+
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <link rel="icon" type="image/x-icon" href="assets/img/logo_openkonsequenz.png">
+</head>
+
+<body class="body-style">
+ <app-root>Loading...</app-root>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..28fdf06
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,23 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+import { enableProdMode } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+import { environment } from './environments/environment';
+
+if (environment.production) {
+ enableProdMode();
+}
+
+platformBrowserDynamic().bootstrapModule(AppModule)
+ .catch(err => console.log(err));
diff --git a/src/polyfills.ts b/src/polyfills.ts
new file mode 100644
index 0000000..7c71611
--- /dev/null
+++ b/src/polyfills.ts
@@ -0,0 +1,93 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+/**
+ * This file includes polyfills needed by Angular and is loaded before the app.
+ * You can add your own extra polyfills to this file.
+ *
+ * This file is divided into 2 sections:
+ * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
+ * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
+ * file.
+ *
+ * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
+ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
+ * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
+ *
+ * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
+ */
+
+/***************************************************************************************************
+ * BROWSER POLYFILLS
+ */
+
+/** IE9, IE10 and IE11 requires all of the following polyfills. **/
+import 'core-js/es6/symbol';
+import 'core-js/es6/object';
+import 'core-js/es6/function';
+import 'core-js/es6/parse-int';
+import 'core-js/es6/parse-float';
+import 'core-js/es6/number';
+import 'core-js/es6/math';
+import 'core-js/es6/string';
+import 'core-js/es6/date';
+import 'core-js/es6/array';
+import 'core-js/es6/regexp';
+import 'core-js/es6/map';
+import 'core-js/es6/weak-map';
+import 'core-js/es6/set';
+
+
+
+/** IE10 and IE11 requires the following for NgClass support on SVG elements */
+import 'classlist.js'; // Run `npm install --save classlist.js`.
+
+/** IE10 and IE11 requires the following for the Reflect API. */
+import 'core-js/es6/reflect';
+
+
+/** Evergreen browsers require these. **/
+// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
+import 'core-js/es7/reflect';
+
+
+/**
+ * Required to support Web Animations `@angular/platform-browser/animations`.
+ * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
+ **/
+import 'web-animations-js'; // Run `npm install --save web-animations-js`.
+
+/**
+ * By default, zone.js will patch all possible macroTask and DomEvents
+ * user can disable parts of macroTask/DomEvents patch by setting following flags
+ */
+
+// (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
+// (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
+// (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
+
+/*
+* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
+* with the following flag, it will bypass `zone.js` patch for IE/Edge
+*/
+// (window as any).__Zone_enable_cross_context_check = true;
+
+/***************************************************************************************************
+ * Zone JS is required by default for Angular itself.
+ */
+import 'zone.js/dist/zone'; // Included with Angular CLI.
+
+
+
+/***************************************************************************************************
+ * APPLICATION IMPORTS
+ */
+import 'core-js/es7/array';
diff --git a/src/styles.css b/src/styles.css
new file mode 100644
index 0000000..436359e
--- /dev/null
+++ b/src/styles.css
@@ -0,0 +1,405 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+/* You can add global styles to this file, and also import other style files */
+
+@import url("assets/source-sans-pro/source-sans-pro.css");
+@import url("assets/css/main.css");
+@import url("assets/css/custom.css");
+/* General styling */
+
+@import url("../node_modules/primeicons/primeicons.css");
+@import url("../node_modules/primeng/resources/themes/nova-light/theme.css");
+@import url("../node_modules/primeng/resources/primeng.min.css");
+@import "~ng2-tree/styles.css";
+@import "~ag-grid/dist/styles/ag-grid.css";
+@import "~ag-grid/dist/styles/ag-theme-balham.css";
+/* Change autocomplete field color for chrome (default was yellow) ;) */
+
+input:-webkit-autofill,
+input:-webkit-autofill:hover,
+input:-webkit-autofill:focus,
+input:-webkit-autofill:active {
+ transition: background-color 5000s ease-in-out 0s;
+}
+
+html {
+ overflow-y: auto;
+ -ms-overflow-style: scrollbar;
+}
+
+body {
+ background: #f8fafd;
+}
+
+.margin-top {
+ margin-top: 15px;
+}
+
+.margin-zero {
+ margin: 0;
+}
+
+.spacer {
+ margin-top: 15px;
+}
+
+/* General panel styling */
+
+div.panel-group {
+ margin: 15px;
+}
+
+div.panel-default {
+ min-height: 0;
+ margin: 15px 0px;
+}
+
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ div.panel-default {
+ padding: 1px 0px 0px 1px;
+ }
+}
+
+div.panel-default>.panel-heading {
+ background-color: #f5f8fc;
+}
+
+/* General button styling */
+
+.btn {
+ border-radius: 6px;
+}
+
+/* General table styling */
+
+.table-striped {
+ margin: 0;
+ padding: 5px;
+ vertical-align: middle;
+}
+
+/* Ag-Grid stuff */
+
+.ag-theme-balham {
+ font-size: 14px;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+}
+
+.ag-theme-balham .ag-header {
+ background-color: #f5f8fc;
+}
+
+span.ag-row-drag {
+ cursor: move;
+ /* fallback if grab cursor is unsupported */
+ /*cursor: grab;
+ cursor: -moz-grab;
+ cursor: -webkit-grab;*/
+}
+
+span.ag-row-drag:active {
+ cursor: grabbing;
+ cursor: -moz-grabbing;
+ cursor: -webkit-grabbing;
+}
+
+/* Ag-Grid stuff end */
+
+.grid-measures-header {
+ font-size: 14px;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+}
+
+.grid-measure-check-col {
+ line-height: 1.42857143;
+ width: 1%;
+ padding-right: 6px;
+}
+
+.grid-measure-tab-date-col {
+ width: 8%;
+}
+
+.grid-measure-tab-id {
+ width: 10%;
+}
+
+.grid-measure-mode {
+ width: 40px;
+}
+
+.grid-measure-mode span {
+ margin-left: 0px !important;
+}
+
+.grid-measure-tab-branche {
+ width: 5%;
+ text-align: center;
+ padding-right: 2px;
+}
+
+.grid-measure-tab-status .ag-cell-label-container .ag-header-cell-label {
+ justify-content: center;
+}
+
+.grid-measure-tab-branche .ag-cell-label-container .ag-header-cell-label {
+ justify-content: center;
+}
+
+.grid-measure-tab-title {
+ width: 30%;
+}
+
+.grid-measure-tab-createUser {
+ width: 15%;
+}
+
+.grid-measure-tab-affectedResource {
+ width: 20%;
+}
+
+.grid-measure-tab-status {
+ width: 10%;
+ text-align: center;
+}
+.ag-layout-normal .ag-body-viewport{
+ height: 99%!important;
+}
+.table-striped>tbody>tr:nth-of-type(odd) {
+ background: #e9f0f9;
+}
+
+.table>thead>tr>th {
+ padding: 5px;
+ vertical-align: bottom;
+}
+
+.table>tbody>tr>td {
+ padding: 5px;
+ vertical-align: middle;
+}
+
+/* General dialog styling */
+
+.mat-dialog-container {
+ background: #f8fafd;
+}
+
+.dialog {
+ overflow: auto;
+ width: 75vw;
+ height: 75vh;
+ margin: 0px 0px 15px 0px;
+}
+
+/* Login styling */
+
+.login-outer {
+ width: 100%;
+ height: 90%;
+ position: absolute;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #f8fafd;
+}
+
+.login-inner {
+ width: 240px;
+ height: 320px;
+}
+
+.login-error-message {
+ color: red;
+}
+
+.login-margin-top5 {
+ margin-top: 5px;
+}
+
+.login-margin-top10 {
+ margin-top: 10px;
+}
+
+.maincontent {
+ background: #ffffff;
+ padding: 0px 0px 1px 0px;
+ margin: 15px;
+ color: #333333;
+ min-width: inherit;
+}
+
+.maincontent>.row {
+ background: #f8fafd;
+ margin: 0;
+ padding: 0;
+}
+
+/* Responsibility dialog styling */
+
+.responsibility-dialog {
+ overflow: auto;
+ width: 40vw;
+ height: auto;
+ margin: 0px 0px 15px 0px;
+}
+
+.responsibility-column-striped>thead th {
+ text-align: center;
+}
+
+.responsibility-column-striped>tbody td:nth-of-type(even) {
+ background: #f5f8fc;
+}
+
+.responsibility-column-striped>tbody td {
+ text-align: center;
+}
+
+.responsibility-column-striped>tbody td:first-child {
+ text-align: left;
+}
+
+/* Entry dialog styling */
+
+.entry-dialog {
+ overflow: auto;
+ width: 75vw;
+ max-height: 70vh;
+ margin: 0px 0px 15px 0px;
+}
+
+.entry-textarea {
+ resize: none;
+ width: 100%;
+ height: 10vh;
+}
+
+.input-group {
+ width: 100%;
+ white-space: nowrap;
+}
+
+.entry-input {
+ width: 100%;
+}
+
+.panel-body {}
+
+/* Dropdown logout menu styling */
+
+.dropdown-open {
+ color: #537798;
+}
+
+.dropdown-open:hover {
+ background: #b7cbda;
+}
+
+.navbar-default .navbar-nav>.open>a,
+.navbar-default .navbar-nav>.open>a:focus {
+ color: #0080c0;
+ background: #b7cbda;
+}
+
+.dropdown-menu {
+ top: 43px;
+ min-width: 160px;
+ background: #e9f0f9;
+}
+
+.dropdown-menu>.dropdown>a:hover {
+ background: #b7cbda;
+ color: #0080c0;
+}
+
+/* Logout dialog styling */
+
+.logout-dialog {
+ width: 320px;
+ height: 240px;
+}
+
+div.center {
+ text-align: center;
+}
+
+div.center-margin {
+ text-align: center;
+ margin: 15px;
+}
+
+.custom-drop-down .c-token span {
+ pointer-events: auto;
+ position: relative;
+}
+
+.custom-drop-down .c-token {
+ background: #337ab7 !important;
+}
+
+.custom-drop-down .pure-checkbox input[type="checkbox"]:hover+label:before {
+ border-color: #337ab7 !important;
+}
+
+.custom-drop-down .pure-checkbox input[type="checkbox"]+label:before {
+ color: #337ab7 !important;
+}
+
+.custom-drop-down .pure-checkbox input[type="checkbox"]+label:before {
+ border: 2px solid #337ab7 !important;
+}
+
+.custom-drop-down .pure-checkbox input[type="checkbox"]+label:after {
+ background-color: #337ab7 !important;
+}
+
+.custom-drop-down .pure-checkbox input[type="checkbox"]:checked+label:before {
+ background: #337ab7 !important;
+}
+
+.custom-drop-down .selected-list button {
+ box-shadow: none !important;
+}
+
+.tooltip>.tooltip-inner {
+ background-color: #f5f8fc;
+ color: black;
+ word-break: break-all;
+ border: 1px solid grey;
+}
+
+.daterangepicker.dropdown-menu.ltr {
+ max-width: 496px;
+}
+
+.daterangepicker.dropdown-menu.ltr.single {
+ max-width: 255px;
+}
+
+.cdk-overlay-pane {
+ position: relative !important;
+}
+
+.custom-dark-blue {
+ color: #0080c0;
+ ;
+}
+
+.btn-primary {
+ z-index: 0 !important;
+}
+
+.nav-tabs>li>a {
+ border: 1px solid #ddd;
+}
\ No newline at end of file
diff --git a/src/test.ts b/src/test.ts
new file mode 100644
index 0000000..cf24d37
--- /dev/null
+++ b/src/test.ts
@@ -0,0 +1,31 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+// This file is required by karma.conf.js and loads recursively all the .spec and framework files
+
+import 'zone.js/dist/zone-testing';
+import { getTestBed } from '@angular/core/testing';
+import {
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting
+} from '@angular/platform-browser-dynamic/testing';
+
+declare const require: any;
+
+// First, initialize the Angular testing environment.
+getTestBed().initTestEnvironment(
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting()
+);
+// Then we find all the tests.
+const context = require.context('./', true, /\.spec\.ts$/);
+// And load the modules.
+context.keys().map(context);
diff --git a/src/tsconfig.app.json b/src/tsconfig.app.json
new file mode 100644
index 0000000..a5c88fb
--- /dev/null
+++ b/src/tsconfig.app.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../out-tsc/app",
+ "baseUrl": "./",
+ "module": "es2015",
+ "types": []
+ },
+ "exclude": [
+ "test.ts",
+ "**/*.spec.ts",
+ "**/app/testing/*.ts"
+ ]
+}
diff --git a/src/tsconfig.spec.json b/src/tsconfig.spec.json
new file mode 100644
index 0000000..1a18e6d
--- /dev/null
+++ b/src/tsconfig.spec.json
@@ -0,0 +1,20 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../out-tsc/spec",
+ "baseUrl": "./",
+ "module": "commonjs",
+ "types": [
+ "jasmine",
+ "node"
+ ]
+ },
+ "files": [
+ "test.ts",
+ "polyfills.ts"
+ ],
+ "include": [
+ "**/*.spec.ts",
+ "**/*.d.ts"
+ ]
+}
diff --git a/src/typings.d.ts b/src/typings.d.ts
new file mode 100644
index 0000000..7166679
--- /dev/null
+++ b/src/typings.d.ts
@@ -0,0 +1,17 @@
+/*
+******************************************************************************
+* Copyright © 2018 PTA GmbH.
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License v1.0
+* which accompanies this distribution, and is available at
+*
+* http://www.eclipse.org/legal/epl-v10.html
+*
+******************************************************************************
+*/
+
+/* SystemJS module definition */
+declare var module: NodeModule;
+interface NodeModule {
+ id: string;
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..33ed6b4
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "outDir": "./dist/out-tsc",
+ "sourceMap": true,
+ "declaration": false,
+ "moduleResolution": "node",
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "target": "es5",
+ "allowJs": true,
+ "typeRoots": [
+ "node_modules/@types"
+ ],
+ "lib": [
+ "es2017",
+ "dom"
+ ]
+ }
+}
diff --git a/tslint.json b/tslint.json
new file mode 100644
index 0000000..d8d3956
--- /dev/null
+++ b/tslint.json
@@ -0,0 +1,142 @@
+{
+ "rulesDirectory": [
+ "node_modules/codelyzer"
+ ],
+ "rules": {
+ "arrow-return-shorthand": true,
+ "callable-types": true,
+ "class-name": true,
+ "comment-format": [
+ true,
+ "check-space"
+ ],
+ "curly": true,
+ "deprecation": {
+ "severity": "warn"
+ },
+ "eofline": true,
+ "forin": true,
+ "import-blacklist": [
+ true,
+ "rxjs/Rx"
+ ],
+ "import-spacing": true,
+ "indent": [
+ true,
+ "spaces"
+ ],
+ "interface-over-type-literal": true,
+ "label-position": true,
+ "max-line-length": [
+ true,
+ 140
+ ],
+ "member-access": false,
+ "member-ordering": [
+ true,
+ {
+ "order": [
+ "static-field",
+ "instance-field",
+ "static-method",
+ "instance-method"
+ ]
+ }
+ ],
+ "no-arg": true,
+ "no-bitwise": true,
+ "no-console": [
+ true,
+ "debug",
+ "info",
+ "time",
+ "timeEnd",
+ "trace"
+ ],
+ "no-construct": true,
+ "no-debugger": true,
+ "no-duplicate-super": true,
+ "no-empty": false,
+ "no-empty-interface": true,
+ "no-eval": true,
+ "no-inferrable-types": [
+ true,
+ "ignore-params"
+ ],
+ "no-misused-new": true,
+ "no-non-null-assertion": true,
+ "no-shadowed-variable": true,
+ "no-string-literal": false,
+ "no-string-throw": true,
+ "no-switch-case-fall-through": true,
+ "no-trailing-whitespace": true,
+ "no-unnecessary-initializer": true,
+ "no-unused-expression": true,
+ "no-use-before-declare": true,
+ "no-var-keyword": true,
+ "object-literal-sort-keys": false,
+ "one-line": [
+ true,
+ "check-open-brace",
+ "check-catch",
+ "check-else",
+ "check-whitespace"
+ ],
+ "prefer-const": true,
+ "quotemark": [
+ true,
+ "single"
+ ],
+ "radix": true,
+ "semicolon": [
+ true,
+ "always"
+ ],
+ "triple-equals": [
+ true,
+ "allow-null-check"
+ ],
+ "typedef-whitespace": [
+ true,
+ {
+ "call-signature": "nospace",
+ "index-signature": "nospace",
+ "parameter": "nospace",
+ "property-declaration": "nospace",
+ "variable-declaration": "nospace"
+ }
+ ],
+ "unified-signatures": true,
+ "variable-name": false,
+ "whitespace": [
+ true,
+ "check-branch",
+ "check-decl",
+ "check-operator",
+ "check-separator",
+ "check-type"
+ ],
+ "directive-selector": [
+ true,
+ "attribute",
+ "app",
+ "camelCase"
+ ],
+ "component-selector": [
+ true,
+ "element",
+ "app",
+ "kebab-case"
+ ],
+ "no-output-on-prefix": true,
+ "use-input-property-decorator": true,
+ "use-output-property-decorator": true,
+ "use-host-property-decorator": true,
+ "no-input-rename": true,
+ "no-output-rename": true,
+ "use-life-cycle-interface": true,
+ "use-pipe-transform-interface": true,
+ "component-class-suffix": true,
+ "directive-class-suffix": true
+ }
+}