fixed issues due to angular redux
diff --git a/.angular-cli.json b/.angular-cli.json
index f3e3c41..2b63cca 100644
--- a/.angular-cli.json
+++ b/.angular-cli.json
@@ -1,15 +1,16 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"project": {
- "name": "fineract-cn-web-app"
+ "version": "1.0.0",
+ "name": "fims"
},
"apps": [
{
"root": "src",
"outDir": "dist",
"assets": [
- "assets",
- "favicon.ico"
+ "favicon.png",
+ "assets"
],
"index": "index.html",
"main": "main.ts",
@@ -18,10 +19,16 @@
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
+ "mobile": false,
"styles": [
- "styles.scss"
+ "styles.scss",
+ "theme.scss",
+ "../node_modules/@covalent/core/common/platform.css"
],
- "scripts": [],
+ "scripts": [
+ "../node_modules/hammerjs/hammer.min.js",
+ "../node_modules/showdown/dist/showdown.js"
+ ],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
@@ -29,25 +36,13 @@
}
}
],
+ "addons": [],
+ "packages": [],
"e2e": {
"protractor": {
"config": "./protractor.conf.js"
}
},
- "lint": [
- {
- "project": "src/tsconfig.app.json",
- "exclude": "**/node_modules/**"
- },
- {
- "project": "src/tsconfig.spec.json",
- "exclude": "**/node_modules/**"
- },
- {
- "project": "e2e/tsconfig.e2e.json",
- "exclude": "**/node_modules/**"
- }
- ],
"test": {
"karma": {
"config": "./karma.conf.js"
@@ -55,6 +50,9 @@
},
"defaults": {
"styleExt": "scss",
- "component": {}
+ "serve": {
+ "port": 4200,
+ "host": "localhost"
+ }
}
}
diff --git a/.editorconfig b/.editorconfig
index 6e87a00..da0310f 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,13 +1,13 @@
-# Editor configuration, see http://editorconfig.org
+# editorconfig.org
root = true
[*]
-charset = utf-8
indent_style = space
indent_size = 2
-insert_final_newline = true
+end_of_line = lf
+charset = utf-8
trim_trailing_whitespace = true
+insert_final_newline = true
[*.md]
-max_line_length = off
-trim_trailing_whitespace = false
+trim_trailing_whitespace = false
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..46ae083
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,30 @@
+This repository's issues are reserved for feature requests and bug reports.
+
+### Do you want to request a *feature* or report a *bug*?
+
+#### Feature Request
+
+please first make sure your request falls under the official Material Design spec guidelines https://material.google.com/
+
+#### Bug Report
+
+please provide steps to reproduce and if possible screenhots or animated Gifs.
+you can easily create animated Gif with this free PC/OSX App: http://www.cockos.com/licecap/
+
+##### Screenshots or link to CodePen/Plunker/JSfiddle
+
+
+#### What is the expected behavior?
+
+
+#### What is the motivation / use case for changing the behavior?
+
+
+#### Which version of Angular and Material, and which browser and OS does this issue affect?
+
+Did this work in previous versions of Angular / Material?
+Please also test with the latest stable and snapshot versions.
+
+
+##### Other information
+(e.g. detailed explanation, stacktraces, related issues, suggestions how to fix)
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..cff04b7
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,17 @@
+## Description
+
+Talk about the great work you've done!
+
+### What's included?
+
+- One
+- Two
+- Three
+
+#### Test Steps
+
+- [ ] do this
+- [ ] then this
+- [ ] finally this
+
+##### Screenshots or link to CodePen/Plunker/JSfiddle
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index eabf65e..7346eca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,43 +2,31 @@
# compiled output
/dist
-/dist-server
+/deploy
/tmp
-/out-tsc
# dependencies
/node_modules
+/bower_components
# 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
+.idea
+*.iml
+/.vscode
# misc
/.sass-cache
/connect.lock
-/coverage
+/coverage/*
/libpeerconnection.log
npm-debug.log
-yarn-error.log
testem.log
/typings
+/.vagrant
# e2e
/e2e/*.js
/e2e/*.map
-# System Files
+#System Files
.DS_Store
-Thumbs.db
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..4afbd4b
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,47 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+sudo: required
+
+dist: trusty
+
+language: node_js
+
+node_js:
+- '6.9.4'
+
+branches:
+ only:
+ - develop
+
+before_script:
+ - export CHROME_BIN=chromium-browser
+ - export DISPLAY=:99.0
+ - sh -e /etc/init.d/xvfb start
+
+install:
+ - npm install
+
+script:
+ - npm run tslint
+ - npm run test
+ - npm run build
+
+notifications:
+ email: false
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..0255d60
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,80 @@
+# Contributing
+
+## Take Your First Steps
+
+### Understand the basics
+
+Not sure what a pull request is, or how to submit one? Take a look at GitHub's excellent [help
+documentation](https://help.github.com/articles/using-pull-requests/) first.
+
+### Search Issues first; create an issue if necessary
+
+Is there already an issue that addresses your concern? Do a bit of searching in our [issue
+tracker](https://issues.apache.org/jira/browse/FINCN-3?jql=project%20%3D%20FINCN%20AND%20component%20%3D%20fineract-cn-fims-web-app) to see if you can find something
+similar. If you do not find something similar, please create a new issue before submitting a pull
+request unless the change is truly trivial -- for example: typo fixes, removing compiler warnings,
+etc.
+
+### Discuss non-trivial contribution ideas with committers
+
+If you're considering anything more than correcting a typo or fixing a minor bug, please discuss it
+on the [dev list](mailto:dev-subscribe@fineract.apage.org) before submitting a pull request. We're
+happy to provide guidance, but please spend an hour or two researching the subject on your own.
+
+### Sign the Contributor License Agreement
+
+Before we accept a non-trivial patch or pull request we will need you to sign the [Apache iCLA
+form](https://www.apache.org/licenses/icla.pdf). Signing the
+contributor's agreement does not grant anyone commit rights to the main repository, but it does mean
+that we can accept your contributions, and you will get an author credit if we do.
+
+## Create a Branch
+
+### Branch from `develop`
+
+Develop currently represents work toward Apache Fineract CN Framework 1.0.0. Please submit all pull requests
+there, even bug fixes and minor improvements.
+
+### Use short branch names
+
+Branches used when submitting pull requests should preferably be named according to issues prefixed
+FINCN followed by a dash and the issue number, e.g. 'FINCN-1234'. This is important, because
+branch names show up in the merge commits that result from accepting pull requests and should be as
+expressive and concise as possible.
+
+## Coding Conventions
+[Google Java Style](https://google.github.io/styleguide/javaguide.html) covers filenames, file
+organization, indentation, comments, declarations, statements, white space, naming conventions, and
+programming practices. All code written for Apache Fineract CN should follow these conventions except as noted
+below.
+
+### 3.1 License or copyright information, if present
+
+Content of the license header:
+
+```javascript
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+```
+
+### 4.4 Column Limit
+We've chosen to use a column limit of 100 characters.
+
+## Contributors
+Huge thanks to the following contributors (by github username).
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8dada3e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..a979824
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,21 @@
+Apache Fineract CN Fims Web App
+Copyright 2008-2018 The Apache Software Foundation
+
+This product includes software developed by The Apache Software
+Foundation (http://www.apache.org/).
+
+fims - notice for binary distribution
+==========================================
+fims includes caniuse(https://github.com/Fyrd/caniuse).
+fims elects to include this software in this distribution under the
+CC-BY-4.0 license. You can obtain a copy of the License at:
+https://creativecommons.org/licenses/by/4.0/
+
+fims includes spdx-expression-parse.js(https://github.com/kemitchell/spdx-expression-parse.js).
+fims includes spdx-exceptions(https://github.com/kemitchell/spdx-exceptions.json).
+The Linux Foundation and its contributors license the SPDX standard under the terms
+of the Creative Commons Attribution License 3.0 Unported (SPDX: "CC-BY-3.0").
+"SPDX" is a United States federally registered trademark of the Linux Foundation.
+fims elects to include this software in this distribution under the
+CC-BY-3.0 license. You can obtain a copy of the License at:
+http://spdx.org/licenses/CC-BY-3.0
diff --git a/README.md b/README.md
index 93ae45a..fefa308 100644
--- a/README.md
+++ b/README.md
@@ -1,27 +1,44 @@
-# FineractCnWebApp
+# QuickStart
-This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.7.2.
+## Setup
-## Development server
+* Ensure you have Node 6.10.0+ and NPM 3+ installed.
+* Install Node packages `npm i`
-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.
+## Development
+* Follow instructions at https://github.com/apache/fineract-cn-demo-server and start demo-server
+* Run local dev environment `npm run dev`
+* Go to http://localhost:4200
-## Code scaffolding
+## Production build
+* Run license check `npm run checkLicenses`
+* Run in production mode `npm run runProd`. This is only to test if AOT is working and should never be used in a production environment.
+* Build production assets `npm run build`. Files will be stored under /dist.
-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`.
+## Tests
+* Please follow the best practices here [Angular Testing](https://angular.io/docs/ts/latest/guide/testing.html)
+* Run karma tests `npm run test`
-## 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.
+## Versioning
+The version numbers follow the [Semantic Versioning](http://semver.org/) scheme.
-## Running unit tests
+In addition to MAJOR.MINOR.PATCH the following postfixes are used to indicate the development state.
-Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
+* snapshot - A release currently in development.
+* m - A _milestone_ release include specific sets of functions and are released as soon as the functionality is complete.
+* rc - A _release candidate_ is a version with potential to be a final product, considered _code complete_.
+* ga - _General availability_ indicates that this release is the best available version and is recommended for all usage.
-## Running end-to-end tests
+The versioning layout is {MAJOR}.{MINOR}.{PATCH}-{INDICATOR}[.{PATCH}]. Only milestones and release candidates can have patch versions. Some examples:
-Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
+1.2.3-snapshot
+1.3.5-m.1
+1.5.7-rc.2
+2.0.0-ga
-## Further help
+## Contributing
+See [CONTRIBUTING](CONTRIBUTING.md) file.
-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).
+## License
+See [LICENSE](LICENSE) file.
diff --git a/e2e/dashboard.e2e.ts b/e2e/dashboard.e2e.ts
new file mode 100644
index 0000000..4b50e78
--- /dev/null
+++ b/e2e/dashboard.e2e.ts
@@ -0,0 +1,18 @@
+import { browser, by, protractor, $, element, ProtractorExpectedConditions } from 'protractor';
+
+describe('basic e2e test with loading', function(): void {
+ let EC: ProtractorExpectedConditions = protractor.ExpectedConditions;
+ describe('home', function(): void {
+ browser.get('/');
+ it('should load quick access', function(): void {
+ expect(browser.getTitle()).toBe('Quick access');
+ // Waits for the element 'td-loading' to not be present on the dom.
+ browser.wait(EC.not(EC.presenceOf($('td-loading'))), 10000)
+ .then(() => {
+
+ // checks if elements were rendered
+ expect(element(by.id('dashboard-favorites-card')).isPresent()).toBe(true);
+ });
+ });
+ });
+});
diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json
new file mode 100644
index 0000000..656bdb1
--- /dev/null
+++ b/e2e/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "declaration": false,
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "outDir": "../dist/out-tsc-e2e",
+ "sourceMap": true,
+ "target": "es5",
+ "typeRoots": [
+ "../node_modules/@types"
+ ]
+ }
+}
diff --git a/karma.conf.js b/karma.conf.js
index af139fa..9065608 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -1,5 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+*/
+
// Karma configuration file, see link for more information
-// https://karma-runner.github.io/1.0/config/configuration-file.html
+// https://karma-runner.github.io/0.13/config/configuration-file.html
module.exports = function (config) {
config.set({
@@ -10,11 +29,29 @@
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
+ require('karma-firefox-launcher'),
require('@angular/cli/plugins/karma')
],
+ customLaunchers: {
+ // chrome setup for travis CI using chromium
+ Chrome_travis_ci: {
+ base: 'Chrome',
+ flags: ['--no-sandbox']
+ }
+ },
client:{
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
+ files: [
+ { pattern: './src/test.ts', watched: false },
+ { pattern: './node_modules/@angular/material/prebuilt-themes/indigo-pink.css', included: true, watched: true }
+ ],
+ preprocessors: {
+ './src/test.ts': ['@angular/cli']
+ },
+ mime: {
+ 'text/x-typescript': ['ts','tsx']
+ },
coverageIstanbulReporter: {
reports: [ 'html', 'lcovonly' ],
fixWebpackSourcePaths: true
@@ -22,12 +59,15 @@
angularCli: {
environment: 'dev'
},
- reporters: ['progress', 'kjhtml'],
+ reporters: config.angularCli && config.angularCli.codeCoverage
+ ? ['progress', 'coverage-istanbul']
+ : ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
- singleRun: false
+ singleRun: false,
+ browserNoActivityTimeout: 20000
});
};
diff --git a/license.config.js b/license.config.js
new file mode 100644
index 0000000..c18cc52
--- /dev/null
+++ b/license.config.js
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+*/
+
+module.exports = {
+ "allowedPackages": [
+ {
+ "name": "xmldom",
+ "reason": "License offers option between LGPL or MIT(https://github.com/jindw/xmldom/blob/master/LICENSE)"
+ },
+ {
+ "name": "css-select",
+ "reason": "BSD-2-Clause license"
+ },
+ {
+ "name": "css-what",
+ "reason": "BSD-2-Clause license"
+ },
+ {
+ "name": "entities",
+ "reason": "BSD-2-Clause license"
+ },
+ {
+ "name": "spdx-expression-parse",
+ "reason": "Uses CC-BY-3.0 and therefore added to NOTICE file"
+ },
+ {
+ "name": "spdx-exceptions",
+ "reason": "Uses CC-BY-3.0 and therefore added to NOTICE file"
+ },
+ {
+ "name": "caniuse-db",
+ "reason": "Uses CC-BY-4.0 and therefore added to NOTICE file"
+ }
+ ],
+ "disallowedPackages": [],
+ "allowedLicenses": [
+ "MIT",
+ "(MIT AND BSD-3-Clause)",
+ "(MIT AND Zlib)",
+ "MIT/X11",
+ "ISC",
+ "Apache-2.0",
+ "Apache version 2.0",
+ "BSD",
+ "BSD-like",
+ "BSD-2-Clause",
+ "BSD-3-Clause",
+ "WTFPL",
+ "JSF",
+ "Unlicense",
+ "Public Domain",
+ "CC0-1.0"
+ ],
+ "strictMode": true
+};
diff --git a/package-lock.json b/package-lock.json
index d049bfe..4610a39 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,13 +1,13 @@
{
- "name": "fineract-cn-web-app",
- "version": "0.0.0",
+ "name": "fims",
+ "version": "0.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@angular-devkit/build-optimizer": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.3.2.tgz",
- "integrity": "sha512-U0BCZtThq5rUfY08shHXpxe8ZhSsiYB/cJjUvAWRTs/ORrs8pbngS6xwseQws8d/vHoVrtqGD9GU9h8AmFRERQ==",
+ "version": "0.0.42",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.0.42.tgz",
+ "integrity": "sha512-BAYCVZ10ro6mgZQDZiNiVbX8ppygw4q7z/stpwG8WjMswgMRIcxsxYoC1VFuWcUPAf4UyfTIav6e8UZWA5+xnQ==",
"dev": true,
"requires": {
"loader-utils": "1.1.0",
@@ -25,150 +25,161 @@
}
},
"@angular-devkit/core": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-0.3.2.tgz",
- "integrity": "sha512-zABk/iP7YX5SVbmK4e+IX7j2d0D37MQJQiKgWdV3JzfvVJhNJzddiirtT980pIafoq+KyvTgVwXtc+vnux0oeQ==",
+ "version": "0.0.20",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-0.0.20.tgz",
+ "integrity": "sha512-lg5BvMxOfbVD//SOQvpq6TPIKTXYNMj0I9N/kfXbXkUGgiBGFLyFMf2fc+qNvDoa7lulKMPT8OJWS1YlGt93eg==",
"dev": true,
"requires": {
- "ajv": "5.5.2",
- "chokidar": "1.7.0",
- "rxjs": "5.5.11",
"source-map": "0.5.7"
- },
- "dependencies": {
- "ajv": {
- "version": "5.5.2",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
- "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
- "dev": true,
- "requires": {
- "co": "4.6.0",
- "fast-deep-equal": "1.1.0",
- "fast-json-stable-stringify": "2.0.0",
- "json-schema-traverse": "0.3.1"
- }
- }
}
},
"@angular-devkit/schematics": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-0.3.2.tgz",
- "integrity": "sha512-B6zZoqvHaTJy+vVdA6EtlxnCdGMa5elCa4j9lQLC3JI8DLvMXUWkCIPVbPzJ/GSRR9nsKWpvYMYaJyfBDUqfhw==",
+ "version": "0.0.52",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-0.0.52.tgz",
+ "integrity": "sha512-NtG8VB5aWtg0cw1Y7EJinJMuAnXsNdkQkkVe/i7CO6TPLyFQSFQCN1YojCr43l8jTWTRebRslrBawPCMOxsOgw==",
"dev": true,
"requires": {
- "@ngtools/json-schema": "1.2.0",
+ "@ngtools/json-schema": "1.1.0",
"rxjs": "5.5.11"
+ },
+ "dependencies": {
+ "rxjs": {
+ "version": "5.5.11",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.11.tgz",
+ "integrity": "sha512-3bjO7UwWfA2CV7lmwYMBzj4fQ6Cq+ftHc2MvUe+WMS7wcdJ1LosDWmdjPQanYp2dBRj572p7PeU81JUxHKOcBA==",
+ "dev": true,
+ "requires": {
+ "symbol-observable": "1.0.1"
+ }
+ },
+ "symbol-observable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz",
+ "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=",
+ "dev": true
+ }
}
},
"@angular/animations": {
- "version": "5.2.11",
- "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-5.2.11.tgz",
- "integrity": "sha512-J7wKHkFn3wV28/Y1Qm4yjGXVCwXzj1JR5DRjGDTFnxTRacUFx7Nj0ApGhN0b2+V0NOvgxQOvEW415Y22kGoblw==",
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-4.4.5.tgz",
+ "integrity": "sha1-WlpVHXV+WlVgCY9vhTXBAtk5VNc=",
"requires": {
- "tslib": "1.9.2"
+ "tslib": "1.9.1"
}
},
"@angular/cdk": {
- "version": "5.2.5",
- "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-5.2.5.tgz",
- "integrity": "sha512-GN8m1d+VcCE9+Bgwv06Y8YJKyZ0i9ZIq2ZPBcJYt+KVgnVVRg4JkyUNxud07LNsvzOX22DquHqmIZiC4hAG7Ag==",
+ "version": "2.0.0-beta.12",
+ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-2.0.0-beta.12.tgz",
+ "integrity": "sha1-OiQ8tiuT9OA5EgunD5ANyeI1Yi4=",
"requires": {
- "tslib": "1.9.2"
+ "tslib": "1.9.1"
}
},
"@angular/cli": {
- "version": "1.7.4",
- "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-1.7.4.tgz",
- "integrity": "sha512-URdb1QtnQf+Ievy93wjq7gE81s25BkWUwJFPey+YkphBA3G1lbCAQPiEh2pntBwaIKavgEuCw+Sf2YZdgTVhDA==",
+ "version": "1.4.7",
+ "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-1.4.7.tgz",
+ "integrity": "sha512-VJB95B49nh8LjJJDkHNAoklJIsKQ0Xvpu39q/yaicpuzVaSiOUgHO6SfQrW6RwN3uxLAWTREy+FMRtIv1UIXNw==",
"dev": true,
"requires": {
- "@angular-devkit/build-optimizer": "0.3.2",
- "@angular-devkit/core": "0.3.2",
- "@angular-devkit/schematics": "0.3.2",
- "@ngtools/json-schema": "1.2.0",
- "@ngtools/webpack": "1.10.2",
- "@schematics/angular": "0.3.2",
- "@schematics/package-update": "0.3.2",
- "ajv": "6.5.1",
- "autoprefixer": "7.2.6",
- "cache-loader": "1.2.2",
- "chalk": "2.2.2",
- "circular-dependency-plugin": "4.4.0",
- "clean-css": "4.1.11",
- "common-tags": "1.8.0",
- "copy-webpack-plugin": "4.4.3",
+ "@angular-devkit/build-optimizer": "0.0.42",
+ "@angular-devkit/schematics": "0.0.52",
+ "@ngtools/json-schema": "1.1.0",
+ "@ngtools/webpack": "1.7.4",
+ "@schematics/angular": "0.0.49",
+ "autoprefixer": "6.7.7",
+ "chalk": "2.4.1",
+ "circular-dependency-plugin": "3.0.0",
+ "common-tags": "1.7.2",
+ "copy-webpack-plugin": "4.5.1",
"core-object": "3.1.5",
+ "css-loader": "0.28.11",
+ "cssnano": "3.10.0",
"denodeify": "1.2.1",
"ember-cli-string-utils": "1.1.0",
- "extract-text-webpack-plugin": "3.0.2",
- "file-loader": "1.1.11",
+ "exports-loader": "0.6.4",
+ "extract-text-webpack-plugin": "3.0.0",
+ "file-loader": "0.10.1",
"fs-extra": "4.0.3",
"glob": "7.1.2",
"html-webpack-plugin": "2.30.1",
- "istanbul-instrumenter-loader": "3.0.1",
+ "istanbul-instrumenter-loader": "2.0.0",
"karma-source-map-support": "1.3.0",
"less": "2.7.3",
"less-loader": "4.1.0",
"license-webpack-plugin": "1.3.1",
- "loader-utils": "1.1.0",
"lodash": "4.17.10",
"memory-fs": "0.4.1",
- "minimatch": "3.0.4",
"node-modules-path": "1.0.1",
"node-sass": "4.9.0",
"nopt": "4.0.1",
"opn": "5.1.0",
"portfinder": "1.0.13",
- "postcss": "6.0.22",
- "postcss-import": "11.1.0",
- "postcss-loader": "2.1.5",
- "postcss-url": "7.3.2",
+ "postcss-loader": "1.3.3",
+ "postcss-url": "5.1.2",
"raw-loader": "0.5.1",
- "resolve": "1.8.1",
+ "resolve": "1.7.1",
"rxjs": "5.5.11",
"sass-loader": "6.0.7",
"semver": "5.5.0",
"silent-error": "1.1.0",
+ "source-map-loader": "0.2.3",
"source-map-support": "0.4.18",
- "style-loader": "0.19.1",
+ "style-loader": "0.13.2",
"stylus": "0.54.5",
"stylus-loader": "3.0.2",
- "uglifyjs-webpack-plugin": "1.2.6",
+ "typescript": "2.3.4",
"url-loader": "0.6.2",
- "webpack": "3.11.0",
+ "webpack": "3.6.0",
+ "webpack-concat-plugin": "1.4.0",
"webpack-dev-middleware": "1.12.2",
- "webpack-dev-server": "2.11.2",
- "webpack-merge": "4.1.3",
- "webpack-sources": "1.1.0",
- "webpack-subresource-integrity": "1.0.4"
+ "webpack-dev-server": "2.7.1",
+ "webpack-merge": "4.1.2",
+ "zone.js": "0.8.17"
+ },
+ "dependencies": {
+ "rxjs": {
+ "version": "5.5.11",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.11.tgz",
+ "integrity": "sha512-3bjO7UwWfA2CV7lmwYMBzj4fQ6Cq+ftHc2MvUe+WMS7wcdJ1LosDWmdjPQanYp2dBRj572p7PeU81JUxHKOcBA==",
+ "dev": true,
+ "requires": {
+ "symbol-observable": "1.0.1"
+ }
+ },
+ "symbol-observable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz",
+ "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=",
+ "dev": true
+ }
}
},
"@angular/common": {
- "version": "5.2.11",
- "resolved": "https://registry.npmjs.org/@angular/common/-/common-5.2.11.tgz",
- "integrity": "sha512-LniJjGAeftUJDJh+2+LEjltcGen08C/VMxQ/eUYmesytKy1sN+MWzh3GbpKfEWtWmyUsYTG9lAAJNo3L3jPwsw==",
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/@angular/common/-/common-4.4.5.tgz",
+ "integrity": "sha1-vVF53JIq2/TD6m37Gec8uEn/3Dc=",
"requires": {
- "tslib": "1.9.2"
+ "tslib": "1.9.1"
}
},
"@angular/compiler": {
- "version": "5.2.11",
- "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-5.2.11.tgz",
- "integrity": "sha512-ICvB1ud1mxaXUYLb8vhJqiLhGBVocAZGxoHTglv6hMkbrRYcnlB3FZJFOzBvtj+krkd1jamoYLI43UAmesqQ6Q==",
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-4.4.5.tgz",
+ "integrity": "sha1-hyGlkQ8rtS8J4tQEytJk817eWQI=",
"requires": {
- "tslib": "1.9.2"
+ "tslib": "1.9.1"
}
},
"@angular/compiler-cli": {
- "version": "5.2.11",
- "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-5.2.11.tgz",
- "integrity": "sha512-dwrQ0yxoCM/XzKzlm7pTsyg4/6ECjT9emZufGj8t12bLMO8NDn1IJOsqXJA1+onEgQKhlr0Ziwi+96TvDTb1Cg==",
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-4.4.5.tgz",
+ "integrity": "sha1-YfoDNqzRogjF8cXG1N9nnpmVMkg=",
"dev": true,
"requires": {
- "chokidar": "1.7.0",
+ "@angular/tsc-wrapped": "4.4.5",
"minimist": "1.2.0",
- "reflect-metadata": "0.1.12",
- "tsickle": "0.27.5"
+ "reflect-metadata": "0.1.12"
},
"dependencies": {
"minimist": {
@@ -180,114 +191,130 @@
}
},
"@angular/core": {
- "version": "5.2.11",
- "resolved": "https://registry.npmjs.org/@angular/core/-/core-5.2.11.tgz",
- "integrity": "sha512-h2vpvXNAdOqKzbVaZcHnHGMT5A8uDnizk6FgGq6SPyw9s3d+/VxZ9LJaPjUk3g2lICA7og1tUel+2YfF971MlQ==",
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/@angular/core/-/core-4.4.5.tgz",
+ "integrity": "sha1-VKy8vaEXGfiDx4apBpdKvrEy8aA=",
"requires": {
- "tslib": "1.9.2"
+ "tslib": "1.9.1"
}
},
"@angular/forms": {
- "version": "5.2.11",
- "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-5.2.11.tgz",
- "integrity": "sha512-wBllFlIubPclAFRXUc84Kc7TMeKOftzrQraVZ7ooTNeFLLa/FZLN2K8HGyRde8X/XDsMu1XAmjNfkz++spwTzA==",
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-4.4.5.tgz",
+ "integrity": "sha1-6VUghiMqqyzh0I7xmLYiBOoTxDs=",
"requires": {
- "tslib": "1.9.2"
+ "tslib": "1.9.1"
}
},
"@angular/http": {
- "version": "5.2.11",
- "resolved": "https://registry.npmjs.org/@angular/http/-/http-5.2.11.tgz",
- "integrity": "sha512-eR7wNXh1+6MpcQNb3sq4bJVX03dx50Wl3kpPG+Q7N1VSL0oPQSobaTrR17ac3oFCEfSJn6kkUCqtUXha6wcNHg==",
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/@angular/http/-/http-4.4.5.tgz",
+ "integrity": "sha1-LHNe2EJAH8I1ZBkmjiiNzyOW6E8=",
"requires": {
- "tslib": "1.9.2"
+ "tslib": "1.9.1"
}
},
- "@angular/language-service": {
- "version": "5.2.11",
- "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-5.2.11.tgz",
- "integrity": "sha512-tgnFAhwBmUs1W0dmcmlBmUlMaOgkoyuSdrcF23lz8W5+nSLb+LnbH5a3blU2NVqA4ESvLKQkPW5dpKa/LuhrPQ==",
- "dev": true
- },
"@angular/material": {
- "version": "5.2.5",
- "resolved": "https://registry.npmjs.org/@angular/material/-/material-5.2.5.tgz",
- "integrity": "sha512-IltfBeTJWnmZehOQNQ7KoFs7MGWuZTe0g21hIitGkusVNt1cIoTD24xKH5jwztjH19c04IgiwonpurMKM6pBCQ==",
+ "version": "2.0.0-beta.12",
+ "resolved": "https://registry.npmjs.org/@angular/material/-/material-2.0.0-beta.12.tgz",
+ "integrity": "sha1-cbbQt7AhiR5dDjaIwdS9eMdFf1g=",
"requires": {
- "tslib": "1.9.2"
+ "tslib": "1.9.1"
}
},
"@angular/platform-browser": {
- "version": "5.2.11",
- "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-5.2.11.tgz",
- "integrity": "sha512-6YZ4IpBFqXx88vEzBZG2WWnaSYXbFWDgG0iT+bZPHAfwsbmqbcMcs7Ogu+XZ4VmK02dTqbrFh7U4P2W+sqrzow==",
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-4.4.5.tgz",
+ "integrity": "sha1-dOuRwLdYEm8m1T7lbHz0ZovZysU=",
"requires": {
- "tslib": "1.9.2"
+ "tslib": "1.9.1"
}
},
"@angular/platform-browser-dynamic": {
- "version": "5.2.11",
- "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-5.2.11.tgz",
- "integrity": "sha512-5kKPNULcXNwkyBjpHfF+pq+Yxi8Zl866YSOK9t8txoiQ9Ctw97kMkEJcTetk6MJgBp/NP3YyjtoTAm8oXLerug==",
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-4.4.5.tgz",
+ "integrity": "sha1-d029wdkPd12/HjGfbtQrJgYjth8=",
"requires": {
- "tslib": "1.9.2"
+ "tslib": "1.9.1"
+ }
+ },
+ "@angular/platform-server": {
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-4.4.5.tgz",
+ "integrity": "sha1-dvI7LDhO1zldwXk8+Fl4iDuiy1A=",
+ "requires": {
+ "parse5": "3.0.3",
+ "tslib": "1.9.1",
+ "xhr2": "0.1.4"
}
},
"@angular/router": {
- "version": "5.2.11",
- "resolved": "https://registry.npmjs.org/@angular/router/-/router-5.2.11.tgz",
- "integrity": "sha512-NT8xYl7Vr3qPygisek3PlXqNROEjg48GXOEsDEc7c8lDBo3EB9Tf328fWJD0GbLtXZNhmmNNxwIe+qqPFFhFAA==",
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/@angular/router/-/router-4.4.5.tgz",
+ "integrity": "sha1-9zEwz0h9mjLMGYiv2llmX0Siiok=",
"requires": {
- "tslib": "1.9.2"
+ "tslib": "1.9.1"
+ }
+ },
+ "@angular/tsc-wrapped": {
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/@angular/tsc-wrapped/-/tsc-wrapped-4.4.5.tgz",
+ "integrity": "sha1-MKDLtDpmOqddyphIlL5IE3eN3Jw=",
+ "dev": true,
+ "requires": {
+ "tsickle": "0.21.6"
}
},
"@covalent/core": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@covalent/core/-/core-1.0.1.tgz",
- "integrity": "sha512-br5KMBT8xXlctkSENGHnXsV4xyJuq0+yXopHH02zz9E0j1d1zlB43hM7zU1FkeGIFtWtPP7peM4tjD6S+ujkXw==",
+ "version": "1.0.0-beta.8-1",
+ "resolved": "https://registry.npmjs.org/@covalent/core/-/core-1.0.0-beta.8-1.tgz",
+ "integrity": "sha1-GL8J+RLMAvOcHleCVcxCco2hBCY=",
"requires": {
- "tslib": "1.9.2"
+ "tslib": "1.9.1"
}
},
+ "@ngrx/core": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@ngrx/core/-/core-1.2.0.tgz",
+ "integrity": "sha1-iCtGq6+i4ObYh8txobLC+j5tDcY="
+ },
"@ngrx/effects": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-5.2.0.tgz",
- "integrity": "sha1-qnYractv1GRNckoc7NJlyqQrrwk="
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-2.0.3.tgz",
+ "integrity": "sha1-5UzjQIBt2RqBgmeW8T4kRq7q+3c="
},
"@ngrx/store": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-5.2.0.tgz",
- "integrity": "sha1-Yn7XTJzZVGKTBIXZEqVXEXsjkD4="
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-2.2.2.tgz",
+ "integrity": "sha1-oAMFpkUgMqM4WIahHOUp3OLa5ls="
+ },
+ "@ngrx/store-devtools": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@ngrx/store-devtools/-/store-devtools-3.2.4.tgz",
+ "integrity": "sha1-LOTRO/NISKnlHsh+OxJe1ntR5VA="
},
"@ngtools/json-schema": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@ngtools/json-schema/-/json-schema-1.2.0.tgz",
- "integrity": "sha512-pMh+HDc6mOjUO3agRfB1tInimo7hf67u+0Cska2bfXFe6oU7rSMnr5PLVtiZVgwMoBHpx/6XjBymvcnWPo2Uzg==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@ngtools/json-schema/-/json-schema-1.1.0.tgz",
+ "integrity": "sha1-w6DFRNYjkqzCgTpCyKDcb1j4aSI=",
"dev": true
},
"@ngtools/webpack": {
- "version": "1.10.2",
- "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-1.10.2.tgz",
- "integrity": "sha512-3u2zg2rarG3qNLSukBClGADWuq/iNn5SQtlSeAbfKzwBeyLGbF0gN1z1tVx1Bcr8YwFzR6NdRePQmJGcoqq1fg==",
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-1.7.4.tgz",
+ "integrity": "sha512-o0u1Oj1k1WEIamBNEncvXDWmUxCMDIlKrMFp4nIwh7bag4dndDShUVD1EinSpx1TvMjVbA42Z+7cIVmlq+240Q==",
"dev": true,
"requires": {
- "chalk": "2.2.2",
"enhanced-resolve": "3.4.1",
"loader-utils": "1.1.0",
"magic-string": "0.22.5",
- "semver": "5.5.0",
- "source-map": "0.5.7",
- "tree-kill": "1.2.0",
- "webpack-sources": "1.1.0"
+ "source-map": "0.5.7"
}
},
"@ngx-translate/core": {
- "version": "10.0.2",
- "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-10.0.2.tgz",
- "integrity": "sha512-7nM3DrJaqKswwtJlbu2kuKNl+hE8Isr18sKsKvGGpSxQk+G0gO0reDlx2PhUNus7TJTkA1C59vU/JoN8hIvZ4g==",
- "requires": {
- "tslib": "1.9.2"
- }
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-7.0.0.tgz",
+ "integrity": "sha1-W29jvUBCFk1EzYX2hwOvluk5Ln0="
},
"@ngx-translate/http-loader": {
"version": "0.1.0",
@@ -295,52 +322,30 @@
"integrity": "sha1-YCkyVWHXho/jJaQZ3idw6Y/xUC4="
},
"@schematics/angular": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-0.3.2.tgz",
- "integrity": "sha512-Elrk0BA951s0ScFZU0AWrpUeJBYVR52DZ1QTIO5R0AhwEd1PW4olI8szPLGQlVW5Sd6H0FA/fyFLIvn2r9v6Rw==",
+ "version": "0.0.49",
+ "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-0.0.49.tgz",
+ "integrity": "sha512-ZRuWY04P2fiJFKEzWfJBPsM7/ZxPp6bYkCQ38a5KQJwt5X5dKJBMVMcBmjX6TOqreN3eJaJGNr/lcpRNlCvnpw==",
"dev": true,
"requires": {
- "typescript": "2.6.2"
- },
- "dependencies": {
- "typescript": {
- "version": "2.6.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.2.tgz",
- "integrity": "sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q=",
- "dev": true
- }
+ "@angular-devkit/core": "0.0.20"
}
},
- "@schematics/package-update": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/@schematics/package-update/-/package-update-0.3.2.tgz",
- "integrity": "sha512-7aVP4994Hu8vRdTTohXkfGWEwLhrdNP3EZnWyBootm5zshWqlQojUGweZe5zwewsKcixeVOiy2YtW+aI4aGSLA==",
- "dev": true,
- "requires": {
- "rxjs": "5.5.11",
- "semver": "5.5.0",
- "semver-intersect": "1.3.1"
- }
- },
- "@types/jasmine": {
- "version": "2.8.8",
- "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.8.tgz",
- "integrity": "sha512-OJSUxLaxXsjjhob2DBzqzgrkLmukM3+JMpRp0r0E4HTdT1nwDCWhaswjYxazPij6uOdzHCJfNbDjmQ1/rnNbCg==",
+ "@types/hammerjs": {
+ "version": "2.0.30",
+ "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.30.tgz",
+ "integrity": "sha1-RAM1tBM4kxmS+5v6UTgWa3GKlAc=",
"dev": true
},
- "@types/jasminewd2": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/@types/jasminewd2/-/jasminewd2-2.0.3.tgz",
- "integrity": "sha512-hYDVmQZT5VA2kigd4H4bv7vl/OhlympwREUemqBdOqtrYTo5Ytm12a5W5/nGgGYdanGVxj0x/VhZ7J3hOg/YKg==",
- "dev": true,
- "requires": {
- "@types/jasmine": "2.8.8"
- }
+ "@types/jasmine": {
+ "version": "2.5.38",
+ "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.5.38.tgz",
+ "integrity": "sha1-pDeRJMSSHU4h3lTsdGacnps1Zxc=",
+ "dev": true
},
"@types/node": {
- "version": "6.0.113",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.113.tgz",
- "integrity": "sha512-f9XXUWFqryzjkZA1EqFvJHSFyqyasV17fq8zCDIzbRV4ctL7RrJGKvG+lcex86Rjbzd1GrER9h9VmF5sSjV0BQ==",
+ "version": "6.0.78",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.78.tgz",
+ "integrity": "sha512-+vD6E8ixntRzzZukoF3uP1iV+ZjVN3koTcaeK+BEoc/kSfGbLDIGC7RmCaUgVpUfN6cWvfczFRERCyKM9mkvXg==",
"dev": true
},
"@types/q": {
@@ -350,21 +355,9 @@
"dev": true
},
"@types/selenium-webdriver": {
- "version": "2.53.43",
- "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-2.53.43.tgz",
- "integrity": "sha512-UBYHWph6P3tutkbXpW6XYg9ZPbTKjw/YC2hGG1/GEvWwTbvezBUv3h+mmUFw79T3RFPnmedpiXdOBbXX+4l0jg==",
- "dev": true
- },
- "@types/strip-bom": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz",
- "integrity": "sha1-FKjsOVbC6B7bdSB5CuzyHCkK69I=",
- "dev": true
- },
- "@types/strip-json-comments": {
- "version": "0.0.30",
- "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz",
- "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==",
+ "version": "2.53.36",
+ "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-2.53.36.tgz",
+ "integrity": "sha1-otJ9BwmzaK4IlPwwwOQK+CfW6FE=",
"dev": true
},
"abbrev": {
@@ -384,9 +377,9 @@
}
},
"acorn": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.1.tgz",
- "integrity": "sha512-d+nbxBUGKg7Arpsvbnlq61mc12ek3EY8EQldM3GPAhWJ1UVxC6TDGbIvUMNU6obBX3i1+ptCIzV4vq0gFPEGVQ==",
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz",
+ "integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==",
"dev": true
},
"acorn-dynamic-import": {
@@ -406,13 +399,6 @@
}
}
},
- "addressparser": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/addressparser/-/addressparser-1.0.1.tgz",
- "integrity": "sha1-R6++GiqSYhkdtoOOT9HTm0CCF0Y=",
- "dev": true,
- "optional": true
- },
"adm-zip": {
"version": "0.4.11",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.11.tgz",
@@ -426,44 +412,39 @@
"dev": true
},
"agent-base": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.0.tgz",
- "integrity": "sha512-c+R/U5X+2zz2+UCrCFv6odQzJdoqI+YecuhnAJLa1zYaMc13zPfwMwZrr91Pd1DYNo/yPRbiM4WVf9whgwFsIg==",
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-2.1.1.tgz",
+ "integrity": "sha1-1t4Q1a9hMtW9aSQn1G/FOFOQlMc=",
"dev": true,
"requires": {
- "es6-promisify": "5.0.0"
- }
- },
- "ajv": {
- "version": "6.5.1",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.1.tgz",
- "integrity": "sha512-pgZos1vgOHDiC7gKNbZW8eKvCnNXARv2oqrGQT7Hzbq5Azp7aZG6DJzADnkuSq7RH6qkXp4J/m68yPX/2uBHyQ==",
- "dev": true,
- "requires": {
- "fast-deep-equal": "2.0.1",
- "fast-json-stable-stringify": "2.0.0",
- "json-schema-traverse": "0.4.1",
- "uri-js": "4.2.2"
+ "extend": "3.0.1",
+ "semver": "5.0.3"
},
"dependencies": {
- "fast-deep-equal": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
- "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
- "dev": true
- },
- "json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "semver": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.0.3.tgz",
+ "integrity": "sha1-d0Zt5YnNXTyV8TiqeLxWmjy10no=",
"dev": true
}
}
},
+ "ajv": {
+ "version": "5.5.2",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
+ "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
+ "dev": true,
+ "requires": {
+ "co": "4.6.0",
+ "fast-deep-equal": "1.1.0",
+ "fast-json-stable-stringify": "2.0.0",
+ "json-schema-traverse": "0.3.1"
+ }
+ },
"ajv-keywords": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz",
- "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=",
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz",
+ "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=",
"dev": true
},
"align-text": {
@@ -475,55 +456,37 @@
"kind-of": "3.2.2",
"longest": "1.0.1",
"repeat-string": "1.6.1"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "1.1.6"
+ }
+ }
}
},
+ "alphanum-sort": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz",
+ "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
+ "dev": true
+ },
"amdefine": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
"dev": true
},
- "amqplib": {
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.5.2.tgz",
- "integrity": "sha512-l9mCs6LbydtHqRniRwYkKdqxVa6XMz3Vw1fh+2gJaaVgTM6Jk3o8RccAKWKtlhT1US5sWrFh+KKxsVUALURSIA==",
- "dev": true,
- "optional": true,
+ "angular2-text-mask": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/angular2-text-mask/-/angular2-text-mask-8.0.5.tgz",
+ "integrity": "sha512-s6fqiQtIa16v0FzMhk1Y5LApAoq/0okKwtyCW70/UkHbRdRkN/uHa2LplX7F/SyTJFsYZJTRzH8IlpaMbQcccw==",
"requires": {
- "bitsyntax": "0.0.4",
- "bluebird": "3.5.1",
- "buffer-more-ints": "0.0.2",
- "readable-stream": "1.1.14",
- "safe-buffer": "5.1.2"
- },
- "dependencies": {
- "isarray": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
- "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
- "dev": true,
- "optional": true
- },
- "readable-stream": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
- "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
- "dev": true,
- "optional": true,
- "requires": {
- "core-util-is": "1.0.2",
- "inherits": "2.0.3",
- "isarray": "0.0.1",
- "string_decoder": "0.10.31"
- }
- },
- "string_decoder": {
- "version": "0.10.31",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
- "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
- "dev": true,
- "optional": true
- }
+ "text-mask-core": "5.1.1"
}
},
"ansi-html": {
@@ -535,25 +498,21 @@
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
- "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
- "dev": true
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
},
"ansi-styles": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
- "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
- "dev": true,
- "requires": {
- "color-convert": "1.9.2"
- }
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+ "dev": true
},
"anymatch": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz",
- "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
+ "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
"dev": true,
"requires": {
- "micromatch": "2.3.11",
+ "micromatch": "3.1.10",
"normalize-path": "2.1.1"
}
},
@@ -564,12 +523,12 @@
"dev": true
},
"append-transform": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz",
- "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==",
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz",
+ "integrity": "sha1-126/jKlNJ24keja61EpLdKthGZE=",
"dev": true,
"requires": {
- "default-require-extensions": "2.0.0"
+ "default-require-extensions": "1.0.0"
}
},
"aproba": {
@@ -579,9 +538,9 @@
"dev": true
},
"are-we-there-yet": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
- "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz",
+ "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=",
"dev": true,
"requires": {
"delegates": "1.0.0",
@@ -598,13 +557,10 @@
}
},
"arr-diff": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
- "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
- "dev": true,
- "requires": {
- "arr-flatten": "1.1.0"
- }
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+ "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
+ "dev": true
},
"arr-flatten": {
"version": "1.1.0",
@@ -630,16 +586,6 @@
"integrity": "sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=",
"dev": true
},
- "array-includes": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz",
- "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=",
- "dev": true,
- "requires": {
- "define-properties": "1.1.2",
- "es-abstract": "1.12.0"
- }
- },
"array-slice": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz",
@@ -662,15 +608,15 @@
"dev": true
},
"array-unique": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
- "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
"dev": true
},
"arraybuffer.slice": {
- "version": "0.0.7",
- "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
- "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==",
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz",
+ "integrity": "sha1-8zshWfBTKj8xB6JywMz70a0peco=",
"dev": true
},
"arrify": {
@@ -683,8 +629,7 @@
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
- "dev": true,
- "optional": true
+ "dev": true
},
"asn1": {
"version": "0.2.3",
@@ -710,23 +655,6 @@
"dev": true,
"requires": {
"util": "0.10.3"
- },
- "dependencies": {
- "inherits": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
- "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
- "dev": true
- },
- "util": {
- "version": "0.10.3",
- "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
- "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
- "dev": true,
- "requires": {
- "inherits": "2.0.1"
- }
- }
}
},
"assert-plus": {
@@ -741,13 +669,6 @@
"integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
"dev": true
},
- "ast-types": {
- "version": "0.11.5",
- "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.11.5.tgz",
- "integrity": "sha512-oJjo+5e7/vEc2FBK8gUalV0pba4L3VdBIs2EKhOLHLcOd2FgQIVQN9xb0eZ9IjEWyAL7vq6fGJxOvVvdCHNyMw==",
- "dev": true,
- "optional": true
- },
"async": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
@@ -789,16 +710,16 @@
"dev": true
},
"autoprefixer": {
- "version": "7.2.6",
- "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-7.2.6.tgz",
- "integrity": "sha512-Iq8TRIB+/9eQ8rbGhcP7ct5cYb/3qjNYAR2SnzLCEcwF6rvVOax8+9+fccgXk4bEhQGjOZd5TLhsksmAdsbGqQ==",
+ "version": "6.7.7",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.7.7.tgz",
+ "integrity": "sha1-Hb0cg1ZY41zj+ZhAmdsAWFx4IBQ=",
"dev": true,
"requires": {
- "browserslist": "2.11.3",
- "caniuse-lite": "1.0.30000856",
+ "browserslist": "1.7.7",
+ "caniuse-db": "1.0.30000844",
"normalize-range": "0.1.2",
"num2fraction": "1.2.2",
- "postcss": "6.0.22",
+ "postcss": "5.2.18",
"postcss-value-parser": "3.3.0"
}
},
@@ -814,28 +735,6 @@
"integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==",
"dev": true
},
- "axios": {
- "version": "0.15.3",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.15.3.tgz",
- "integrity": "sha1-LJ1jiy4ZGgjqHWzJiOrda6W9wFM=",
- "dev": true,
- "optional": true,
- "requires": {
- "follow-redirects": "1.0.0"
- },
- "dependencies": {
- "follow-redirects": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.0.0.tgz",
- "integrity": "sha1-jjQpjL0uF28lTv/sdaHHjMhJ/Tc=",
- "dev": true,
- "optional": true,
- "requires": {
- "debug": "2.6.9"
- }
- }
- }
- },
"babel-code-frame": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
@@ -847,12 +746,6 @@
"js-tokens": "3.0.2"
},
"dependencies": {
- "ansi-styles": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
- "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
- "dev": true
- },
"chalk": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
@@ -888,6 +781,14 @@
"lodash": "4.17.10",
"source-map": "0.5.7",
"trim-right": "1.0.1"
+ },
+ "dependencies": {
+ "jsesc": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz",
+ "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=",
+ "dev": true
+ }
}
},
"babel-messages": {
@@ -905,7 +806,7 @@
"integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
"dev": true,
"requires": {
- "core-js": "2.5.7",
+ "core-js": "2.4.1",
"regenerator-runtime": "0.11.1"
}
},
@@ -1021,18 +922,6 @@
"is-data-descriptor": "1.0.0",
"kind-of": "6.0.2"
}
- },
- "isobject": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
- "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
- "dev": true
- },
- "kind-of": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
- "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
- "dev": true
}
}
},
@@ -1079,6 +968,17 @@
"callsite": "1.0.0"
}
},
+ "bfj-node4": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/bfj-node4/-/bfj-node4-5.3.1.tgz",
+ "integrity": "sha512-SOmOsowQWfXc7ybFARsK3C4MCOWzERaOMV/Fl3Tgjs+5dJWyzo3oa127jL44eMbQiAN17J7SvAs2TRxEScTUmg==",
+ "dev": true,
+ "requires": {
+ "bluebird": "3.5.1",
+ "check-types": "7.3.0",
+ "tryer": "1.0.0"
+ }
+ },
"big.js": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz",
@@ -1091,22 +991,11 @@
"integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=",
"dev": true
},
- "bitsyntax": {
- "version": "0.0.4",
- "resolved": "https://registry.npmjs.org/bitsyntax/-/bitsyntax-0.0.4.tgz",
- "integrity": "sha1-6xDMb4K4xJDj6FaY8H6D1G4MuoI=",
- "dev": true,
- "optional": true,
- "requires": {
- "buffer-more-ints": "0.0.2"
- }
- },
"bl": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz",
- "integrity": "sha1-/cqHGplxOqANGeO7ukHER4emU5g=",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-1.0.3.tgz",
+ "integrity": "sha1-/FQhoo/UImA2w7OJGmaiW8ZNIm4=",
"dev": true,
- "optional": true,
"requires": {
"readable-stream": "2.0.6"
},
@@ -1115,15 +1004,13 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=",
- "dev": true,
- "optional": true
+ "dev": true
},
"readable-stream": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz",
"integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=",
"dev": true,
- "optional": true,
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
@@ -1137,8 +1024,7 @@
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
- "dev": true,
- "optional": true
+ "dev": true
}
}
},
@@ -1159,9 +1045,9 @@
}
},
"blocking-proxy": {
- "version": "0.0.5",
- "resolved": "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-0.0.5.tgz",
- "integrity": "sha1-RikF4Nz76pcPQao3Ij3anAexkSs=",
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-0.0.4.tgz",
+ "integrity": "sha1-SQFnMqw46NU6LH3NUCUgqg5Y4EQ=",
"dev": true,
"requires": {
"minimist": "1.2.0"
@@ -1253,14 +1139,32 @@
}
},
"braces": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz",
- "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=",
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
"dev": true,
"requires": {
- "expand-range": "1.8.2",
- "preserve": "0.2.0",
- "repeat-element": "1.1.2"
+ "arr-flatten": "1.1.0",
+ "array-unique": "0.3.2",
+ "extend-shallow": "2.0.1",
+ "fill-range": "4.0.0",
+ "isobject": "3.0.1",
+ "repeat-element": "1.1.2",
+ "snapdragon": "0.8.2",
+ "snapdragon-node": "2.1.1",
+ "split-string": "3.1.0",
+ "to-regex": "3.0.2"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "0.1.1"
+ }
+ }
}
},
"brorand": {
@@ -1340,13 +1244,13 @@
}
},
"browserslist": {
- "version": "2.11.3",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.3.tgz",
- "integrity": "sha512-yWu5cXT7Av6mVwzWc8lMsJMHWn4xyjSuGYi4IozbVTLUOEYPSagUB8kiMDUHA1fS3zjr8nkxkn9jdvug4BBRmA==",
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz",
+ "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=",
"dev": true,
"requires": {
- "caniuse-lite": "1.0.30000856",
- "electron-to-chromium": "1.3.49"
+ "caniuse-db": "1.0.30000844",
+ "electron-to-chromium": "1.3.48"
}
},
"buffer": {
@@ -1356,14 +1260,14 @@
"dev": true,
"requires": {
"base64-js": "1.3.0",
- "ieee754": "1.1.12",
+ "ieee754": "1.1.11",
"isarray": "1.0.0"
}
},
"buffer-from": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz",
- "integrity": "sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ==",
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz",
+ "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==",
"dev": true
},
"buffer-indexof": {
@@ -1372,48 +1276,16 @@
"integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==",
"dev": true
},
- "buffer-more-ints": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-0.0.2.tgz",
- "integrity": "sha1-JrOIXRD6E9t/wBquOquHAZngEkw=",
- "dev": true
- },
"buffer-xor": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
"integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
"dev": true
},
- "buildmail": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/buildmail/-/buildmail-4.0.1.tgz",
- "integrity": "sha1-h393OLeHKYccmhBeO4N9K+EaenI=",
- "dev": true,
- "optional": true,
- "requires": {
- "addressparser": "1.0.1",
- "libbase64": "0.1.0",
- "libmime": "3.0.0",
- "libqp": "1.1.0",
- "nodemailer-fetch": "1.6.0",
- "nodemailer-shared": "1.1.0",
- "punycode": "1.4.1"
- },
- "dependencies": {
- "punycode": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
- "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
- "dev": true,
- "optional": true
- }
- }
- },
"builtin-modules": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
- "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
- "dev": true
+ "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8="
},
"builtin-status-codes": {
"version": "3.0.0",
@@ -1446,6 +1318,14 @@
"ssri": "5.3.0",
"unique-filename": "1.1.0",
"y18n": "4.0.0"
+ },
+ "dependencies": {
+ "y18n": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
+ "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
+ "dev": true
+ }
}
},
"cache-base": {
@@ -1463,26 +1343,6 @@
"to-object-path": "0.3.0",
"union-value": "1.0.0",
"unset-value": "1.0.0"
- },
- "dependencies": {
- "isobject": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
- "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
- "dev": true
- }
- }
- },
- "cache-loader": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/cache-loader/-/cache-loader-1.2.2.tgz",
- "integrity": "sha512-rsGh4SIYyB9glU+d0OcHwiXHXBoUgDhHZaQ1KAbiXqfz1CDPxtTboh1gPbJ0q2qdO8a9lfcjgC5CJ2Ms32y5bw==",
- "dev": true,
- "requires": {
- "loader-utils": "1.1.0",
- "mkdirp": "0.5.1",
- "neo-async": "2.5.1",
- "schema-utils": "0.4.5"
}
},
"callsite": {
@@ -1502,10 +1362,9 @@
}
},
"camelcase": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
- "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
- "dev": true
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
+ "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo="
},
"camelcase-keys": {
"version": "2.1.0",
@@ -1515,12 +1374,32 @@
"requires": {
"camelcase": "2.1.1",
"map-obj": "1.0.1"
+ },
+ "dependencies": {
+ "camelcase": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
+ "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
+ "dev": true
+ }
}
},
- "caniuse-lite": {
- "version": "1.0.30000856",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000856.tgz",
- "integrity": "sha512-x3mYcApHMQemyaHuH/RyqtKCGIYTgEA63fdi+VBvDz8xUSmRiVWTLeyKcoGQCGG6UPR9/+4qG4OKrTa6aSQRKg==",
+ "caniuse-api": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-1.6.1.tgz",
+ "integrity": "sha1-tTTnxzTE+B7F++isoq0kNUuWLGw=",
+ "dev": true,
+ "requires": {
+ "browserslist": "1.7.7",
+ "caniuse-db": "1.0.30000844",
+ "lodash.memoize": "4.1.2",
+ "lodash.uniq": "4.5.0"
+ }
+ },
+ "caniuse-db": {
+ "version": "1.0.30000844",
+ "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000844.tgz",
+ "integrity": "sha1-vKV5jNoraTHWgQDC1p5V+zOMu0E=",
"dev": true
},
"caseless": {
@@ -1540,31 +1419,72 @@
}
},
"chalk": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.2.2.tgz",
- "integrity": "sha512-LvixLAQ4MYhbf7hgL4o5PeK32gJKvVzDRiSNIApDofQvyhl8adgG2lJVXn4+ekQoK7HL9RF8lqxwerpe0x2pCw==",
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+ "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
"dev": true,
"requires": {
"ansi-styles": "3.2.1",
"escape-string-regexp": "1.0.5",
- "supports-color": "4.5.0"
+ "supports-color": "5.4.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "1.9.1"
+ }
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
+ "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
+ "dev": true,
+ "requires": {
+ "has-flag": "3.0.0"
+ }
+ }
}
},
+ "charenc": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
+ "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=",
+ "dev": true
+ },
+ "check-types": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/check-types/-/check-types-7.3.0.tgz",
+ "integrity": "sha1-Ro9XGkQ1wkJI9f0MsOjYfDw0Hn0=",
+ "dev": true
+ },
"chokidar": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
- "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz",
+ "integrity": "sha512-zW8iXYZtXMx4kux/nuZVXjkLP+CyIK5Al5FHnj1OgTKGZfp4Oy6/ymtMSKFv3GD8DviEmUPmJg9eFdJ/JzudMg==",
"dev": true,
"requires": {
- "anymatch": "1.3.2",
+ "anymatch": "2.0.0",
"async-each": "1.0.1",
+ "braces": "2.3.2",
"fsevents": "1.2.4",
- "glob-parent": "2.0.0",
+ "glob-parent": "3.1.0",
"inherits": "2.0.3",
"is-binary-path": "1.0.1",
- "is-glob": "2.0.1",
+ "is-glob": "4.0.0",
+ "normalize-path": "2.1.1",
"path-is-absolute": "1.0.1",
- "readdirp": "2.1.0"
+ "readdirp": "2.1.0",
+ "upath": "1.1.0"
}
},
"chownr": {
@@ -1584,16 +1504,40 @@
}
},
"circular-dependency-plugin": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-4.4.0.tgz",
- "integrity": "sha512-yEFtUNUYT4jBykEX5ZOHw+5goA3glGZr9wAXIQqoyakjz5H5TeUmScnWRc52douAhb9eYzK3s7V6bXfNnjFdzg==",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-3.0.0.tgz",
+ "integrity": "sha1-m2hpLjWw41EJmNAWS2rlARvqV2A=",
"dev": true
},
- "circular-json": {
- "version": "0.5.4",
- "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.5.4.tgz",
- "integrity": "sha512-vnJA8KS0BfOihugYEUkLRcnmq21FbuivbxgzDLXNs3zIk4KllV4Mx4UuTzBXht9F00C7QfD1YqMXg1zP6EXpig==",
- "dev": true
+ "clap": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz",
+ "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==",
+ "dev": true,
+ "requires": {
+ "chalk": "1.1.3"
+ },
+ "dependencies": {
+ "chalk": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "2.2.1",
+ "escape-string-regexp": "1.0.5",
+ "has-ansi": "2.0.0",
+ "strip-ansi": "3.0.1",
+ "supports-color": "2.0.0"
+ }
+ },
+ "supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+ "dev": true
+ }
+ }
},
"class-utils": {
"version": "0.3.6",
@@ -1615,12 +1559,6 @@
"requires": {
"is-descriptor": "0.1.6"
}
- },
- "isobject": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
- "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
- "dev": true
}
}
},
@@ -1637,7 +1575,6 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
"integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
- "dev": true,
"requires": {
"string-width": "1.0.2",
"strip-ansi": "3.0.1",
@@ -1645,9 +1582,9 @@
}
},
"clone": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz",
- "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+ "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
"dev": true
},
"clone-deep": {
@@ -1660,23 +1597,6 @@
"is-plain-object": "2.0.4",
"kind-of": "6.0.2",
"shallow-clone": "1.0.0"
- },
- "dependencies": {
- "for-own": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
- "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=",
- "dev": true,
- "requires": {
- "for-in": "1.0.2"
- }
- },
- "kind-of": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
- "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
- "dev": true
- }
}
},
"co": {
@@ -1685,16 +1605,24 @@
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
"dev": true
},
+ "coa": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz",
+ "integrity": "sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0=",
+ "dev": true,
+ "requires": {
+ "q": "1.5.1"
+ }
+ },
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
- "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
- "dev": true
+ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
},
"codelyzer": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-4.3.0.tgz",
- "integrity": "sha512-RLMrtLwrBS0dfo2/KTP+2NHofCpzcuh0bEp/A/naqvQonbUL4AW/qWQdbpn8dMNudtpmzEx9eS8KEpGdVPg1BA==",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-3.0.0.tgz",
+ "integrity": "sha1-sz60iGJxtdggmg36SiD8J+76QCk=",
"dev": true,
"requires": {
"app-root-path": "2.0.1",
@@ -1715,21 +1643,52 @@
"object-visit": "1.0.1"
}
},
- "color-convert": {
- "version": "1.9.2",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz",
- "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==",
+ "color": {
+ "version": "0.11.4",
+ "resolved": "https://registry.npmjs.org/color/-/color-0.11.4.tgz",
+ "integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=",
"dev": true,
"requires": {
- "color-name": "1.1.1"
+ "clone": "1.0.4",
+ "color-convert": "1.9.1",
+ "color-string": "0.3.0"
+ }
+ },
+ "color-convert": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz",
+ "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "1.1.3"
}
},
"color-name": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz",
- "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
+ "color-string": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz",
+ "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=",
+ "dev": true,
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "colormin": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/colormin/-/colormin-1.1.2.tgz",
+ "integrity": "sha1-6i90IKcrlogaOKrlnsEkpvcpgTM=",
+ "dev": true,
+ "requires": {
+ "color": "0.11.4",
+ "css-color-names": "0.0.4",
+ "has": "1.0.1"
+ }
+ },
"colors": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz",
@@ -1761,10 +1720,13 @@
"dev": true
},
"common-tags": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz",
- "integrity": "sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==",
- "dev": true
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.7.2.tgz",
+ "integrity": "sha512-joj9ZlUOjCrwdbmiLqafeUSgkUM74NqhLsZtSqDmhKudaIY197zTrb8JMl31fMnCUuxwFT23eC/oWvrZzDLRJQ==",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "6.26.0"
+ }
},
"commondir": {
"version": "1.0.1",
@@ -1773,9 +1735,9 @@
"dev": true
},
"compare-versions": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.3.0.tgz",
- "integrity": "sha512-MAAAIOdi2s4Gl6rZ76PNcUa9IOYB+5ICdT41o5uMRf09aEu/F9RK+qhe8RjXNPwcTjGV7KU7h2P/fljThFVqyQ==",
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.2.1.tgz",
+ "integrity": "sha512-2y2nHcopMG/NAyk6vWXlLs86XeM9sik4jmx1tKIgzMi9/RQ2eo758RGpxQO3ErihHmg0RlQITPqgz73y6s7quA==",
"dev": true
},
"component-bind": {
@@ -1797,20 +1759,12 @@
"dev": true
},
"compressible": {
- "version": "2.0.14",
- "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.14.tgz",
- "integrity": "sha1-MmxfUH+7BV9UEWeCuWmoG2einac=",
+ "version": "2.0.13",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.13.tgz",
+ "integrity": "sha1-DRAgq5JLL9tNYnmHXH1tq6a6p6k=",
"dev": true,
"requires": {
- "mime-db": "1.34.0"
- },
- "dependencies": {
- "mime-db": {
- "version": "1.34.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.34.0.tgz",
- "integrity": "sha1-RS0Oz/XDA0am3B5kseruDTcZ/5o=",
- "dev": true
- }
+ "mime-db": "1.33.0"
}
},
"compression": {
@@ -1821,7 +1775,7 @@
"requires": {
"accepts": "1.3.5",
"bytes": "3.0.0",
- "compressible": "2.0.14",
+ "compressible": "2.0.13",
"debug": "2.6.9",
"on-headers": "1.0.1",
"safe-buffer": "5.1.1",
@@ -1848,7 +1802,7 @@
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"dev": true,
"requires": {
- "buffer-from": "1.1.0",
+ "buffer-from": "1.0.0",
"inherits": "2.0.3",
"readable-stream": "2.3.6",
"typedarray": "0.0.6"
@@ -1967,9 +1921,9 @@
"dev": true
},
"copy-webpack-plugin": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-4.4.3.tgz",
- "integrity": "sha512-v4THQ24Tks2NkyOvZuFDgZVfDD9YaA9rwYLZTrWg2GHIA8lrH5DboEyeoorh5Skki+PUbgSmnsCwhMWqYrQZrA==",
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-4.5.1.tgz",
+ "integrity": "sha512-OlTo6DYg0XfTKOF8eLf79wcHm4Ut10xU2cRBRPMW/NA5F9VMjZGTfRHWDIYC3s+1kObGYrBLshXWU1K0hILkNQ==",
"dev": true,
"requires": {
"cacache": "10.0.4",
@@ -1978,31 +1932,14 @@
"is-glob": "4.0.0",
"loader-utils": "1.1.0",
"minimatch": "3.0.4",
- "p-limit": "1.3.0",
+ "p-limit": "1.2.0",
"serialize-javascript": "1.5.0"
- },
- "dependencies": {
- "is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
- "dev": true
- },
- "is-glob": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
- "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=",
- "dev": true,
- "requires": {
- "is-extglob": "2.1.1"
- }
- }
}
},
"core-js": {
- "version": "2.5.7",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
- "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw=="
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz",
+ "integrity": "sha1-TekR5mew6ukSTjQlS1OupvxhjT4="
},
"core-object": {
"version": "3.1.5",
@@ -2010,7 +1947,7 @@
"integrity": "sha512-sA2/4+/PZ/KV6CKgjrVrrUVBKCkdDO02CUlQ0YKTQoYUwPYNOtOAcWlbYhd5v/1JqYaA6oZ4sDlOU4ppVw6Wbg==",
"dev": true,
"requires": {
- "chalk": "2.2.2"
+ "chalk": "2.4.1"
}
},
"core-util-is": {
@@ -2026,7 +1963,7 @@
"dev": true,
"requires": {
"is-directory": "0.3.1",
- "js-yaml": "3.12.0",
+ "js-yaml": "3.7.0",
"minimist": "1.2.0",
"object-assign": "4.1.1",
"os-homedir": "1.0.2",
@@ -2087,9 +2024,15 @@
"optional": true,
"requires": {
"lru-cache": "4.1.3",
- "which": "1.3.1"
+ "which": "1.3.0"
}
},
+ "crypt": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
+ "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=",
+ "dev": true
+ },
"cryptiles": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz",
@@ -2118,6 +2061,34 @@
"randomfill": "1.0.4"
}
},
+ "css-color-names": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz",
+ "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=",
+ "dev": true
+ },
+ "css-loader": {
+ "version": "0.28.11",
+ "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-0.28.11.tgz",
+ "integrity": "sha512-wovHgjAx8ZIMGSL8pTys7edA1ClmzxHeY6n/d97gg5odgsxEgKjULPR0viqyC+FWMCL9sfqoC/QCUBo62tLvPg==",
+ "dev": true,
+ "requires": {
+ "babel-code-frame": "6.26.0",
+ "css-selector-tokenizer": "0.7.0",
+ "cssnano": "3.10.0",
+ "icss-utils": "2.1.0",
+ "loader-utils": "1.1.0",
+ "lodash.camelcase": "4.3.0",
+ "object-assign": "4.1.1",
+ "postcss": "5.2.18",
+ "postcss-modules-extract-imports": "1.2.0",
+ "postcss-modules-local-by-default": "1.2.0",
+ "postcss-modules-scope": "1.1.0",
+ "postcss-modules-values": "1.3.0",
+ "postcss-value-parser": "3.3.0",
+ "source-list-map": "2.0.0"
+ }
+ },
"css-parse": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.7.0.tgz",
@@ -2168,11 +2139,55 @@
"integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=",
"dev": true
},
- "cuint": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz",
- "integrity": "sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs=",
- "dev": true
+ "cssnano": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-3.10.0.tgz",
+ "integrity": "sha1-Tzj2zqK5sX+gFJDyPx3GjqZcHDg=",
+ "dev": true,
+ "requires": {
+ "autoprefixer": "6.7.7",
+ "decamelize": "1.2.0",
+ "defined": "1.0.0",
+ "has": "1.0.1",
+ "object-assign": "4.1.1",
+ "postcss": "5.2.18",
+ "postcss-calc": "5.3.1",
+ "postcss-colormin": "2.2.2",
+ "postcss-convert-values": "2.6.1",
+ "postcss-discard-comments": "2.0.4",
+ "postcss-discard-duplicates": "2.1.0",
+ "postcss-discard-empty": "2.1.0",
+ "postcss-discard-overridden": "0.1.1",
+ "postcss-discard-unused": "2.2.3",
+ "postcss-filter-plugins": "2.0.3",
+ "postcss-merge-idents": "2.1.7",
+ "postcss-merge-longhand": "2.0.2",
+ "postcss-merge-rules": "2.1.2",
+ "postcss-minify-font-values": "1.0.5",
+ "postcss-minify-gradients": "1.0.5",
+ "postcss-minify-params": "1.2.2",
+ "postcss-minify-selectors": "2.1.1",
+ "postcss-normalize-charset": "1.1.1",
+ "postcss-normalize-url": "3.0.8",
+ "postcss-ordered-values": "2.2.3",
+ "postcss-reduce-idents": "2.4.0",
+ "postcss-reduce-initial": "1.0.1",
+ "postcss-reduce-transforms": "1.0.4",
+ "postcss-svgo": "2.1.6",
+ "postcss-unique-selectors": "2.0.2",
+ "postcss-value-parser": "3.3.0",
+ "postcss-zindex": "2.2.0"
+ }
+ },
+ "csso": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/csso/-/csso-2.3.2.tgz",
+ "integrity": "sha1-3dUsWHAz9J6Utx/FVWnyUuj/X4U=",
+ "dev": true,
+ "requires": {
+ "clap": "1.2.3",
+ "source-map": "0.5.7"
+ }
},
"currently-unhandled": {
"version": "0.4.1",
@@ -2201,7 +2216,7 @@
"integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=",
"dev": true,
"requires": {
- "es5-ext": "0.10.45"
+ "es5-ext": "0.10.42"
}
},
"dashdash": {
@@ -2221,19 +2236,6 @@
}
}
},
- "data-uri-to-buffer": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz",
- "integrity": "sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ==",
- "dev": true,
- "optional": true
- },
- "date-format": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/date-format/-/date-format-1.2.0.tgz",
- "integrity": "sha1-YV6CjiM90aubua4JUODOzPpuytg=",
- "dev": true
- },
"date-now": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
@@ -2249,11 +2251,16 @@
"ms": "2.0.0"
}
},
+ "debuglog": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz",
+ "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=",
+ "dev": true
+ },
"decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
- "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
- "dev": true
+ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"decode-uri-component": {
"version": "0.2.0",
@@ -2267,38 +2274,13 @@
"integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=",
"dev": true
},
- "deep-is": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
- "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
- "dev": true,
- "optional": true
- },
"default-require-extensions": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz",
- "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=",
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz",
+ "integrity": "sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=",
"dev": true,
"requires": {
- "strip-bom": "3.0.0"
- },
- "dependencies": {
- "strip-bom": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
- "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
- "dev": true
- }
- }
- },
- "define-properties": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz",
- "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=",
- "dev": true,
- "requires": {
- "foreach": "2.0.5",
- "object-keys": "1.0.12"
+ "strip-bom": "2.0.0"
}
},
"define-property": {
@@ -2339,41 +2321,14 @@
"is-data-descriptor": "1.0.0",
"kind-of": "6.0.2"
}
- },
- "isobject": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
- "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
- "dev": true
- },
- "kind-of": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
- "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
- "dev": true
}
}
},
- "degenerator": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-1.0.4.tgz",
- "integrity": "sha1-/PSQo37OJmRk2cxDGrmMWBnO0JU=",
- "dev": true,
- "optional": true,
- "requires": {
- "ast-types": "0.11.5",
- "escodegen": "1.10.0",
- "esprima": "3.1.3"
- },
- "dependencies": {
- "esprima": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz",
- "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=",
- "dev": true,
- "optional": true
- }
- }
+ "defined": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz",
+ "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=",
+ "dev": true
},
"del": {
"version": "3.0.0",
@@ -2409,6 +2364,12 @@
"dev": true
}
}
+ },
+ "pify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+ "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+ "dev": true
}
}
},
@@ -2467,6 +2428,16 @@
"integrity": "sha1-ogM8CcyOFY03dI+951B4Mr1s4Sc=",
"dev": true
},
+ "dezalgo": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz",
+ "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=",
+ "dev": true,
+ "requires": {
+ "asap": "2.0.6",
+ "wrappy": "1.0.2"
+ }
+ },
"di": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz",
@@ -2498,6 +2469,57 @@
"requires": {
"arrify": "1.0.1",
"path-type": "3.0.0"
+ },
+ "dependencies": {
+ "path-type": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
+ "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
+ "dev": true,
+ "requires": {
+ "pify": "3.0.0"
+ }
+ },
+ "pify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+ "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+ "dev": true
+ }
+ }
+ },
+ "directory-encoder": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/directory-encoder/-/directory-encoder-0.7.2.tgz",
+ "integrity": "sha1-WbTiqk8lQi9sY7UntGL14tDdLFg=",
+ "dev": true,
+ "requires": {
+ "fs-extra": "0.23.1",
+ "handlebars": "1.3.0",
+ "img-stats": "0.5.2"
+ },
+ "dependencies": {
+ "fs-extra": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.23.1.tgz",
+ "integrity": "sha1-ZhHbpq3yq43Jxp+rN83fiBgVfj0=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "4.1.11",
+ "jsonfile": "2.4.0",
+ "path-is-absolute": "1.0.1",
+ "rimraf": "2.6.2"
+ }
+ },
+ "jsonfile": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz",
+ "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "4.1.11"
+ }
+ }
}
},
"dns-equal": {
@@ -2603,12 +2625,11 @@
"domelementtype": "1.3.0"
}
},
- "double-ended-queue": {
- "version": "2.1.0-0",
- "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz",
- "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=",
- "dev": true,
- "optional": true
+ "duplexer": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
+ "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=",
+ "dev": true
},
"duplexify": {
"version": "3.6.0",
@@ -2645,9 +2666,9 @@
"dev": true
},
"electron-to-chromium": {
- "version": "1.3.49",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.49.tgz",
- "integrity": "sha1-ZROEsNgfB4qWY5srNpdRQbeRUAQ=",
+ "version": "1.3.48",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.48.tgz",
+ "integrity": "sha1-07DYWTgUBE4JLs4hCPw6ya6kuQA=",
"dev": true
},
"elliptic": {
@@ -2658,7 +2679,7 @@
"requires": {
"bn.js": "4.11.8",
"brorand": "1.1.0",
- "hash.js": "1.1.4",
+ "hash.js": "1.1.3",
"hmac-drbg": "1.0.1",
"inherits": "2.0.3",
"minimalistic-assert": "1.0.1",
@@ -2693,72 +2714,95 @@
}
},
"engine.io": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.1.5.tgz",
- "integrity": "sha512-D06ivJkYxyRrcEe0bTpNnBQNgP9d3xog+qZlLbui8EsMr/DouQpf5o9FzJnWYHEYE0YsFHllUv2R1dkgYZXHcA==",
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.8.2.tgz",
+ "integrity": "sha1-a1m+cws0jAElsKRYneHDVavPen4=",
"dev": true,
"requires": {
- "accepts": "1.3.5",
+ "accepts": "1.3.3",
"base64id": "1.0.0",
"cookie": "0.3.1",
- "debug": "3.1.0",
- "engine.io-parser": "2.1.2",
- "uws": "9.14.0",
- "ws": "3.3.3"
+ "debug": "2.3.3",
+ "engine.io-parser": "1.3.2",
+ "ws": "1.1.1"
},
"dependencies": {
- "debug": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
- "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "accepts": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz",
+ "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=",
"dev": true,
"requires": {
- "ms": "2.0.0"
+ "mime-types": "2.1.18",
+ "negotiator": "0.6.1"
}
+ },
+ "debug": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
+ "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=",
+ "dev": true,
+ "requires": {
+ "ms": "0.7.2"
+ }
+ },
+ "ms": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz",
+ "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=",
+ "dev": true
}
}
},
"engine.io-client": {
- "version": "3.1.6",
- "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.1.6.tgz",
- "integrity": "sha512-hnuHsFluXnsKOndS4Hv6SvUrgdYx1pk2NqfaDMW+GWdgfU3+/V25Cj7I8a0x92idSpa5PIhJRKxPvp9mnoLsfg==",
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.8.2.tgz",
+ "integrity": "sha1-w4dnVH8qfRhPV1L28K1QEAZwN2Y=",
"dev": true,
"requires": {
"component-emitter": "1.2.1",
"component-inherit": "0.0.3",
- "debug": "3.1.0",
- "engine.io-parser": "2.1.2",
+ "debug": "2.3.3",
+ "engine.io-parser": "1.3.2",
"has-cors": "1.1.0",
"indexof": "0.0.1",
+ "parsejson": "0.0.3",
"parseqs": "0.0.5",
"parseuri": "0.0.5",
- "ws": "3.3.3",
- "xmlhttprequest-ssl": "1.5.5",
+ "ws": "1.1.1",
+ "xmlhttprequest-ssl": "1.5.3",
"yeast": "0.1.2"
},
"dependencies": {
"debug": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
- "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
+ "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=",
"dev": true,
"requires": {
- "ms": "2.0.0"
+ "ms": "0.7.2"
}
+ },
+ "ms": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz",
+ "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=",
+ "dev": true
}
}
},
"engine.io-parser": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.2.tgz",
- "integrity": "sha512-dInLFzr80RijZ1rGpx1+56/uFoH7/7InhH3kZt+Ms6hT8tNx3NGW/WNSA/f8As1WkOfkuyb3tnRyuXGxusclMw==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.3.2.tgz",
+ "integrity": "sha1-k3sHnwAH0Ik+xW1GyyILjLQ1Igo=",
"dev": true,
"requires": {
"after": "0.8.2",
- "arraybuffer.slice": "0.0.7",
+ "arraybuffer.slice": "0.0.6",
"base64-arraybuffer": "0.1.5",
"blob": "0.0.4",
- "has-binary2": "1.0.3"
+ "has-binary": "0.1.7",
+ "wtf-8": "1.0.0"
}
},
"enhanced-resolve": {
@@ -2795,42 +2839,17 @@
}
},
"error-ex": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
- "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
- "dev": true,
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz",
+ "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=",
"requires": {
"is-arrayish": "0.2.1"
}
},
- "es-abstract": {
- "version": "1.12.0",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz",
- "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==",
- "dev": true,
- "requires": {
- "es-to-primitive": "1.1.1",
- "function-bind": "1.1.1",
- "has": "1.0.3",
- "is-callable": "1.1.3",
- "is-regex": "1.0.4"
- }
- },
- "es-to-primitive": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz",
- "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=",
- "dev": true,
- "requires": {
- "is-callable": "1.1.3",
- "is-date-object": "1.0.1",
- "is-symbol": "1.0.1"
- }
- },
"es5-ext": {
- "version": "0.10.45",
- "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.45.tgz",
- "integrity": "sha512-FkfM6Vxxfmztilbxxz5UKSD4ICMf5tSpRFtDNtkAhOxZ0EKtX6qwmXNyH/sFyIbX2P/nU5AMiA9jilWsUGJzCQ==",
+ "version": "0.10.42",
+ "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.42.tgz",
+ "integrity": "sha512-AJxO1rmPe1bDEfSR6TJ/FgMFYuTBhR5R57KW58iCkYACMyFbrkqVyzXSurYoScDGvgyMpk7uRF/lPUPPTmsRSA==",
"dev": true,
"requires": {
"es6-iterator": "2.0.3",
@@ -2845,7 +2864,7 @@
"dev": true,
"requires": {
"d": "1.0.0",
- "es5-ext": "0.10.45",
+ "es5-ext": "0.10.42",
"es6-symbol": "3.1.1"
}
},
@@ -2856,28 +2875,13 @@
"dev": true,
"requires": {
"d": "1.0.0",
- "es5-ext": "0.10.45",
+ "es5-ext": "0.10.42",
"es6-iterator": "2.0.3",
"es6-set": "0.1.5",
"es6-symbol": "3.1.1",
"event-emitter": "0.3.5"
}
},
- "es6-promise": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz",
- "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==",
- "dev": true
- },
- "es6-promisify": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
- "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
- "dev": true,
- "requires": {
- "es6-promise": "4.2.4"
- }
- },
"es6-set": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz",
@@ -2885,7 +2889,7 @@
"dev": true,
"requires": {
"d": "1.0.0",
- "es5-ext": "0.10.45",
+ "es5-ext": "0.10.42",
"es6-iterator": "2.0.3",
"es6-symbol": "3.1.1",
"event-emitter": "0.3.5"
@@ -2898,7 +2902,7 @@
"dev": true,
"requires": {
"d": "1.0.0",
- "es5-ext": "0.10.45"
+ "es5-ext": "0.10.42"
}
},
"es6-weak-map": {
@@ -2908,7 +2912,7 @@
"dev": true,
"requires": {
"d": "1.0.0",
- "es5-ext": "0.10.45",
+ "es5-ext": "0.10.42",
"es6-iterator": "2.0.3",
"es6-symbol": "3.1.1"
}
@@ -2925,36 +2929,6 @@
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true
},
- "escodegen": {
- "version": "1.10.0",
- "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.10.0.tgz",
- "integrity": "sha512-fjUOf8johsv23WuIKdNQU4P9t9jhQ4Qzx6pC2uW890OloK3Zs1ZAoCNpg/2larNF501jLl3UNy0kIRcF6VI22g==",
- "dev": true,
- "optional": true,
- "requires": {
- "esprima": "3.1.3",
- "estraverse": "4.2.0",
- "esutils": "2.0.2",
- "optionator": "0.8.2",
- "source-map": "0.6.1"
- },
- "dependencies": {
- "esprima": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz",
- "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=",
- "dev": true,
- "optional": true
- },
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "optional": true
- }
- }
- },
"escope": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz",
@@ -2968,9 +2942,9 @@
}
},
"esprima": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz",
- "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==",
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz",
+ "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=",
"dev": true
},
"esrecurse": {
@@ -3007,7 +2981,7 @@
"dev": true,
"requires": {
"d": "1.0.0",
- "es5-ext": "0.10.45"
+ "es5-ext": "0.10.42"
}
},
"eventemitter3": {
@@ -3064,7 +3038,7 @@
"requires": {
"lru-cache": "4.1.3",
"shebang-command": "1.2.0",
- "which": "1.3.1"
+ "which": "1.3.0"
}
}
}
@@ -3086,6 +3060,12 @@
"braces": "0.1.5"
},
"dependencies": {
+ "array-unique": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
+ "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
+ "dev": true
+ },
"braces": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/braces/-/braces-0.1.5.tgz",
@@ -3120,12 +3100,38 @@
}
},
"expand-brackets": {
- "version": "0.1.5",
- "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz",
- "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=",
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
+ "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
"dev": true,
"requires": {
- "is-posix-bracket": "0.1.1"
+ "debug": "2.6.9",
+ "define-property": "0.2.5",
+ "extend-shallow": "2.0.1",
+ "posix-character-classes": "0.1.1",
+ "regex-not": "1.0.2",
+ "snapdragon": "0.8.2",
+ "to-regex": "3.0.2"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "0.1.6"
+ }
+ },
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "0.1.1"
+ }
+ }
}
},
"expand-range": {
@@ -3135,6 +3141,58 @@
"dev": true,
"requires": {
"fill-range": "2.2.4"
+ },
+ "dependencies": {
+ "fill-range": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz",
+ "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==",
+ "dev": true,
+ "requires": {
+ "is-number": "2.1.0",
+ "isobject": "2.1.0",
+ "randomatic": "3.0.0",
+ "repeat-element": "1.1.2",
+ "repeat-string": "1.6.1"
+ }
+ },
+ "is-number": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz",
+ "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=",
+ "dev": true,
+ "requires": {
+ "kind-of": "3.2.2"
+ }
+ },
+ "isobject": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+ "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+ "dev": true,
+ "requires": {
+ "isarray": "1.0.0"
+ }
+ },
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "1.1.6"
+ }
+ }
+ }
+ },
+ "exports-loader": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/exports-loader/-/exports-loader-0.6.4.tgz",
+ "integrity": "sha1-1w/GEhl1s1/BKDDPUnVL4nQPyIY=",
+ "dev": true,
+ "requires": {
+ "loader-utils": "1.1.0",
+ "source-map": "0.5.7"
}
},
"express": {
@@ -3223,46 +3281,145 @@
}
},
"extglob": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
- "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=",
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
+ "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
"dev": true,
"requires": {
- "is-extglob": "1.0.0"
+ "array-unique": "0.3.2",
+ "define-property": "1.0.0",
+ "expand-brackets": "2.1.4",
+ "extend-shallow": "2.0.1",
+ "fragment-cache": "0.2.1",
+ "regex-not": "1.0.2",
+ "snapdragon": "0.8.2",
+ "to-regex": "3.0.2"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "1.0.2"
+ }
+ },
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "0.1.1"
+ }
+ },
+ "is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "6.0.2"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "6.0.2"
+ }
+ },
+ "is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "requires": {
+ "is-accessor-descriptor": "1.0.0",
+ "is-data-descriptor": "1.0.0",
+ "kind-of": "6.0.2"
+ }
+ }
}
},
"extract-text-webpack-plugin": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.2.tgz",
- "integrity": "sha512-bt/LZ4m5Rqt/Crl2HiKuAl/oqg0psx1tsTLkvWbJen1CtD+fftkZhMaQ9HOtY2gWsl2Wq+sABmMVi9z3DhKWQQ==",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.0.tgz",
+ "integrity": "sha1-kMqnkHvESfM1AF46x1MrQbAN5hI=",
"dev": true,
"requires": {
"async": "2.6.1",
"loader-utils": "1.1.0",
"schema-utils": "0.3.0",
"webpack-sources": "1.1.0"
+ }
+ },
+ "extract-zip": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.5.0.tgz",
+ "integrity": "sha1-ksz22B73Cp+kwXRxFMzvbYaIpsQ=",
+ "dev": true,
+ "requires": {
+ "concat-stream": "1.5.0",
+ "debug": "0.7.4",
+ "mkdirp": "0.5.0",
+ "yauzl": "2.4.1"
},
"dependencies": {
- "ajv": {
- "version": "5.5.2",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
- "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
+ "concat-stream": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.0.tgz",
+ "integrity": "sha1-U/fUPFHF5D+ByP3QMyHGMb5o1hE=",
"dev": true,
"requires": {
- "co": "4.6.0",
- "fast-deep-equal": "1.1.0",
- "fast-json-stable-stringify": "2.0.0",
- "json-schema-traverse": "0.3.1"
+ "inherits": "2.0.3",
+ "readable-stream": "2.0.6",
+ "typedarray": "0.0.6"
}
},
- "schema-utils": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz",
- "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=",
+ "debug": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz",
+ "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk=",
+ "dev": true
+ },
+ "mkdirp": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz",
+ "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=",
"dev": true,
"requires": {
- "ajv": "5.5.2"
+ "minimist": "0.0.8"
}
+ },
+ "process-nextick-args": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
+ "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=",
+ "dev": true
+ },
+ "readable-stream": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz",
+ "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=",
+ "dev": true,
+ "requires": {
+ "core-util-is": "1.0.2",
+ "inherits": "2.0.3",
+ "isarray": "1.0.0",
+ "process-nextick-args": "1.0.7",
+ "string_decoder": "0.10.31",
+ "util-deprecate": "1.0.2"
+ }
+ },
+ "string_decoder": {
+ "version": "0.10.31",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+ "dev": true
}
}
},
@@ -3284,13 +3441,6 @@
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
"dev": true
},
- "fast-levenshtein": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
- "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
- "dev": true,
- "optional": true
- },
"fastparse": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz",
@@ -3306,22 +3456,23 @@
"websocket-driver": "0.7.0"
}
},
- "file-loader": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.1.11.tgz",
- "integrity": "sha512-TGR4HU7HUsGg6GCOPJnFk06RhWgEWFLAGWiT6rcD+GRC2keU3s9RGJ+b3Z6/U73jwwNb2gKLJ7YCrp+jvU4ALg==",
+ "fd-slicer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz",
+ "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=",
"dev": true,
"requires": {
- "loader-utils": "1.1.0",
- "schema-utils": "0.4.5"
+ "pend": "1.2.0"
}
},
- "file-uri-to-path": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
- "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "file-loader": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-0.10.1.tgz",
+ "integrity": "sha1-gVA0EZiR/GRB+1pkwRvJPCLd2EI=",
"dev": true,
- "optional": true
+ "requires": {
+ "loader-utils": "1.1.0"
+ }
},
"filename-regex": {
"version": "2.0.1",
@@ -3339,17 +3490,33 @@
"minimatch": "3.0.4"
}
},
+ "filesize": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz",
+ "integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==",
+ "dev": true
+ },
"fill-range": {
- "version": "2.2.4",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz",
- "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
"dev": true,
"requires": {
- "is-number": "2.1.0",
- "isobject": "2.1.0",
- "randomatic": "3.0.0",
- "repeat-element": "1.1.2",
- "repeat-string": "1.6.1"
+ "extend-shallow": "2.0.1",
+ "is-number": "3.0.0",
+ "repeat-string": "1.6.1",
+ "to-regex-range": "2.1.1"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "0.1.1"
+ }
+ }
}
},
"finalhandler": {
@@ -3379,14 +3546,44 @@
}
},
"find-up": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
- "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
+ "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
+ "requires": {
+ "path-exists": "2.1.0",
+ "pinkie-promise": "2.0.1"
+ }
+ },
+ "findup-sync": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz",
+ "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=",
"dev": true,
"requires": {
- "locate-path": "2.0.0"
+ "glob": "5.0.15"
+ },
+ "dependencies": {
+ "glob": {
+ "version": "5.0.15",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
+ "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=",
+ "dev": true,
+ "requires": {
+ "inflight": "1.0.6",
+ "inherits": "2.0.3",
+ "minimatch": "3.0.4",
+ "once": "1.4.0",
+ "path-is-absolute": "1.0.1"
+ }
+ }
}
},
+ "flatten": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz",
+ "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=",
+ "dev": true
+ },
"flush-write-stream": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz",
@@ -3424,20 +3621,14 @@
"dev": true
},
"for-own": {
- "version": "0.1.5",
- "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz",
- "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=",
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
+ "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=",
"dev": true,
"requires": {
"for-in": "1.0.2"
}
},
- "foreach": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz",
- "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=",
- "dev": true
- },
"forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@@ -3503,7 +3694,7 @@
"requires": {
"graceful-fs": "4.1.11",
"jsonfile": "4.0.0",
- "universalify": "0.1.2"
+ "universalify": "0.1.1"
}
},
"fs-write-stream-atomic": {
@@ -4065,46 +4256,6 @@
"rimraf": "2.6.2"
}
},
- "ftp": {
- "version": "0.3.10",
- "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz",
- "integrity": "sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=",
- "dev": true,
- "optional": true,
- "requires": {
- "readable-stream": "1.1.14",
- "xregexp": "2.0.0"
- },
- "dependencies": {
- "isarray": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
- "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
- "dev": true,
- "optional": true
- },
- "readable-stream": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
- "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
- "dev": true,
- "optional": true,
- "requires": {
- "core-util-is": "1.0.2",
- "inherits": "2.0.3",
- "isarray": "0.0.1",
- "string_decoder": "0.10.31"
- }
- },
- "string_decoder": {
- "version": "0.10.31",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
- "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
- "dev": true,
- "optional": true
- }
- }
- },
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -4124,7 +4275,7 @@
"signal-exit": "3.0.2",
"string-width": "1.0.2",
"strip-ansi": "3.0.1",
- "wide-align": "1.1.3"
+ "wide-align": "1.1.2"
}
},
"gaze": {
@@ -4134,22 +4285,20 @@
"dev": true,
"optional": true,
"requires": {
- "globule": "1.2.1"
+ "globule": "1.2.0"
}
},
"generate-function": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz",
"integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=",
- "dev": true,
- "optional": true
+ "dev": true
},
"generate-object-property": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz",
"integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=",
"dev": true,
- "optional": true,
"requires": {
"is-property": "1.0.2"
}
@@ -4157,8 +4306,7 @@
"get-caller-file": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz",
- "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=",
- "dev": true
+ "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U="
},
"get-stdin": {
"version": "4.0.1",
@@ -4172,21 +4320,6 @@
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
"dev": true
},
- "get-uri": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-2.0.2.tgz",
- "integrity": "sha512-ZD325dMZOgerGqF/rF6vZXyFGTAay62svjQIT+X/oU2PtxYpFxvSkbsdi+oxIrsNxlZVd4y8wUDqkaExWTI/Cw==",
- "dev": true,
- "optional": true,
- "requires": {
- "data-uri-to-buffer": "1.2.0",
- "debug": "2.6.9",
- "extend": "3.0.1",
- "file-uri-to-path": "1.0.0",
- "ftp": "0.3.10",
- "readable-stream": "2.3.6"
- }
- },
"get-value": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
@@ -4232,15 +4365,53 @@
"requires": {
"glob-parent": "2.0.0",
"is-glob": "2.0.1"
+ },
+ "dependencies": {
+ "glob-parent": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
+ "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
+ "dev": true,
+ "requires": {
+ "is-glob": "2.0.1"
+ }
+ },
+ "is-extglob": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+ "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+ "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+ "dev": true,
+ "requires": {
+ "is-extglob": "1.0.0"
+ }
+ }
}
},
"glob-parent": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
- "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+ "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
"dev": true,
"requires": {
- "is-glob": "2.0.1"
+ "is-glob": "3.1.0",
+ "path-dirname": "1.0.2"
+ },
+ "dependencies": {
+ "is-glob": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+ "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+ "dev": true,
+ "requires": {
+ "is-extglob": "2.1.1"
+ }
+ }
}
},
"globals": {
@@ -4258,15 +4429,23 @@
"array-union": "1.0.2",
"dir-glob": "2.0.0",
"glob": "7.1.2",
- "ignore": "3.3.10",
+ "ignore": "3.3.8",
"pify": "3.0.0",
"slash": "1.0.0"
+ },
+ "dependencies": {
+ "pify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+ "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+ "dev": true
+ }
}
},
"globule": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz",
- "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.0.tgz",
+ "integrity": "sha1-HcScaCLdnoovoAuiopUAboZkvQk=",
"dev": true,
"optional": true,
"requires": {
@@ -4278,8 +4457,25 @@
"graceful-fs": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
- "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
- "dev": true
+ "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
+ },
+ "gzip-size": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-4.1.0.tgz",
+ "integrity": "sha1-iuCWJX6r59acRb4rZ8RIEk/7UXw=",
+ "dev": true,
+ "requires": {
+ "duplexer": "0.1.1",
+ "pify": "3.0.0"
+ },
+ "dependencies": {
+ "pify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+ "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+ "dev": true
+ }
+ }
},
"hammerjs": {
"version": "2.0.8",
@@ -4293,83 +4489,42 @@
"dev": true
},
"handlebars": {
- "version": "4.0.11",
- "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz",
- "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-1.3.0.tgz",
+ "integrity": "sha1-npsTCpPjiUkTItl1zz7BgYw3zjQ=",
"dev": true,
"requires": {
- "async": "1.5.2",
- "optimist": "0.6.1",
- "source-map": "0.4.4",
- "uglify-js": "2.8.29"
+ "optimist": "0.3.7",
+ "uglify-js": "2.3.6"
},
"dependencies": {
"async": {
- "version": "1.5.2",
- "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
- "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
- "dev": true
- },
- "camelcase": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz",
- "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=",
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
+ "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=",
"dev": true,
"optional": true
},
- "cliui": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz",
- "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=",
+ "source-map": {
+ "version": "0.1.43",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
+ "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=",
"dev": true,
"optional": true,
"requires": {
- "center-align": "0.1.3",
- "right-align": "0.1.3",
- "wordwrap": "0.0.2"
- }
- },
- "source-map": {
- "version": "0.4.4",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
- "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
- "dev": true,
- "requires": {
"amdefine": "1.0.1"
}
},
"uglify-js": {
- "version": "2.8.29",
- "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
- "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=",
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.3.6.tgz",
+ "integrity": "sha1-+gmEdwtCi3qbKoBY9GNV0U/vIRo=",
"dev": true,
"optional": true,
"requires": {
- "source-map": "0.5.7",
- "uglify-to-browserify": "1.0.2",
- "yargs": "3.10.0"
- },
- "dependencies": {
- "source-map": {
- "version": "0.5.7",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
- "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
- "dev": true,
- "optional": true
- }
- }
- },
- "yargs": {
- "version": "3.10.0",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
- "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=",
- "dev": true,
- "optional": true,
- "requires": {
- "camelcase": "1.2.1",
- "cliui": "2.1.0",
- "decamelize": "1.2.0",
- "window-size": "0.1.0"
+ "async": "0.2.10",
+ "optimist": "0.3.7",
+ "source-map": "0.1.43"
}
}
}
@@ -4403,9 +4558,9 @@
}
},
"has": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
- "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz",
+ "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=",
"dev": true,
"requires": {
"function-bind": "1.1.1"
@@ -4420,19 +4575,19 @@
"ansi-regex": "2.1.1"
}
},
- "has-binary2": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
- "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
+ "has-binary": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.7.tgz",
+ "integrity": "sha1-aOYesWIQyVRaClzOBqhzkS/h5ow=",
"dev": true,
"requires": {
- "isarray": "2.0.1"
+ "isarray": "0.0.1"
},
"dependencies": {
"isarray": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
- "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=",
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
"dev": true
}
}
@@ -4444,9 +4599,9 @@
"dev": true
},
"has-flag": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
- "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=",
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
+ "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
"dev": true
},
"has-unicode": {
@@ -4464,14 +4619,6 @@
"get-value": "2.0.6",
"has-values": "1.0.0",
"isobject": "3.0.1"
- },
- "dependencies": {
- "isobject": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
- "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
- "dev": true
- }
}
},
"has-values": {
@@ -4484,26 +4631,6 @@
"kind-of": "4.0.0"
},
"dependencies": {
- "is-number": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
- "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
- "dev": true,
- "requires": {
- "kind-of": "3.2.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "1.1.6"
- }
- }
- }
- },
"kind-of": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
@@ -4526,15 +4653,25 @@
}
},
"hash.js": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.4.tgz",
- "integrity": "sha512-A6RlQvvZEtFS5fLU43IDu0QUmBy+fDO9VMdTXvufKwIkt/rFfvICAViCax5fbDO4zdNzaC3/27ZhKUok5bAJyw==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz",
+ "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==",
"dev": true,
"requires": {
"inherits": "2.0.3",
"minimalistic-assert": "1.0.1"
}
},
+ "hasha": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz",
+ "integrity": "sha1-eNfL/B5tZjA/55g3NlmEUXsvbuE=",
+ "dev": true,
+ "requires": {
+ "is-stream": "1.1.0",
+ "pinkie-promise": "2.0.1"
+ }
+ },
"hawk": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz",
@@ -4553,16 +4690,10 @@
"integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
"dev": true
},
- "hipchat-notifier": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/hipchat-notifier/-/hipchat-notifier-1.1.0.tgz",
- "integrity": "sha1-ttJJdVQ3wZEII2d5nTupoPI7Ix4=",
- "dev": true,
- "optional": true,
- "requires": {
- "lodash": "4.17.10",
- "request": "2.81.0"
- }
+ "highlight.js": {
+ "version": "9.10.0",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.10.0.tgz",
+ "integrity": "sha1-+fCxTAvgDw5PseV3t0n+2eb1L1U="
},
"hmac-drbg": {
"version": "1.0.1",
@@ -4570,31 +4701,21 @@
"integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
"dev": true,
"requires": {
- "hash.js": "1.1.4",
+ "hash.js": "1.1.3",
"minimalistic-assert": "1.0.1",
"minimalistic-crypto-utils": "1.0.1"
}
},
"hoek": {
- "version": "4.2.1",
+ "version": "2.16.3",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
"integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=",
"dev": true
},
- "homedir-polyfill": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz",
- "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=",
- "dev": true,
- "requires": {
- "parse-passwd": "1.0.0"
- }
- },
"hosted-git-info": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz",
- "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==",
- "dev": true
+ "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw=="
},
"hpack.js": {
"version": "2.1.6",
@@ -4608,6 +4729,12 @@
"wbuf": "1.7.3"
}
},
+ "html-comment-regex": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.1.tgz",
+ "integrity": "sha1-ZouTd26q5V696POtRkswekljYl4=",
+ "dev": true
+ },
"html-entities": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz",
@@ -4626,7 +4753,7 @@
"he": "1.1.1",
"param-case": "2.1.1",
"relateurl": "0.2.7",
- "uglify-js": "3.3.28"
+ "uglify-js": "3.3.27"
}
},
"html-webpack-plugin": {
@@ -4739,27 +4866,6 @@
"requires-port": "1.0.0"
}
},
- "http-proxy-agent": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz",
- "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==",
- "dev": true,
- "requires": {
- "agent-base": "4.2.0",
- "debug": "3.1.0"
- },
- "dependencies": {
- "debug": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
- "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
- }
- }
- }
- },
"http-proxy-middleware": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz",
@@ -4772,12 +4878,58 @@
"micromatch": "2.3.11"
},
"dependencies": {
- "is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "arr-diff": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
+ "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
+ "dev": true,
+ "requires": {
+ "arr-flatten": "1.1.0"
+ }
+ },
+ "array-unique": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
+ "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
"dev": true
},
+ "braces": {
+ "version": "1.8.5",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz",
+ "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=",
+ "dev": true,
+ "requires": {
+ "expand-range": "1.8.2",
+ "preserve": "0.2.0",
+ "repeat-element": "1.1.2"
+ }
+ },
+ "expand-brackets": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz",
+ "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=",
+ "dev": true,
+ "requires": {
+ "is-posix-bracket": "0.1.1"
+ }
+ },
+ "extglob": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
+ "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=",
+ "dev": true,
+ "requires": {
+ "is-extglob": "1.0.0"
+ },
+ "dependencies": {
+ "is-extglob": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+ "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+ "dev": true
+ }
+ }
+ },
"is-glob": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
@@ -4786,6 +4938,53 @@
"requires": {
"is-extglob": "2.1.1"
}
+ },
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "1.1.6"
+ }
+ },
+ "micromatch": {
+ "version": "2.3.11",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
+ "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
+ "dev": true,
+ "requires": {
+ "arr-diff": "2.0.0",
+ "array-unique": "0.2.1",
+ "braces": "1.8.5",
+ "expand-brackets": "0.1.5",
+ "extglob": "0.3.2",
+ "filename-regex": "2.0.1",
+ "is-extglob": "1.0.0",
+ "is-glob": "2.0.1",
+ "kind-of": "3.2.2",
+ "normalize-path": "2.1.1",
+ "object.omit": "2.0.1",
+ "parse-glob": "3.0.4",
+ "regex-cache": "0.4.4"
+ },
+ "dependencies": {
+ "is-extglob": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+ "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+ "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+ "dev": true,
+ "requires": {
+ "is-extglob": "1.0.0"
+ }
+ }
+ }
}
}
},
@@ -4797,25 +4996,9 @@
"requires": {
"assert-plus": "0.2.0",
"jsprim": "1.4.1",
- "sshpk": "1.14.2"
+ "sshpk": "1.14.1"
}
},
- "httpntlm": {
- "version": "1.6.1",
- "resolved": "https://registry.npmjs.org/httpntlm/-/httpntlm-1.6.1.tgz",
- "integrity": "sha1-rQFScUOi6Hc8+uapb1hla7UqNLI=",
- "dev": true,
- "requires": {
- "httpreq": "0.4.24",
- "underscore": "1.7.0"
- }
- },
- "httpreq": {
- "version": "0.4.24",
- "resolved": "https://registry.npmjs.org/httpreq/-/httpreq-0.4.24.tgz",
- "integrity": "sha1-QzX/2CzZaWaKOUZckprGHWOTYn8=",
- "dev": true
- },
"https-browserify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
@@ -4823,24 +5006,14 @@
"dev": true
},
"https-proxy-agent": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz",
- "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==",
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz",
+ "integrity": "sha1-NffabEjOTdv6JkiRrFk+5f+GceY=",
"dev": true,
"requires": {
- "agent-base": "4.2.0",
- "debug": "3.1.0"
- },
- "dependencies": {
- "debug": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
- "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
- }
- }
+ "agent-base": "2.1.1",
+ "debug": "2.6.9",
+ "extend": "3.0.1"
}
},
"iconv-lite": {
@@ -4849,10 +5022,59 @@
"integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==",
"dev": true
},
+ "icss-replace-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz",
+ "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=",
+ "dev": true
+ },
+ "icss-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-2.1.0.tgz",
+ "integrity": "sha1-g/Cg7DeL8yRheLbCrZE28TWxyWI=",
+ "dev": true,
+ "requires": {
+ "postcss": "6.0.22"
+ },
+ "dependencies": {
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "postcss": {
+ "version": "6.0.22",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.22.tgz",
+ "integrity": "sha512-Toc9lLoUASwGqxBSJGTVcOQiDqjK+Z2XlWBg+IgYwQMY9vA2f7iMpXVc1GpPcfTSyM5lkxNo0oDwDRO+wm7XHA==",
+ "dev": true,
+ "requires": {
+ "chalk": "2.4.1",
+ "source-map": "0.6.1",
+ "supports-color": "5.4.0"
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
+ "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
+ "dev": true,
+ "requires": {
+ "has-flag": "3.0.0"
+ }
+ }
+ }
+ },
"ieee754": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz",
- "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==",
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.11.tgz",
+ "integrity": "sha512-VhDzCKN7K8ufStx/CLj5/PDTMgph+qwN5Pkd5i0sGnVwk56zJ0lkT8Qzi1xqWLS0Wp29DgDtNeS7v8/wMoZeHg==",
"dev": true
},
"iferr": {
@@ -4862,9 +5084,9 @@
"dev": true
},
"ignore": {
- "version": "3.3.10",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz",
- "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==",
+ "version": "3.3.8",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.8.tgz",
+ "integrity": "sha512-pUh+xUQQhQzevjRHHFqqcTy0/dP/kS9I8HSrUydhihjuD09W6ldVWFtIrwhXdUJHis3i2rZNqEHpZH/cbinFbg==",
"dev": true
},
"image-size": {
@@ -4874,14 +5096,13 @@
"dev": true,
"optional": true
},
- "import-local": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz",
- "integrity": "sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ==",
+ "img-stats": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/img-stats/-/img-stats-0.5.2.tgz",
+ "integrity": "sha1-wgNJbELy2esuWrgjL6dWurMsnis=",
"dev": true,
"requires": {
- "pkg-dir": "2.0.0",
- "resolve-cwd": "2.0.0"
+ "xmldom": "0.1.27"
}
},
"imurmurhash": {
@@ -4906,19 +5127,18 @@
"repeating": "2.0.1"
}
},
+ "indexes-of": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
+ "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=",
+ "dev": true
+ },
"indexof": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
"integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=",
"dev": true
},
- "inflection": {
- "version": "1.12.0",
- "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz",
- "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=",
- "dev": true,
- "optional": true
- },
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -4968,8 +5188,7 @@
"invert-kv": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
- "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
- "dev": true
+ "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY="
},
"ip": {
"version": "1.1.5",
@@ -4983,6 +5202,12 @@
"integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=",
"dev": true
},
+ "is-absolute-url": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz",
+ "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=",
+ "dev": true
+ },
"is-accessor-descriptor": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
@@ -4990,13 +5215,23 @@
"dev": true,
"requires": {
"kind-of": "3.2.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "1.1.6"
+ }
+ }
}
},
"is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
- "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
- "dev": true
+ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
},
"is-binary-path": {
"version": "1.0.1",
@@ -5017,17 +5252,10 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
"integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=",
- "dev": true,
"requires": {
"builtin-modules": "1.1.1"
}
},
- "is-callable": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz",
- "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=",
- "dev": true
- },
"is-data-descriptor": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
@@ -5035,14 +5263,19 @@
"dev": true,
"requires": {
"kind-of": "3.2.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "1.1.6"
+ }
+ }
}
},
- "is-date-object": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
- "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=",
- "dev": true
- },
"is-descriptor": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
@@ -5090,9 +5323,9 @@
"dev": true
},
"is-extglob": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
- "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"dev": true
},
"is-finite": {
@@ -5108,33 +5341,30 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
- "dev": true,
"requires": {
"number-is-nan": "1.0.1"
}
},
"is-glob": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
- "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
+ "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=",
"dev": true,
"requires": {
- "is-extglob": "1.0.0"
+ "is-extglob": "2.1.1"
}
},
"is-my-ip-valid": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz",
"integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==",
- "dev": true,
- "optional": true
+ "dev": true
},
"is-my-json-valid": {
"version": "2.17.2",
"resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz",
"integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==",
"dev": true,
- "optional": true,
"requires": {
"generate-function": "2.0.0",
"generate-object-property": "1.2.0",
@@ -5144,12 +5374,23 @@
}
},
"is-number": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz",
- "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
"dev": true,
"requires": {
"kind-of": "3.2.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "1.1.6"
+ }
+ }
}
},
"is-odd": {
@@ -5193,6 +5434,12 @@
"path-is-inside": "1.0.2"
}
},
+ "is-plain-obj": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
+ "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
+ "dev": true
+ },
"is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@@ -5200,14 +5447,6 @@
"dev": true,
"requires": {
"isobject": "3.0.1"
- },
- "dependencies": {
- "isobject": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
- "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
- "dev": true
- }
}
},
"is-posix-bracket": {
@@ -5226,17 +5465,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
"integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=",
- "dev": true,
- "optional": true
- },
- "is-regex": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
- "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=",
- "dev": true,
- "requires": {
- "has": "1.0.3"
- }
+ "dev": true
},
"is-stream": {
"version": "1.1.0",
@@ -5244,11 +5473,14 @@
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
"dev": true
},
- "is-symbol": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz",
- "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=",
- "dev": true
+ "is-svg": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-2.1.0.tgz",
+ "integrity": "sha1-z2EJDaDZ77yrhyLeum8DIgjbsOk=",
+ "dev": true,
+ "requires": {
+ "html-comment-regex": "1.1.1"
+ }
},
"is-typedarray": {
"version": "1.0.0",
@@ -5259,8 +5491,7 @@
"is-utf8": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
- "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
- "dev": true
+ "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI="
},
"is-windows": {
"version": "1.0.2",
@@ -5293,13 +5524,10 @@
"dev": true
},
"isobject": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
- "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
- "dev": true,
- "requires": {
- "isarray": "1.0.0"
- }
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+ "dev": true
},
"isstream": {
"version": "0.1.2",
@@ -5314,50 +5542,41 @@
"dev": true,
"requires": {
"async": "2.6.1",
- "compare-versions": "3.3.0",
+ "compare-versions": "3.2.1",
"fileset": "2.0.3",
"istanbul-lib-coverage": "1.2.0",
- "istanbul-lib-hook": "1.2.1",
+ "istanbul-lib-hook": "1.2.0",
"istanbul-lib-instrument": "1.10.1",
"istanbul-lib-report": "1.1.4",
- "istanbul-lib-source-maps": "1.2.5",
+ "istanbul-lib-source-maps": "1.2.4",
"istanbul-reports": "1.3.0",
- "js-yaml": "3.12.0",
+ "js-yaml": "3.7.0",
"mkdirp": "0.5.1",
"once": "1.4.0"
}
},
"istanbul-instrumenter-loader": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/istanbul-instrumenter-loader/-/istanbul-instrumenter-loader-3.0.1.tgz",
- "integrity": "sha512-a5SPObZgS0jB/ixaKSMdn6n/gXSrK2S6q/UfRJBT3e6gQmVjwZROTODQsYW5ZNwOu78hG62Y3fWlebaVOL0C+w==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/istanbul-instrumenter-loader/-/istanbul-instrumenter-loader-2.0.0.tgz",
+ "integrity": "sha1-5UkpAKsLuoNe+oAkywC+mz7qJwA=",
"dev": true,
"requires": {
"convert-source-map": "1.5.1",
"istanbul-lib-instrument": "1.10.1",
- "loader-utils": "1.1.0",
- "schema-utils": "0.3.0"
+ "loader-utils": "0.2.17",
+ "object-assign": "4.1.1"
},
"dependencies": {
- "ajv": {
- "version": "5.5.2",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
- "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
+ "loader-utils": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz",
+ "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=",
"dev": true,
"requires": {
- "co": "4.6.0",
- "fast-deep-equal": "1.1.0",
- "fast-json-stable-stringify": "2.0.0",
- "json-schema-traverse": "0.3.1"
- }
- },
- "schema-utils": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz",
- "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=",
- "dev": true,
- "requires": {
- "ajv": "5.5.2"
+ "big.js": "3.2.0",
+ "emojis-list": "2.1.0",
+ "json5": "0.5.1",
+ "object-assign": "4.1.1"
}
}
}
@@ -5369,12 +5588,12 @@
"dev": true
},
"istanbul-lib-hook": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.1.tgz",
- "integrity": "sha512-eLAMkPG9FU0v5L02lIkcj/2/Zlz9OuluaXikdr5iStk8FDbSwAixTK9TkYxbF0eNnzAJTwM2fkV2A1tpsIp4Jg==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.0.tgz",
+ "integrity": "sha512-p3En6/oGkFQV55Up8ZPC2oLxvgSxD8CzA0yBrhRZSh3pfv3OFj9aSGVC0yoerAi/O4u7jUVnOGVX1eVFM+0tmQ==",
"dev": true,
"requires": {
- "append-transform": "1.0.0"
+ "append-transform": "0.4.0"
}
},
"istanbul-lib-instrument": {
@@ -5402,29 +5621,12 @@
"mkdirp": "0.5.1",
"path-parse": "1.0.5",
"supports-color": "3.2.3"
- },
- "dependencies": {
- "has-flag": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
- "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
- "dev": true
- },
- "supports-color": {
- "version": "3.2.3",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
- "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=",
- "dev": true,
- "requires": {
- "has-flag": "1.0.0"
- }
- }
}
},
"istanbul-lib-source-maps": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.5.tgz",
- "integrity": "sha512-8O2T/3VhrQHn0XcJbP1/GN7kXMiRAlPi+fj3uEHrjBD8Oz7Py0prSC25C09NuAZS6bgW1NNKAvCSHZXB0irSGA==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.4.tgz",
+ "integrity": "sha512-UzuK0g1wyQijiaYQxj/CdNycFhAd2TLtO2obKQMTZrZ1jzEMRY3rvpASEKkaxbRR6brvdovfA03znPa/pXcejg==",
"dev": true,
"requires": {
"debug": "3.1.0",
@@ -5452,6 +5654,107 @@
"dev": true,
"requires": {
"handlebars": "4.0.11"
+ },
+ "dependencies": {
+ "async": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+ "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
+ "dev": true
+ },
+ "camelcase": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz",
+ "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=",
+ "dev": true,
+ "optional": true
+ },
+ "cliui": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz",
+ "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "center-align": "0.1.3",
+ "right-align": "0.1.3",
+ "wordwrap": "0.0.2"
+ },
+ "dependencies": {
+ "wordwrap": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz",
+ "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=",
+ "dev": true,
+ "optional": true
+ }
+ }
+ },
+ "handlebars": {
+ "version": "4.0.11",
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz",
+ "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=",
+ "dev": true,
+ "requires": {
+ "async": "1.5.2",
+ "optimist": "0.6.1",
+ "source-map": "0.4.4",
+ "uglify-js": "2.8.29"
+ }
+ },
+ "optimist": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
+ "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
+ "dev": true,
+ "requires": {
+ "minimist": "0.0.8",
+ "wordwrap": "0.0.3"
+ }
+ },
+ "source-map": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
+ "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
+ "dev": true,
+ "requires": {
+ "amdefine": "1.0.1"
+ }
+ },
+ "uglify-js": {
+ "version": "2.8.29",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
+ "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "source-map": "0.5.7",
+ "uglify-to-browserify": "1.0.2",
+ "yargs": "3.10.0"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+ "dev": true,
+ "optional": true
+ }
+ }
+ },
+ "yargs": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
+ "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "camelcase": "1.2.1",
+ "cliui": "2.1.0",
+ "decamelize": "1.2.0",
+ "window-size": "0.1.0"
+ }
+ }
}
},
"jasmine": {
@@ -5474,15 +5777,15 @@
}
},
"jasmine-core": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.8.0.tgz",
- "integrity": "sha1-vMl5rh+f0FcB5F5S5l06XWPxok4=",
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.5.2.tgz",
+ "integrity": "sha1-b2G9eQYeJ/Q+b5NV5Es8bKtv8pc=",
"dev": true
},
"jasmine-spec-reporter": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz",
- "integrity": "sha512-FZBoZu7VE5nR7Nilzy+Np8KuVIOxF4oXDPDknehCYBDE080EnlPu0afdZNmpGDBRCUBv3mj5qgqCRmk6W/K8vg==",
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-3.2.0.tgz",
+ "integrity": "sha1-/b6FqAzN07J2dGvHf96Dwc53Pv8=",
"dev": true,
"requires": {
"colors": "1.1.2"
@@ -5498,8 +5801,7 @@
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.5.tgz",
"integrity": "sha512-aUnNwqMOXw3yvErjMPSQu6qIIzUmT1e5KcU1OZxRDU1g/am6mzBvcrmLAYwzmB59BHPrh5/tKaiF4OPhqRWESQ==",
- "dev": true,
- "optional": true
+ "dev": true
},
"js-tokens": {
"version": "3.0.2",
@@ -5508,13 +5810,13 @@
"dev": true
},
"js-yaml": {
- "version": "3.12.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz",
- "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==",
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz",
+ "integrity": "sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A=",
"dev": true,
"requires": {
"argparse": "1.0.10",
- "esprima": "4.0.0"
+ "esprima": "2.7.3"
}
},
"jsbn": {
@@ -5525,9 +5827,9 @@
"optional": true
},
"jsesc": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz",
- "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=",
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
+ "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
"dev": true
},
"json-loader": {
@@ -5536,6 +5838,12 @@
"integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==",
"dev": true
},
+ "json-parse-better-errors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
+ "dev": true
+ },
"json-schema": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
@@ -5594,8 +5902,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz",
"integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=",
- "dev": true,
- "optional": true
+ "dev": true
},
"jsprim": {
"version": "1.4.1",
@@ -5618,18 +5925,18 @@
}
},
"karma": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/karma/-/karma-2.0.3.tgz",
- "integrity": "sha512-7bVCQs8+DCLWj5TIUBIgPa95/o8X9pBhyF+E2hX51Z6Ttq2biYWQlynBmunKZGRyNOIyg89TnVtC58q9eGBFFw==",
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/karma/-/karma-1.4.1.tgz",
+ "integrity": "sha1-QZgacdVCN2BrCj6oxYyQdz9BZQ4=",
"dev": true,
"requires": {
"bluebird": "3.5.1",
"body-parser": "1.18.2",
- "chokidar": "2.0.4",
+ "chokidar": "1.7.0",
"colors": "1.1.2",
"combine-lists": "1.0.1",
"connect": "3.6.6",
- "core-js": "2.5.7",
+ "core-js": "2.4.1",
"di": "0.0.1",
"dom-serialize": "2.2.1",
"expand-braces": "0.1.2",
@@ -5637,8 +5944,8 @@
"graceful-fs": "4.1.11",
"http-proxy": "1.17.0",
"isbinaryfile": "3.0.2",
- "lodash": "4.17.10",
- "log4js": "2.9.0",
+ "lodash": "3.10.1",
+ "log4js": "0.6.38",
"mime": "1.6.0",
"minimatch": "3.0.4",
"optimist": "0.6.1",
@@ -5646,385 +5953,193 @@
"range-parser": "1.2.0",
"rimraf": "2.6.2",
"safe-buffer": "5.1.2",
- "socket.io": "2.0.4",
- "source-map": "0.6.1",
- "tmp": "0.0.33",
- "useragent": "2.2.1"
+ "socket.io": "1.7.2",
+ "source-map": "0.5.7",
+ "tmp": "0.0.28",
+ "useragent": "2.3.0"
},
"dependencies": {
"anymatch": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
- "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz",
+ "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==",
"dev": true,
"requires": {
- "micromatch": "3.1.10",
+ "micromatch": "2.3.11",
"normalize-path": "2.1.1"
}
},
"arr-diff": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
- "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
- "dev": true
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
+ "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
+ "dev": true,
+ "requires": {
+ "arr-flatten": "1.1.0"
+ }
},
"array-unique": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
- "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
+ "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
"dev": true
},
"braces": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
- "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "version": "1.8.5",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz",
+ "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=",
"dev": true,
"requires": {
- "arr-flatten": "1.1.0",
- "array-unique": "0.3.2",
- "extend-shallow": "2.0.1",
- "fill-range": "4.0.0",
- "isobject": "3.0.1",
- "repeat-element": "1.1.2",
- "snapdragon": "0.8.2",
- "snapdragon-node": "2.1.1",
- "split-string": "3.1.0",
- "to-regex": "3.0.2"
- },
- "dependencies": {
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "0.1.1"
- }
- }
+ "expand-range": "1.8.2",
+ "preserve": "0.2.0",
+ "repeat-element": "1.1.2"
}
},
"chokidar": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz",
- "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
+ "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=",
"dev": true,
"requires": {
- "anymatch": "2.0.0",
+ "anymatch": "1.3.2",
"async-each": "1.0.1",
- "braces": "2.3.2",
"fsevents": "1.2.4",
- "glob-parent": "3.1.0",
+ "glob-parent": "2.0.0",
"inherits": "2.0.3",
"is-binary-path": "1.0.1",
- "is-glob": "4.0.0",
- "lodash.debounce": "4.0.8",
- "normalize-path": "2.1.1",
+ "is-glob": "2.0.1",
"path-is-absolute": "1.0.1",
- "readdirp": "2.1.0",
- "upath": "1.1.0"
+ "readdirp": "2.1.0"
}
},
"expand-brackets": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
- "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz",
+ "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=",
"dev": true,
"requires": {
- "debug": "2.6.9",
- "define-property": "0.2.5",
- "extend-shallow": "2.0.1",
- "posix-character-classes": "0.1.1",
- "regex-not": "1.0.2",
- "snapdragon": "0.8.2",
- "to-regex": "3.0.2"
- },
- "dependencies": {
- "define-property": {
- "version": "0.2.5",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
- "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
- "dev": true,
- "requires": {
- "is-descriptor": "0.1.6"
- }
- },
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "0.1.1"
- }
- },
- "is-accessor-descriptor": {
- "version": "0.1.6",
- "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
- "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
- "dev": true,
- "requires": {
- "kind-of": "3.2.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "1.1.6"
- }
- }
- }
- },
- "is-data-descriptor": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
- "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
- "dev": true,
- "requires": {
- "kind-of": "3.2.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "1.1.6"
- }
- }
- }
- },
- "is-descriptor": {
- "version": "0.1.6",
- "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
- "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
- "dev": true,
- "requires": {
- "is-accessor-descriptor": "0.1.6",
- "is-data-descriptor": "0.1.4",
- "kind-of": "5.1.0"
- }
- },
- "kind-of": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
- "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
- "dev": true
- }
+ "is-posix-bracket": "0.1.1"
}
},
"extglob": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
- "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
+ "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=",
"dev": true,
"requires": {
- "array-unique": "0.3.2",
- "define-property": "1.0.0",
- "expand-brackets": "2.1.4",
- "extend-shallow": "2.0.1",
- "fragment-cache": "0.2.1",
- "regex-not": "1.0.2",
- "snapdragon": "0.8.2",
- "to-regex": "3.0.2"
- },
- "dependencies": {
- "define-property": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
- "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
- "dev": true,
- "requires": {
- "is-descriptor": "1.0.2"
- }
- },
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "0.1.1"
- }
- }
- }
- },
- "fill-range": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
- "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
- "dev": true,
- "requires": {
- "extend-shallow": "2.0.1",
- "is-number": "3.0.0",
- "repeat-string": "1.6.1",
- "to-regex-range": "2.1.1"
- },
- "dependencies": {
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "0.1.1"
- }
- }
+ "is-extglob": "1.0.0"
}
},
"glob-parent": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
- "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
+ "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
"dev": true,
"requires": {
- "is-glob": "3.1.0",
- "path-dirname": "1.0.2"
- },
- "dependencies": {
- "is-glob": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
- "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
- "dev": true,
- "requires": {
- "is-extglob": "2.1.1"
- }
- }
- }
- },
- "is-accessor-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
- "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
- "dev": true,
- "requires": {
- "kind-of": "6.0.2"
- }
- },
- "is-data-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
- "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
- "dev": true,
- "requires": {
- "kind-of": "6.0.2"
- }
- },
- "is-descriptor": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
- "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
- "dev": true,
- "requires": {
- "is-accessor-descriptor": "1.0.0",
- "is-data-descriptor": "1.0.0",
- "kind-of": "6.0.2"
+ "is-glob": "2.0.1"
}
},
"is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+ "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
"dev": true
},
"is-glob": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
- "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+ "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
"dev": true,
"requires": {
- "is-extglob": "2.1.1"
+ "is-extglob": "1.0.0"
}
},
- "is-number": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
- "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
- "dev": true,
- "requires": {
- "kind-of": "3.2.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "1.1.6"
- }
- }
- }
- },
- "isobject": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
- "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
- "dev": true
- },
"kind-of": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
- "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "1.1.6"
+ }
+ },
+ "lodash": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
+ "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=",
"dev": true
},
"micromatch": {
- "version": "3.1.10",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
- "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "version": "2.3.11",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
+ "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
"dev": true,
"requires": {
- "arr-diff": "4.0.0",
- "array-unique": "0.3.2",
- "braces": "2.3.2",
- "define-property": "2.0.2",
- "extend-shallow": "3.0.2",
- "extglob": "2.0.4",
- "fragment-cache": "0.2.1",
- "kind-of": "6.0.2",
- "nanomatch": "1.2.9",
- "object.pick": "1.3.0",
- "regex-not": "1.0.2",
- "snapdragon": "0.8.2",
- "to-regex": "3.0.2"
+ "arr-diff": "2.0.0",
+ "array-unique": "0.2.1",
+ "braces": "1.8.5",
+ "expand-brackets": "0.1.5",
+ "extglob": "0.3.2",
+ "filename-regex": "2.0.1",
+ "is-extglob": "1.0.0",
+ "is-glob": "2.0.1",
+ "kind-of": "3.2.2",
+ "normalize-path": "2.1.1",
+ "object.omit": "2.0.1",
+ "parse-glob": "3.0.4",
+ "regex-cache": "0.4.4"
}
},
- "source-map": {
+ "optimist": {
"version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
+ "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
+ "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
+ "dev": true,
+ "requires": {
+ "minimist": "0.0.8",
+ "wordwrap": "0.0.3"
+ }
}
}
},
"karma-chrome-launcher": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz",
- "integrity": "sha512-uf/ZVpAabDBPvdPdveyk1EPgbnloPvFFGgmRhYLTDH7gEB4nZdSBk8yTU47w1g/drLSx5uMOkjKk7IWKfWg/+w==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-2.0.0.tgz",
+ "integrity": "sha1-wnkMWjKxVXfQ//Wk1aJwOztDnCU=",
"dev": true,
"requires": {
"fs-access": "1.0.1",
- "which": "1.3.1"
+ "which": "1.3.0"
+ }
+ },
+ "karma-cli": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/karma-cli/-/karma-cli-1.0.1.tgz",
+ "integrity": "sha1-rmw8WKMTodALRRZMRVubhs4X+WA=",
+ "dev": true,
+ "requires": {
+ "resolve": "1.7.1"
}
},
"karma-coverage-istanbul-reporter": {
- "version": "1.4.3",
- "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-1.4.3.tgz",
- "integrity": "sha1-O13/RmT6W41RlrmInj9hwforgNk=",
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-0.2.2.tgz",
+ "integrity": "sha1-0Xu4AttCh9O2qO9PENecMYU24MQ=",
"dev": true,
"requires": {
- "istanbul-api": "1.3.1",
- "minimatch": "3.0.4"
+ "istanbul-api": "1.3.1"
}
},
+ "karma-firefox-launcher": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-1.0.0.tgz",
+ "integrity": "sha1-4IrzzkLjmGDClS6nt+qmTWNQi9w=",
+ "dev": true
+ },
"karma-jasmine": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-1.1.2.tgz",
- "integrity": "sha1-OU8rJf+0pkS5rabyLUQ+L9CIhsM=",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-1.1.0.tgz",
+ "integrity": "sha1-IuTAa/mhguUpTR9wXjczgRuBCs8=",
"dev": true
},
"karma-jasmine-html-reporter": {
@@ -6033,7 +6148,7 @@
"integrity": "sha1-SKjl7xiAdhfuK14zwRlMNbQ5Ukw=",
"dev": true,
"requires": {
- "karma-jasmine": "1.1.2"
+ "karma-jasmine": "1.1.0"
}
},
"karma-source-map-support": {
@@ -6057,25 +6172,31 @@
"integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==",
"dev": true,
"requires": {
- "buffer-from": "1.1.0",
+ "buffer-from": "1.0.0",
"source-map": "0.6.1"
}
}
}
},
- "killable": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.0.tgz",
- "integrity": "sha1-2ouEvUfeU5WHj5XWTQLyRJ/gXms=",
+ "kew": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz",
+ "integrity": "sha1-edk9LTM2PW/dKXCzNdkUGtWR15s=",
"dev": true
},
"kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+ "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+ "dev": true
+ },
+ "klaw": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz",
+ "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=",
"dev": true,
"requires": {
- "is-buffer": "1.1.6"
+ "graceful-fs": "4.1.11"
}
},
"lazy-cache": {
@@ -6088,7 +6209,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
"integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
- "dev": true,
"requires": {
"invert-kv": "1.0.0"
}
@@ -6118,49 +6238,110 @@
"clone": "2.1.1",
"loader-utils": "1.1.0",
"pify": "3.0.0"
- }
- },
- "levn": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
- "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
- "dev": true,
- "optional": true,
- "requires": {
- "prelude-ls": "1.1.2",
- "type-check": "0.3.2"
- }
- },
- "libbase64": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-0.1.0.tgz",
- "integrity": "sha1-YjUag5VjrF/1vSbxL2Dpgwu3UeY=",
- "dev": true
- },
- "libmime": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/libmime/-/libmime-3.0.0.tgz",
- "integrity": "sha1-UaGp50SOy9Ms2lRCFnW7IbwJPaY=",
- "dev": true,
- "requires": {
- "iconv-lite": "0.4.15",
- "libbase64": "0.1.0",
- "libqp": "1.1.0"
},
"dependencies": {
- "iconv-lite": {
- "version": "0.4.15",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz",
- "integrity": "sha1-/iZaIYrGpXz+hUkn6dBMGYJe3es=",
+ "clone": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz",
+ "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=",
+ "dev": true
+ },
+ "pify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+ "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"dev": true
}
}
},
- "libqp": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/libqp/-/libqp-1.1.0.tgz",
- "integrity": "sha1-9ebgatdLeU+1tbZpiL9yjvHe2+g=",
- "dev": true
+ "license-checker": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-7.1.1.tgz",
+ "integrity": "sha1-sKPsR/JGn+OmOeUqEVHNJvwsKQU=",
+ "dev": true,
+ "requires": {
+ "chalk": "0.5.1",
+ "debug": "2.6.9",
+ "mkdirp": "0.3.5",
+ "nopt": "2.2.1",
+ "read-installed": "4.0.3",
+ "treeify": "1.1.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz",
+ "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz",
+ "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=",
+ "dev": true
+ },
+ "chalk": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz",
+ "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "1.1.0",
+ "escape-string-regexp": "1.0.5",
+ "has-ansi": "0.1.0",
+ "strip-ansi": "0.3.0",
+ "supports-color": "0.2.0"
+ }
+ },
+ "has-ansi": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz",
+ "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "0.2.1"
+ }
+ },
+ "mkdirp": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
+ "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=",
+ "dev": true
+ },
+ "nopt": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-2.2.1.tgz",
+ "integrity": "sha1-KqCbfRdoSHs7ianFqlIzW/8Lrqc=",
+ "dev": true,
+ "requires": {
+ "abbrev": "1.1.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz",
+ "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "0.2.1"
+ }
+ },
+ "supports-color": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz",
+ "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=",
+ "dev": true
+ }
+ }
+ },
+ "license-to-fail": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/license-to-fail/-/license-to-fail-2.2.0.tgz",
+ "integrity": "sha1-bVkp87Ht+whm4F6yKRml4d4y/zc=",
+ "dev": true,
+ "requires": {
+ "license-checker": "7.1.1"
+ }
},
"license-webpack-plugin": {
"version": "1.3.1",
@@ -6175,21 +6356,12 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
"integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
- "dev": true,
"requires": {
"graceful-fs": "4.1.11",
"parse-json": "2.2.0",
"pify": "2.3.0",
"pinkie-promise": "2.0.1",
"strip-bom": "2.0.0"
- },
- "dependencies": {
- "pify": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
- "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
- "dev": true
- }
}
},
"loader-runner": {
@@ -6217,6 +6389,14 @@
"requires": {
"p-locate": "2.0.0",
"path-exists": "3.0.0"
+ },
+ "dependencies": {
+ "path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "dev": true
+ }
}
},
"lodash": {
@@ -6232,16 +6412,22 @@
"dev": true,
"optional": true
},
+ "lodash.camelcase": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+ "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=",
+ "dev": true
+ },
"lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
"dev": true
},
- "lodash.debounce": {
- "version": "4.0.8",
- "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
- "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
+ "lodash.memoize": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
+ "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
"dev": true
},
"lodash.mergewith": {
@@ -6257,160 +6443,51 @@
"integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=",
"dev": true
},
- "log4js": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/log4js/-/log4js-2.9.0.tgz",
- "integrity": "sha512-pptn4+5Q3ysOW6Jgm9lzhDUCFEYv7FLrazEzPQQlxgSP+IVl5HMPgT8hm2DyRqGY4GUiVjZz4XXRvTZ9BELQyw==",
- "dev": true,
- "requires": {
- "amqplib": "0.5.2",
- "axios": "0.15.3",
- "circular-json": "0.5.4",
- "date-format": "1.2.0",
- "debug": "3.1.0",
- "hipchat-notifier": "1.1.0",
- "loggly": "1.1.1",
- "mailgun-js": "0.18.1",
- "nodemailer": "2.7.2",
- "redis": "2.8.0",
- "semver": "5.5.0",
- "slack-node": "0.2.0",
- "streamroller": "0.7.0"
- },
- "dependencies": {
- "debug": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
- "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
- }
- }
- }
+ "lodash.uniq": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
+ "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=",
+ "dev": true
},
- "loggly": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/loggly/-/loggly-1.1.1.tgz",
- "integrity": "sha1-Cg/B0/o6XsRP3HuJe+uipGlc6+4=",
+ "log4js": {
+ "version": "0.6.38",
+ "resolved": "https://registry.npmjs.org/log4js/-/log4js-0.6.38.tgz",
+ "integrity": "sha1-LElBFmldb7JUgJQ9P8hy5mKlIv0=",
"dev": true,
- "optional": true,
"requires": {
- "json-stringify-safe": "5.0.1",
- "request": "2.75.0",
- "timespan": "2.3.0"
+ "readable-stream": "1.0.34",
+ "semver": "4.3.6"
},
"dependencies": {
- "ansi-styles": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
- "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
- "dev": true,
- "optional": true
+ "isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+ "dev": true
},
- "caseless": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz",
- "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=",
+ "readable-stream": {
+ "version": "1.0.34",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
+ "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
"dev": true,
- "optional": true
- },
- "chalk": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
- "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
- "dev": true,
- "optional": true,
"requires": {
- "ansi-styles": "2.2.1",
- "escape-string-regexp": "1.0.5",
- "has-ansi": "2.0.0",
- "strip-ansi": "3.0.1",
- "supports-color": "2.0.0"
+ "core-util-is": "1.0.2",
+ "inherits": "2.0.3",
+ "isarray": "0.0.1",
+ "string_decoder": "0.10.31"
}
},
- "form-data": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.0.0.tgz",
- "integrity": "sha1-bwrrrcxdoWwT4ezBETfYX5uIOyU=",
- "dev": true,
- "optional": true,
- "requires": {
- "asynckit": "0.4.0",
- "combined-stream": "1.0.6",
- "mime-types": "2.1.18"
- }
+ "semver": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz",
+ "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=",
+ "dev": true
},
- "har-validator": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz",
- "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=",
- "dev": true,
- "optional": true,
- "requires": {
- "chalk": "1.1.3",
- "commander": "2.15.1",
- "is-my-json-valid": "2.17.2",
- "pinkie-promise": "2.0.1"
- }
- },
- "node-uuid": {
- "version": "1.4.8",
- "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz",
- "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=",
- "dev": true,
- "optional": true
- },
- "qs": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.3.tgz",
- "integrity": "sha1-HPyyXBCpsrSDBT/zn138kjOQjP4=",
- "dev": true,
- "optional": true
- },
- "request": {
- "version": "2.75.0",
- "resolved": "https://registry.npmjs.org/request/-/request-2.75.0.tgz",
- "integrity": "sha1-0rgmiihtoT6qXQGt9dGMyQ9lfZM=",
- "dev": true,
- "optional": true,
- "requires": {
- "aws-sign2": "0.6.0",
- "aws4": "1.7.0",
- "bl": "1.1.2",
- "caseless": "0.11.0",
- "combined-stream": "1.0.6",
- "extend": "3.0.1",
- "forever-agent": "0.6.1",
- "form-data": "2.0.0",
- "har-validator": "2.0.6",
- "hawk": "3.1.3",
- "http-signature": "1.1.1",
- "is-typedarray": "1.0.0",
- "isstream": "0.1.2",
- "json-stringify-safe": "5.0.1",
- "mime-types": "2.1.18",
- "node-uuid": "1.4.8",
- "oauth-sign": "0.8.2",
- "qs": "6.2.3",
- "stringstream": "0.0.6",
- "tough-cookie": "2.3.4",
- "tunnel-agent": "0.4.3"
- }
- },
- "supports-color": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
- "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
- "dev": true,
- "optional": true
- },
- "tunnel-agent": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz",
- "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=",
- "dev": true,
- "optional": true
+ "string_decoder": {
+ "version": "0.10.31",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+ "dev": true
}
}
},
@@ -6470,59 +6547,6 @@
"vlq": "0.2.3"
}
},
- "mailcomposer": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/mailcomposer/-/mailcomposer-4.0.1.tgz",
- "integrity": "sha1-DhxEsqB890DuF9wUm6AJ8Zyt/rQ=",
- "dev": true,
- "optional": true,
- "requires": {
- "buildmail": "4.0.1",
- "libmime": "3.0.0"
- }
- },
- "mailgun-js": {
- "version": "0.18.1",
- "resolved": "https://registry.npmjs.org/mailgun-js/-/mailgun-js-0.18.1.tgz",
- "integrity": "sha512-lvuMP14u24HS2uBsJEnzSyPMxzU2b99tQsIx1o6QNjqxjk8b3WvR+vq5oG1mjqz/IBYo+5gF+uSoDS0RkMVHmg==",
- "dev": true,
- "optional": true,
- "requires": {
- "async": "2.6.1",
- "debug": "3.1.0",
- "form-data": "2.3.2",
- "inflection": "1.12.0",
- "is-stream": "1.1.0",
- "path-proxy": "1.0.0",
- "promisify-call": "2.0.4",
- "proxy-agent": "3.0.0",
- "tsscmp": "1.0.5"
- },
- "dependencies": {
- "debug": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
- "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
- "dev": true,
- "optional": true,
- "requires": {
- "ms": "2.0.0"
- }
- },
- "form-data": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz",
- "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=",
- "dev": true,
- "optional": true,
- "requires": {
- "asynckit": "0.4.0",
- "combined-stream": "1.0.6",
- "mime-types": "2.1.18"
- }
- }
- }
- },
"make-dir": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
@@ -6530,6 +6554,14 @@
"dev": true,
"requires": {
"pify": "3.0.0"
+ },
+ "dependencies": {
+ "pify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+ "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+ "dev": true
+ }
}
},
"make-error": {
@@ -6559,10 +6591,11 @@
"object-visit": "1.0.1"
}
},
- "material-design-icons": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/material-design-icons/-/material-design-icons-3.0.1.tgz",
- "integrity": "sha1-mnHEh0chjrylHlGmbaaCA4zct78="
+ "math-expression-evaluator": {
+ "version": "1.2.17",
+ "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz",
+ "integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw=",
+ "dev": true
},
"math-random": {
"version": "1.0.1",
@@ -6570,6 +6603,17 @@
"integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=",
"dev": true
},
+ "md5": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
+ "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=",
+ "dev": true,
+ "requires": {
+ "charenc": "0.0.2",
+ "crypt": "0.0.2",
+ "is-buffer": "1.1.6"
+ }
+ },
"md5.js": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz",
@@ -6644,24 +6688,24 @@
"dev": true
},
"micromatch": {
- "version": "2.3.11",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
- "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
"dev": true,
"requires": {
- "arr-diff": "2.0.0",
- "array-unique": "0.2.1",
- "braces": "1.8.5",
- "expand-brackets": "0.1.5",
- "extglob": "0.3.2",
- "filename-regex": "2.0.1",
- "is-extglob": "1.0.0",
- "is-glob": "2.0.1",
- "kind-of": "3.2.2",
- "normalize-path": "2.1.1",
- "object.omit": "2.0.1",
- "parse-glob": "3.0.4",
- "regex-cache": "0.4.4"
+ "arr-diff": "4.0.0",
+ "array-unique": "0.3.2",
+ "braces": "2.3.2",
+ "define-property": "2.0.2",
+ "extend-shallow": "3.0.2",
+ "extglob": "2.0.4",
+ "fragment-cache": "0.2.1",
+ "kind-of": "6.0.2",
+ "nanomatch": "1.2.9",
+ "object.pick": "1.3.0",
+ "regex-not": "1.0.2",
+ "snapdragon": "0.8.2",
+ "to-regex": "3.0.2"
}
},
"miller-rabin": {
@@ -6855,26 +6899,6 @@
"regex-not": "1.0.2",
"snapdragon": "0.8.2",
"to-regex": "3.0.2"
- },
- "dependencies": {
- "arr-diff": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
- "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
- "dev": true
- },
- "array-unique": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
- "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
- "dev": true
- },
- "kind-of": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
- "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
- "dev": true
- }
}
},
"negotiator": {
@@ -6889,13 +6913,6 @@
"integrity": "sha512-3KL3fvuRkZ7s4IFOMfztb7zJp3QaVWnBeGoJlgB38XnCRPj/0tLzzLG5IB8NYOHbJ8g8UGrgZv44GLDk6CxTxA==",
"dev": true
},
- "netmask": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz",
- "integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=",
- "dev": true,
- "optional": true
- },
"next-tick": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
@@ -6903,9 +6920,9 @@
"dev": true
},
"ngrx-store-localstorage": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/ngrx-store-localstorage/-/ngrx-store-localstorage-5.0.0.tgz",
- "integrity": "sha1-rcW4Nz/umV9rssUIoOUQ/CBHp04="
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/ngrx-store-localstorage/-/ngrx-store-localstorage-0.1.8.tgz",
+ "integrity": "sha1-IaROxmHEpH+RN8tcRRqSi6olA8o="
},
"no-case": {
"version": "2.3.2",
@@ -6923,15 +6940,16 @@
"dev": true
},
"node-gyp": {
- "version": "3.7.0",
- "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.7.0.tgz",
- "integrity": "sha512-qDQE/Ft9xXP6zphwx4sD0t+VhwV7yFaloMpfbL2QnnDZcyaiakWlLdtFGGQfTAwpFHdpbRhRxVhIHN1OKAjgbg==",
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.6.2.tgz",
+ "integrity": "sha1-m/vlRWIoYoSDjnUOrAUpWFP6HGA=",
"dev": true,
"optional": true,
"requires": {
"fstream": "1.0.11",
"glob": "7.1.2",
"graceful-fs": "4.1.11",
+ "minimatch": "3.0.4",
"mkdirp": "0.5.1",
"nopt": "3.0.6",
"npmlog": "4.1.2",
@@ -6940,7 +6958,7 @@
"rimraf": "2.6.2",
"semver": "5.3.0",
"tar": "2.2.1",
- "which": "1.3.1"
+ "which": "1.3.0"
},
"dependencies": {
"nopt": {
@@ -6984,21 +7002,13 @@
"querystring-es3": "0.2.1",
"readable-stream": "2.3.6",
"stream-browserify": "2.0.1",
- "stream-http": "2.8.3",
+ "stream-http": "2.8.2",
"string_decoder": "1.1.1",
"timers-browserify": "2.0.10",
"tty-browserify": "0.0.0",
"url": "0.11.0",
- "util": "0.10.4",
+ "util": "0.10.3",
"vm-browserify": "0.0.4"
- },
- "dependencies": {
- "punycode": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
- "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
- "dev": true
- }
}
},
"node-modules-path": {
@@ -7027,7 +7037,7 @@
"meow": "3.7.0",
"mkdirp": "0.5.1",
"nan": "2.10.0",
- "node-gyp": "3.7.0",
+ "node-gyp": "3.6.2",
"npmlog": "4.1.2",
"request": "2.79.0",
"sass-graph": "2.2.4",
@@ -7035,12 +7045,6 @@
"true-case-path": "1.0.2"
},
"dependencies": {
- "ansi-styles": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
- "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
- "dev": true
- },
"caseless": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz",
@@ -7125,91 +7129,6 @@
}
}
},
- "nodemailer": {
- "version": "2.7.2",
- "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-2.7.2.tgz",
- "integrity": "sha1-8kLmSa7q45tsftdA73sGHEBNMPk=",
- "dev": true,
- "optional": true,
- "requires": {
- "libmime": "3.0.0",
- "mailcomposer": "4.0.1",
- "nodemailer-direct-transport": "3.3.2",
- "nodemailer-shared": "1.1.0",
- "nodemailer-smtp-pool": "2.8.2",
- "nodemailer-smtp-transport": "2.7.2",
- "socks": "1.1.9"
- },
- "dependencies": {
- "socks": {
- "version": "1.1.9",
- "resolved": "https://registry.npmjs.org/socks/-/socks-1.1.9.tgz",
- "integrity": "sha1-Yo1+TQSRJDVEWsC25Fk3bLPm1pE=",
- "dev": true,
- "optional": true,
- "requires": {
- "ip": "1.1.5",
- "smart-buffer": "1.1.15"
- }
- }
- }
- },
- "nodemailer-direct-transport": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/nodemailer-direct-transport/-/nodemailer-direct-transport-3.3.2.tgz",
- "integrity": "sha1-6W+vuQNYVglH5WkBfZfmBzilCoY=",
- "dev": true,
- "optional": true,
- "requires": {
- "nodemailer-shared": "1.1.0",
- "smtp-connection": "2.12.0"
- }
- },
- "nodemailer-fetch": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/nodemailer-fetch/-/nodemailer-fetch-1.6.0.tgz",
- "integrity": "sha1-ecSQihwPXzdbc/6IjamCj23JY6Q=",
- "dev": true
- },
- "nodemailer-shared": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/nodemailer-shared/-/nodemailer-shared-1.1.0.tgz",
- "integrity": "sha1-z1mU4v0mjQD1zw+nZ6CBae2wfsA=",
- "dev": true,
- "requires": {
- "nodemailer-fetch": "1.6.0"
- }
- },
- "nodemailer-smtp-pool": {
- "version": "2.8.2",
- "resolved": "https://registry.npmjs.org/nodemailer-smtp-pool/-/nodemailer-smtp-pool-2.8.2.tgz",
- "integrity": "sha1-LrlNbPhXgLG0clzoU7nL1ejajHI=",
- "dev": true,
- "optional": true,
- "requires": {
- "nodemailer-shared": "1.1.0",
- "nodemailer-wellknown": "0.1.10",
- "smtp-connection": "2.12.0"
- }
- },
- "nodemailer-smtp-transport": {
- "version": "2.7.2",
- "resolved": "https://registry.npmjs.org/nodemailer-smtp-transport/-/nodemailer-smtp-transport-2.7.2.tgz",
- "integrity": "sha1-A9ccdjFPFKx9vHvwM6am0W1n+3c=",
- "dev": true,
- "optional": true,
- "requires": {
- "nodemailer-shared": "1.1.0",
- "nodemailer-wellknown": "0.1.10",
- "smtp-connection": "2.12.0"
- }
- },
- "nodemailer-wellknown": {
- "version": "0.1.10",
- "resolved": "https://registry.npmjs.org/nodemailer-wellknown/-/nodemailer-wellknown-0.1.10.tgz",
- "integrity": "sha1-WG24EB2zDLRDjrVGc3pBqtDPE9U=",
- "dev": true
- },
"nopt": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
@@ -7224,7 +7143,6 @@
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
"integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==",
- "dev": true,
"requires": {
"hosted-git-info": "2.6.0",
"is-builtin-module": "1.0.0",
@@ -7247,6 +7165,18 @@
"integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=",
"dev": true
},
+ "normalize-url": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz",
+ "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=",
+ "dev": true,
+ "requires": {
+ "object-assign": "4.1.1",
+ "prepend-http": "1.0.4",
+ "query-string": "4.3.4",
+ "sort-keys": "1.1.2"
+ }
+ },
"npm-run-path": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
@@ -7262,7 +7192,7 @@
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"dev": true,
"requires": {
- "are-we-there-yet": "1.1.5",
+ "are-we-there-yet": "1.1.4",
"console-control-strings": "1.1.0",
"gauge": "2.7.4",
"set-blocking": "2.0.0"
@@ -7292,8 +7222,7 @@
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
- "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
- "dev": true
+ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
},
"oauth-sign": {
"version": "0.8.2",
@@ -7332,15 +7261,18 @@
"requires": {
"is-descriptor": "0.1.6"
}
+ },
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "1.1.6"
+ }
}
}
},
- "object-keys": {
- "version": "1.0.12",
- "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz",
- "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==",
- "dev": true
- },
"object-visit": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
@@ -7348,14 +7280,6 @@
"dev": true,
"requires": {
"isobject": "3.0.1"
- },
- "dependencies": {
- "isobject": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
- "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
- "dev": true
- }
}
},
"object.omit": {
@@ -7366,6 +7290,17 @@
"requires": {
"for-own": "0.1.5",
"is-extendable": "0.1.1"
+ },
+ "dependencies": {
+ "for-own": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz",
+ "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=",
+ "dev": true,
+ "requires": {
+ "for-in": "1.0.2"
+ }
+ }
}
},
"object.pick": {
@@ -7375,14 +7310,6 @@
"dev": true,
"requires": {
"isobject": "3.0.1"
- },
- "dependencies": {
- "isobject": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
- "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
- "dev": true
- }
}
},
"obuf": {
@@ -7415,6 +7342,12 @@
"wrappy": "1.0.2"
}
},
+ "opener": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/opener/-/opener-1.4.3.tgz",
+ "integrity": "sha1-XG2ixdflgx6P+jlklQ+NZnSskLg=",
+ "dev": true
+ },
"opn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/opn/-/opn-5.1.0.tgz",
@@ -7425,37 +7358,12 @@
}
},
"optimist": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
- "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
+ "version": "0.3.7",
+ "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz",
+ "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=",
"dev": true,
"requires": {
- "minimist": "0.0.8",
- "wordwrap": "0.0.2"
- }
- },
- "optionator": {
- "version": "0.8.2",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
- "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=",
- "dev": true,
- "optional": true,
- "requires": {
- "deep-is": "0.1.3",
- "fast-levenshtein": "2.0.6",
- "levn": "0.3.0",
- "prelude-ls": "1.1.2",
- "type-check": "0.3.2",
- "wordwrap": "1.0.0"
- },
- "dependencies": {
- "wordwrap": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
- "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
- "dev": true,
- "optional": true
- }
+ "wordwrap": "0.0.3"
}
},
"options": {
@@ -7470,7 +7378,7 @@
"integrity": "sha512-IEvtB5vM5ULvwnqMxWBLxkS13JIEXbakizMSo3yoPNPCIWzg8TG3Usn/UhXoZFM/m+FuEA20KdzPSFq/0rS+UA==",
"dev": true,
"requires": {
- "url-parse": "1.4.1"
+ "url-parse": "1.4.0"
}
},
"os-browserify": {
@@ -7489,7 +7397,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
"integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
- "dev": true,
"requires": {
"lcid": "1.0.0"
}
@@ -7517,9 +7424,9 @@
"dev": true
},
"p-limit": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
- "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz",
+ "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==",
"dev": true,
"requires": {
"p-try": "1.0.0"
@@ -7531,7 +7438,7 @@
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
"dev": true,
"requires": {
- "p-limit": "1.3.0"
+ "p-limit": "1.2.0"
}
},
"p-map": {
@@ -7546,49 +7453,6 @@
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
"dev": true
},
- "pac-proxy-agent": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-2.0.2.tgz",
- "integrity": "sha512-cDNAN1Ehjbf5EHkNY5qnRhGPUCp6SnpyVof5fRzN800QV1Y2OkzbH9rmjZkbBRa8igof903yOnjIl6z0SlAhxA==",
- "dev": true,
- "optional": true,
- "requires": {
- "agent-base": "4.2.0",
- "debug": "3.1.0",
- "get-uri": "2.0.2",
- "http-proxy-agent": "2.1.0",
- "https-proxy-agent": "2.2.1",
- "pac-resolver": "3.0.0",
- "raw-body": "2.3.2",
- "socks-proxy-agent": "3.0.1"
- },
- "dependencies": {
- "debug": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
- "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
- "dev": true,
- "optional": true,
- "requires": {
- "ms": "2.0.0"
- }
- }
- }
- },
- "pac-resolver": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-3.0.0.tgz",
- "integrity": "sha512-tcc38bsjuE3XZ5+4vP96OfhOugrX+JcnpUbhfuc4LuXBLQhoTthOstZeoQJBDnQUDYzYmdImKsbz0xSl1/9qeA==",
- "dev": true,
- "optional": true,
- "requires": {
- "co": "4.6.0",
- "degenerator": "1.0.4",
- "ip": "1.1.5",
- "netmask": "1.0.6",
- "thunkify": "2.1.2"
- }
- },
"pako": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz",
@@ -7638,22 +7502,56 @@
"is-dotfile": "1.0.3",
"is-extglob": "1.0.0",
"is-glob": "2.0.1"
+ },
+ "dependencies": {
+ "is-extglob": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+ "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+ "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+ "dev": true,
+ "requires": {
+ "is-extglob": "1.0.0"
+ }
+ }
}
},
"parse-json": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
"integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
- "dev": true,
"requires": {
- "error-ex": "1.3.2"
+ "error-ex": "1.3.1"
}
},
- "parse-passwd": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
- "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
- "dev": true
+ "parse5": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz",
+ "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==",
+ "requires": {
+ "@types/node": "10.1.2"
+ },
+ "dependencies": {
+ "@types/node": {
+ "version": "10.1.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.1.2.tgz",
+ "integrity": "sha512-bjk1RIeZBCe/WukrFToIVegOf91Pebr8cXYBwLBIsfiGWVQ+ifwWsT59H3RxrWzWrzd1l/Amk1/ioY5Fq3/bpA=="
+ }
+ }
+ },
+ "parsejson": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.3.tgz",
+ "integrity": "sha1-q343WfIJ7OmUN5c/fQ8fZK4OZKs=",
+ "dev": true,
+ "requires": {
+ "better-assert": "1.0.2"
+ }
},
"parseqs": {
"version": "0.0.5",
@@ -7698,10 +7596,12 @@
"dev": true
},
"path-exists": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
- "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
- "dev": true
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
+ "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
+ "requires": {
+ "pinkie-promise": "2.0.1"
+ }
},
"path-is-absolute": {
"version": "1.0.1",
@@ -7727,25 +7627,6 @@
"integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=",
"dev": true
},
- "path-proxy": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/path-proxy/-/path-proxy-1.0.0.tgz",
- "integrity": "sha1-GOijaFn8nS8aU7SN7hOFQ8Ag3l4=",
- "dev": true,
- "optional": true,
- "requires": {
- "inflection": "1.3.8"
- },
- "dependencies": {
- "inflection": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.3.8.tgz",
- "integrity": "sha1-y9Fg2p91sUw8xjV41POWeEvzAU4=",
- "dev": true,
- "optional": true
- }
- }
- },
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
@@ -7753,12 +7634,13 @@
"dev": true
},
"path-type": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
- "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
- "dev": true,
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
+ "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
"requires": {
- "pify": "3.0.0"
+ "graceful-fs": "4.1.11",
+ "pify": "2.3.0",
+ "pinkie-promise": "2.0.1"
}
},
"pbkdf2": {
@@ -7774,29 +7656,181 @@
"sha.js": "2.4.11"
}
},
+ "pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=",
+ "dev": true
+ },
"performance-now": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz",
"integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=",
"dev": true
},
+ "phantomjs-prebuilt": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.7.tgz",
+ "integrity": "sha1-yQvxuXcvoZeZQzH88/ZwmaloU8o=",
+ "dev": true,
+ "requires": {
+ "extract-zip": "1.5.0",
+ "fs-extra": "0.26.7",
+ "hasha": "2.2.0",
+ "kew": "0.7.0",
+ "progress": "1.1.8",
+ "request": "2.67.0",
+ "request-progress": "2.0.1",
+ "which": "1.2.14"
+ },
+ "dependencies": {
+ "caseless": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz",
+ "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=",
+ "dev": true
+ },
+ "chalk": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "2.2.1",
+ "escape-string-regexp": "1.0.5",
+ "has-ansi": "2.0.0",
+ "strip-ansi": "3.0.1",
+ "supports-color": "2.0.0"
+ }
+ },
+ "form-data": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.1.tgz",
+ "integrity": "sha1-rjFduaSQf6BlUCMEpm13M0de43w=",
+ "dev": true,
+ "requires": {
+ "async": "2.6.1",
+ "combined-stream": "1.0.6",
+ "mime-types": "2.1.18"
+ }
+ },
+ "fs-extra": {
+ "version": "0.26.7",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.26.7.tgz",
+ "integrity": "sha1-muH92UiXeY7at20JGM9C0MMYT6k=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "4.1.11",
+ "jsonfile": "2.4.0",
+ "klaw": "1.3.1",
+ "path-is-absolute": "1.0.1",
+ "rimraf": "2.6.2"
+ }
+ },
+ "har-validator": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz",
+ "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=",
+ "dev": true,
+ "requires": {
+ "chalk": "1.1.3",
+ "commander": "2.15.1",
+ "is-my-json-valid": "2.17.2",
+ "pinkie-promise": "2.0.1"
+ }
+ },
+ "jsonfile": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz",
+ "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "4.1.11"
+ }
+ },
+ "node-uuid": {
+ "version": "1.4.8",
+ "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz",
+ "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=",
+ "dev": true
+ },
+ "qs": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-5.2.1.tgz",
+ "integrity": "sha1-gB/uAw4LlFDWOFrcSKTMVbRK7fw=",
+ "dev": true
+ },
+ "request": {
+ "version": "2.67.0",
+ "resolved": "https://registry.npmjs.org/request/-/request-2.67.0.tgz",
+ "integrity": "sha1-ivdHgOK/EeoK6aqWXBHxGv0nJ0I=",
+ "dev": true,
+ "requires": {
+ "aws-sign2": "0.6.0",
+ "bl": "1.0.3",
+ "caseless": "0.11.0",
+ "combined-stream": "1.0.6",
+ "extend": "3.0.1",
+ "forever-agent": "0.6.1",
+ "form-data": "1.0.1",
+ "har-validator": "2.0.6",
+ "hawk": "3.1.3",
+ "http-signature": "1.1.1",
+ "is-typedarray": "1.0.0",
+ "isstream": "0.1.2",
+ "json-stringify-safe": "5.0.1",
+ "mime-types": "2.1.18",
+ "node-uuid": "1.4.8",
+ "oauth-sign": "0.8.2",
+ "qs": "5.2.1",
+ "stringstream": "0.0.6",
+ "tough-cookie": "2.2.2",
+ "tunnel-agent": "0.4.3"
+ }
+ },
+ "supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+ "dev": true
+ },
+ "tough-cookie": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.2.tgz",
+ "integrity": "sha1-yDoYMPTl7wuT7yo0iOck+N4Basc=",
+ "dev": true
+ },
+ "tunnel-agent": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz",
+ "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=",
+ "dev": true
+ },
+ "which": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz",
+ "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=",
+ "dev": true,
+ "requires": {
+ "isexe": "2.0.0"
+ }
+ }
+ }
+ },
"pify": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
- "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
- "dev": true
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
},
"pinkie": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
- "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
- "dev": true
+ "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA="
},
"pinkie-promise": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
"integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
- "dev": true,
"requires": {
"pinkie": "2.0.4"
}
@@ -7808,6 +7842,17 @@
"dev": true,
"requires": {
"find-up": "2.1.0"
+ },
+ "dependencies": {
+ "find-up": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
+ "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
+ "dev": true,
+ "requires": {
+ "locate-path": "2.0.0"
+ }
+ }
}
},
"portfinder": {
@@ -7836,60 +7881,125 @@
"dev": true
},
"postcss": {
- "version": "6.0.22",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.22.tgz",
- "integrity": "sha512-Toc9lLoUASwGqxBSJGTVcOQiDqjK+Z2XlWBg+IgYwQMY9vA2f7iMpXVc1GpPcfTSyM5lkxNo0oDwDRO+wm7XHA==",
+ "version": "5.2.18",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz",
+ "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==",
"dev": true,
"requires": {
- "chalk": "2.4.1",
- "source-map": "0.6.1",
- "supports-color": "5.4.0"
+ "chalk": "1.1.3",
+ "js-base64": "2.4.5",
+ "source-map": "0.5.7",
+ "supports-color": "3.2.3"
},
"dependencies": {
"chalk": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
- "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true,
"requires": {
- "ansi-styles": "3.2.1",
+ "ansi-styles": "2.2.1",
"escape-string-regexp": "1.0.5",
- "supports-color": "5.4.0"
- }
- },
- "has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
- "dev": true
- },
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
- },
- "supports-color": {
- "version": "5.4.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
- "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
- "dev": true,
- "requires": {
- "has-flag": "3.0.0"
+ "has-ansi": "2.0.0",
+ "strip-ansi": "3.0.1",
+ "supports-color": "2.0.0"
+ },
+ "dependencies": {
+ "supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+ "dev": true
+ }
}
}
}
},
- "postcss-import": {
- "version": "11.1.0",
- "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-11.1.0.tgz",
- "integrity": "sha512-5l327iI75POonjxkXgdRCUS+AlzAdBx4pOvMEhTKTCjb1p8IEeVR9yx3cPbmN7LIWJLbfnIXxAhoB4jpD0c/Cw==",
+ "postcss-calc": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-5.3.1.tgz",
+ "integrity": "sha1-d7rnypKK2FcW4v2kLyYb98HWW14=",
"dev": true,
"requires": {
- "postcss": "6.0.22",
- "postcss-value-parser": "3.3.0",
- "read-cache": "1.0.0",
- "resolve": "1.8.1"
+ "postcss": "5.2.18",
+ "postcss-message-helpers": "2.0.0",
+ "reduce-css-calc": "1.3.0"
+ }
+ },
+ "postcss-colormin": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-2.2.2.tgz",
+ "integrity": "sha1-ZjFBfV8OkJo9fsJrJMio0eT5bks=",
+ "dev": true,
+ "requires": {
+ "colormin": "1.1.2",
+ "postcss": "5.2.18",
+ "postcss-value-parser": "3.3.0"
+ }
+ },
+ "postcss-convert-values": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz",
+ "integrity": "sha1-u9hZPFwf0uPRwyK7kl3K6Nrk1i0=",
+ "dev": true,
+ "requires": {
+ "postcss": "5.2.18",
+ "postcss-value-parser": "3.3.0"
+ }
+ },
+ "postcss-discard-comments": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz",
+ "integrity": "sha1-vv6J+v1bPazlzM5Rt2uBUUvgDj0=",
+ "dev": true,
+ "requires": {
+ "postcss": "5.2.18"
+ }
+ },
+ "postcss-discard-duplicates": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz",
+ "integrity": "sha1-uavye4isGIFYpesSq8riAmO5GTI=",
+ "dev": true,
+ "requires": {
+ "postcss": "5.2.18"
+ }
+ },
+ "postcss-discard-empty": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz",
+ "integrity": "sha1-0rS9nVztXr2Nyt52QMfXzX9PkrU=",
+ "dev": true,
+ "requires": {
+ "postcss": "5.2.18"
+ }
+ },
+ "postcss-discard-overridden": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz",
+ "integrity": "sha1-ix6vVU9ob7KIzYdMVWZ7CqNmjVg=",
+ "dev": true,
+ "requires": {
+ "postcss": "5.2.18"
+ }
+ },
+ "postcss-discard-unused": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz",
+ "integrity": "sha1-vOMLLMWR/8Y0Mitfs0ZLbZNPRDM=",
+ "dev": true,
+ "requires": {
+ "postcss": "5.2.18",
+ "uniqs": "2.0.0"
+ }
+ },
+ "postcss-filter-plugins": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/postcss-filter-plugins/-/postcss-filter-plugins-2.0.3.tgz",
+ "integrity": "sha512-T53GVFsdinJhgwm7rg1BzbeBRomOg9y5MBVhGcsV0CxurUdVj1UlPdKtn7aqYA/c/QVkzKMjq2bSV5dKG5+AwQ==",
+ "dev": true,
+ "requires": {
+ "postcss": "5.2.18"
}
},
"postcss-load-config": {
@@ -7925,28 +8035,384 @@
}
},
"postcss-loader": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-2.1.5.tgz",
- "integrity": "sha512-pV7kB5neJ0/1tZ8L1uGOBNTVBCSCXQoIsZMsrwvO8V2rKGa2tBl/f80GGVxow2jJnRJ2w1ocx693EKhZAb9Isg==",
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-1.3.3.tgz",
+ "integrity": "sha1-piHqH6KQYqg5cqRvVEhncTAZFus=",
"dev": true,
"requires": {
"loader-utils": "1.1.0",
- "postcss": "6.0.22",
- "postcss-load-config": "1.2.0",
- "schema-utils": "0.4.5"
+ "object-assign": "4.1.1",
+ "postcss": "5.2.18",
+ "postcss-load-config": "1.2.0"
+ }
+ },
+ "postcss-merge-idents": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz",
+ "integrity": "sha1-TFUwMTwI4dWzu/PSu8dH4njuonA=",
+ "dev": true,
+ "requires": {
+ "has": "1.0.1",
+ "postcss": "5.2.18",
+ "postcss-value-parser": "3.3.0"
+ }
+ },
+ "postcss-merge-longhand": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz",
+ "integrity": "sha1-I9kM0Sewp3mUkVMyc5A0oaTz1lg=",
+ "dev": true,
+ "requires": {
+ "postcss": "5.2.18"
+ }
+ },
+ "postcss-merge-rules": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz",
+ "integrity": "sha1-0d9d+qexrMO+VT8OnhDofGG19yE=",
+ "dev": true,
+ "requires": {
+ "browserslist": "1.7.7",
+ "caniuse-api": "1.6.1",
+ "postcss": "5.2.18",
+ "postcss-selector-parser": "2.2.3",
+ "vendors": "1.0.2"
+ }
+ },
+ "postcss-message-helpers": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz",
+ "integrity": "sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4=",
+ "dev": true
+ },
+ "postcss-minify-font-values": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz",
+ "integrity": "sha1-S1jttWZB66fIR0qzUmyv17vey2k=",
+ "dev": true,
+ "requires": {
+ "object-assign": "4.1.1",
+ "postcss": "5.2.18",
+ "postcss-value-parser": "3.3.0"
+ }
+ },
+ "postcss-minify-gradients": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz",
+ "integrity": "sha1-Xb2hE3NwP4PPtKPqOIHY11/15uE=",
+ "dev": true,
+ "requires": {
+ "postcss": "5.2.18",
+ "postcss-value-parser": "3.3.0"
+ }
+ },
+ "postcss-minify-params": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz",
+ "integrity": "sha1-rSzgcTc7lDs9kwo/pZo1jCjW8fM=",
+ "dev": true,
+ "requires": {
+ "alphanum-sort": "1.0.2",
+ "postcss": "5.2.18",
+ "postcss-value-parser": "3.3.0",
+ "uniqs": "2.0.0"
+ }
+ },
+ "postcss-minify-selectors": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz",
+ "integrity": "sha1-ssapjAByz5G5MtGkllCBFDEXNb8=",
+ "dev": true,
+ "requires": {
+ "alphanum-sort": "1.0.2",
+ "has": "1.0.1",
+ "postcss": "5.2.18",
+ "postcss-selector-parser": "2.2.3"
+ }
+ },
+ "postcss-modules-extract-imports": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.0.tgz",
+ "integrity": "sha1-ZhQOzs447wa/DT41XWm/WdFB6oU=",
+ "dev": true,
+ "requires": {
+ "postcss": "6.0.22"
+ },
+ "dependencies": {
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "postcss": {
+ "version": "6.0.22",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.22.tgz",
+ "integrity": "sha512-Toc9lLoUASwGqxBSJGTVcOQiDqjK+Z2XlWBg+IgYwQMY9vA2f7iMpXVc1GpPcfTSyM5lkxNo0oDwDRO+wm7XHA==",
+ "dev": true,
+ "requires": {
+ "chalk": "2.4.1",
+ "source-map": "0.6.1",
+ "supports-color": "5.4.0"
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
+ "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
+ "dev": true,
+ "requires": {
+ "has-flag": "3.0.0"
+ }
+ }
+ }
+ },
+ "postcss-modules-local-by-default": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz",
+ "integrity": "sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=",
+ "dev": true,
+ "requires": {
+ "css-selector-tokenizer": "0.7.0",
+ "postcss": "6.0.22"
+ },
+ "dependencies": {
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "postcss": {
+ "version": "6.0.22",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.22.tgz",
+ "integrity": "sha512-Toc9lLoUASwGqxBSJGTVcOQiDqjK+Z2XlWBg+IgYwQMY9vA2f7iMpXVc1GpPcfTSyM5lkxNo0oDwDRO+wm7XHA==",
+ "dev": true,
+ "requires": {
+ "chalk": "2.4.1",
+ "source-map": "0.6.1",
+ "supports-color": "5.4.0"
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
+ "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
+ "dev": true,
+ "requires": {
+ "has-flag": "3.0.0"
+ }
+ }
+ }
+ },
+ "postcss-modules-scope": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz",
+ "integrity": "sha1-1upkmUx5+XtipytCb75gVqGUu5A=",
+ "dev": true,
+ "requires": {
+ "css-selector-tokenizer": "0.7.0",
+ "postcss": "6.0.22"
+ },
+ "dependencies": {
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "postcss": {
+ "version": "6.0.22",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.22.tgz",
+ "integrity": "sha512-Toc9lLoUASwGqxBSJGTVcOQiDqjK+Z2XlWBg+IgYwQMY9vA2f7iMpXVc1GpPcfTSyM5lkxNo0oDwDRO+wm7XHA==",
+ "dev": true,
+ "requires": {
+ "chalk": "2.4.1",
+ "source-map": "0.6.1",
+ "supports-color": "5.4.0"
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
+ "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
+ "dev": true,
+ "requires": {
+ "has-flag": "3.0.0"
+ }
+ }
+ }
+ },
+ "postcss-modules-values": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz",
+ "integrity": "sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=",
+ "dev": true,
+ "requires": {
+ "icss-replace-symbols": "1.1.0",
+ "postcss": "6.0.22"
+ },
+ "dependencies": {
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "postcss": {
+ "version": "6.0.22",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.22.tgz",
+ "integrity": "sha512-Toc9lLoUASwGqxBSJGTVcOQiDqjK+Z2XlWBg+IgYwQMY9vA2f7iMpXVc1GpPcfTSyM5lkxNo0oDwDRO+wm7XHA==",
+ "dev": true,
+ "requires": {
+ "chalk": "2.4.1",
+ "source-map": "0.6.1",
+ "supports-color": "5.4.0"
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
+ "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
+ "dev": true,
+ "requires": {
+ "has-flag": "3.0.0"
+ }
+ }
+ }
+ },
+ "postcss-normalize-charset": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz",
+ "integrity": "sha1-757nEhLX/nWceO0WL2HtYrXLk/E=",
+ "dev": true,
+ "requires": {
+ "postcss": "5.2.18"
+ }
+ },
+ "postcss-normalize-url": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz",
+ "integrity": "sha1-EI90s/L82viRov+j6kWSJ5/HgiI=",
+ "dev": true,
+ "requires": {
+ "is-absolute-url": "2.1.0",
+ "normalize-url": "1.9.1",
+ "postcss": "5.2.18",
+ "postcss-value-parser": "3.3.0"
+ }
+ },
+ "postcss-ordered-values": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz",
+ "integrity": "sha1-7sbCpntsQSqNsgQud/6NpD+VwR0=",
+ "dev": true,
+ "requires": {
+ "postcss": "5.2.18",
+ "postcss-value-parser": "3.3.0"
+ }
+ },
+ "postcss-reduce-idents": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz",
+ "integrity": "sha1-wsbSDMlYKE9qv75j92Cb9AkFmtM=",
+ "dev": true,
+ "requires": {
+ "postcss": "5.2.18",
+ "postcss-value-parser": "3.3.0"
+ }
+ },
+ "postcss-reduce-initial": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz",
+ "integrity": "sha1-aPgGlfBF0IJjqHmtJA343WT2ROo=",
+ "dev": true,
+ "requires": {
+ "postcss": "5.2.18"
+ }
+ },
+ "postcss-reduce-transforms": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz",
+ "integrity": "sha1-/3b02CEkN7McKYpC0uFEQCV3GuE=",
+ "dev": true,
+ "requires": {
+ "has": "1.0.1",
+ "postcss": "5.2.18",
+ "postcss-value-parser": "3.3.0"
+ }
+ },
+ "postcss-selector-parser": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz",
+ "integrity": "sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A=",
+ "dev": true,
+ "requires": {
+ "flatten": "1.0.2",
+ "indexes-of": "1.0.1",
+ "uniq": "1.0.1"
+ }
+ },
+ "postcss-svgo": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-2.1.6.tgz",
+ "integrity": "sha1-tt8YqmE7Zm4TPwittSGcJoSsEI0=",
+ "dev": true,
+ "requires": {
+ "is-svg": "2.1.0",
+ "postcss": "5.2.18",
+ "postcss-value-parser": "3.3.0",
+ "svgo": "0.7.2"
+ }
+ },
+ "postcss-unique-selectors": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz",
+ "integrity": "sha1-mB1X0p3csz57Hf4f1DuGSfkzyh0=",
+ "dev": true,
+ "requires": {
+ "alphanum-sort": "1.0.2",
+ "postcss": "5.2.18",
+ "uniqs": "2.0.0"
}
},
"postcss-url": {
- "version": "7.3.2",
- "resolved": "https://registry.npmjs.org/postcss-url/-/postcss-url-7.3.2.tgz",
- "integrity": "sha512-QMV5mA+pCYZQcUEPQkmor9vcPQ2MT+Ipuu8qdi1gVxbNiIiErEGft+eny1ak19qALoBkccS5AHaCaCDzh7b9MA==",
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-url/-/postcss-url-5.1.2.tgz",
+ "integrity": "sha1-mLMWW+jVkkccsMqt3iwNH4MvEz4=",
"dev": true,
"requires": {
+ "directory-encoder": "0.7.2",
+ "js-base64": "2.4.5",
"mime": "1.6.0",
"minimatch": "3.0.4",
"mkdirp": "0.5.1",
- "postcss": "6.0.22",
- "xxhashjs": "0.2.2"
+ "path-is-absolute": "1.0.1",
+ "postcss": "5.2.18"
}
},
"postcss-value-parser": {
@@ -7955,10 +8421,21 @@
"integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=",
"dev": true
},
- "prelude-ls": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
- "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
+ "postcss-zindex": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-2.2.0.tgz",
+ "integrity": "sha1-0hCd3AVbka9n/EyzsCWUZjnSryI=",
+ "dev": true,
+ "requires": {
+ "has": "1.0.1",
+ "postcss": "5.2.18",
+ "uniqs": "2.0.0"
+ }
+ },
+ "prepend-http": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz",
+ "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=",
"dev": true
},
"preserve": {
@@ -7989,6 +8466,12 @@
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
"dev": true
},
+ "progress": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz",
+ "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=",
+ "dev": true
+ },
"promise": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
@@ -8005,26 +8488,16 @@
"integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
"dev": true
},
- "promisify-call": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/promisify-call/-/promisify-call-2.0.4.tgz",
- "integrity": "sha1-1IwtRWUszM1SgB3ey9UzptS9X7o=",
- "dev": true,
- "optional": true,
- "requires": {
- "with-callback": "1.0.2"
- }
- },
"protractor": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/protractor/-/protractor-5.1.2.tgz",
- "integrity": "sha1-myIXQXCaTGLVzVPGqt1UpxE36V8=",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/protractor/-/protractor-5.1.0.tgz",
+ "integrity": "sha1-0mUPLx/mkDGq01KE7sHveaUGJaE=",
"dev": true,
"requires": {
- "@types/node": "6.0.113",
+ "@types/node": "6.0.78",
"@types/q": "0.0.32",
"@types/selenium-webdriver": "2.53.43",
- "blocking-proxy": "0.0.5",
+ "blocking-proxy": "0.0.4",
"chalk": "1.1.3",
"glob": "7.1.2",
"jasmine": "2.99.0",
@@ -8038,10 +8511,10 @@
"webdriver-manager": "12.0.6"
},
"dependencies": {
- "ansi-styles": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
- "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+ "@types/selenium-webdriver": {
+ "version": "2.53.43",
+ "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-2.53.43.tgz",
+ "integrity": "sha512-UBYHWph6P3tutkbXpW6XYg9ZPbTKjw/YC2hGG1/GEvWwTbvezBUv3h+mmUFw79T3RFPnmedpiXdOBbXX+4l0jg==",
"dev": true
},
"chalk": {
@@ -8086,16 +8559,20 @@
"pinkie-promise": "2.0.1"
}
},
- "minimist": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
- "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
- "dev": true
+ "optimist": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
+ "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
+ "dev": true,
+ "requires": {
+ "minimist": "0.0.8",
+ "wordwrap": "0.0.3"
+ }
},
- "pify": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
- "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+ "q": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz",
+ "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=",
"dev": true
},
"supports-color": {
@@ -8121,6 +8598,14 @@
"rimraf": "2.6.2",
"semver": "5.5.0",
"xml2js": "0.4.19"
+ },
+ "dependencies": {
+ "minimist": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+ "dev": true
+ }
}
}
}
@@ -8135,42 +8620,6 @@
"ipaddr.js": "1.6.0"
}
},
- "proxy-agent": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-3.0.0.tgz",
- "integrity": "sha512-g6n6vnk8fRf705ShN+FEXFG/SDJaW++lSs0d9KaJh4uBWW/wi7en4Cpo5VYQW3SZzAE121lhB/KLQrbURoubZw==",
- "dev": true,
- "optional": true,
- "requires": {
- "agent-base": "4.2.0",
- "debug": "3.1.0",
- "http-proxy-agent": "2.1.0",
- "https-proxy-agent": "2.2.1",
- "lru-cache": "4.1.3",
- "pac-proxy-agent": "2.0.2",
- "proxy-from-env": "1.0.0",
- "socks-proxy-agent": "3.0.1"
- },
- "dependencies": {
- "debug": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
- "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
- "dev": true,
- "optional": true,
- "requires": {
- "ms": "2.0.0"
- }
- }
- }
- },
- "proxy-from-env": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
- "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=",
- "dev": true,
- "optional": true
- },
"prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
@@ -8218,15 +8667,15 @@
}
},
"punycode": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
- "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+ "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
"dev": true
},
"q": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz",
- "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=",
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
+ "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=",
"dev": true
},
"qjobs": {
@@ -8241,6 +8690,16 @@
"integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=",
"dev": true
},
+ "query-string": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz",
+ "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=",
+ "dev": true,
+ "requires": {
+ "object-assign": "4.1.1",
+ "strict-uri-encode": "1.1.0"
+ }
+ },
"querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
@@ -8275,12 +8734,6 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz",
"integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==",
"dev": true
- },
- "kind-of": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
- "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
- "dev": true
}
}
},
@@ -8353,82 +8806,51 @@
"integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=",
"dev": true
},
- "read-cache": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
- "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=",
+ "read-installed": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz",
+ "integrity": "sha1-/5uLZ/GH0eTCm5/rMfayI6zRkGc=",
"dev": true,
"requires": {
- "pify": "2.3.0"
- },
- "dependencies": {
- "pify": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
- "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
- "dev": true
- }
+ "debuglog": "1.0.1",
+ "graceful-fs": "4.1.11",
+ "read-package-json": "2.0.13",
+ "readdir-scoped-modules": "1.0.2",
+ "semver": "5.5.0",
+ "slide": "1.1.6",
+ "util-extend": "1.0.3"
+ }
+ },
+ "read-package-json": {
+ "version": "2.0.13",
+ "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.0.13.tgz",
+ "integrity": "sha512-/1dZ7TRZvGrYqE0UAfN6qQb5GYBsNcqS1C0tNK601CFOJmtHI7NIGXwetEPU/OtoFHZL3hDxm4rolFFVE9Bnmg==",
+ "dev": true,
+ "requires": {
+ "glob": "7.1.2",
+ "graceful-fs": "4.1.11",
+ "json-parse-better-errors": "1.0.2",
+ "normalize-package-data": "2.4.0",
+ "slash": "1.0.0"
}
},
"read-pkg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
"integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
- "dev": true,
"requires": {
"load-json-file": "1.1.0",
"normalize-package-data": "2.4.0",
"path-type": "1.1.0"
- },
- "dependencies": {
- "path-type": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
- "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
- "dev": true,
- "requires": {
- "graceful-fs": "4.1.11",
- "pify": "2.3.0",
- "pinkie-promise": "2.0.1"
- }
- },
- "pify": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
- "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
- "dev": true
- }
}
},
"read-pkg-up": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
"integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
- "dev": true,
"requires": {
"find-up": "1.1.2",
"read-pkg": "1.1.0"
- },
- "dependencies": {
- "find-up": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
- "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
- "dev": true,
- "requires": {
- "path-exists": "2.1.0",
- "pinkie-promise": "2.0.1"
- }
- },
- "path-exists": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
- "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
- "dev": true,
- "requires": {
- "pinkie-promise": "2.0.1"
- }
- }
}
},
"readable-stream": {
@@ -8446,6 +8868,18 @@
"util-deprecate": "1.0.2"
}
},
+ "readdir-scoped-modules": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz",
+ "integrity": "sha1-n6+jfShr5dksuuve4DDcm19AZ0c=",
+ "dev": true,
+ "requires": {
+ "debuglog": "1.0.1",
+ "dezalgo": "1.0.3",
+ "graceful-fs": "4.1.11",
+ "once": "1.4.0"
+ }
+ },
"readdirp": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
@@ -8468,31 +8902,41 @@
"strip-indent": "1.0.1"
}
},
- "redis": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz",
- "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==",
+ "reduce-css-calc": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz",
+ "integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=",
"dev": true,
- "optional": true,
"requires": {
- "double-ended-queue": "2.1.0-0",
- "redis-commands": "1.3.5",
- "redis-parser": "2.6.0"
+ "balanced-match": "0.4.2",
+ "math-expression-evaluator": "1.2.17",
+ "reduce-function-call": "1.0.2"
+ },
+ "dependencies": {
+ "balanced-match": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz",
+ "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=",
+ "dev": true
+ }
}
},
- "redis-commands": {
- "version": "1.3.5",
- "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.3.5.tgz",
- "integrity": "sha512-foGF8u6MXGFF++1TZVC6icGXuMYPftKXt1FBT2vrfU9ZATNtZJ8duRC5d1lEfE8hyVe3jhelHGB91oB7I6qLsA==",
+ "reduce-function-call": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.2.tgz",
+ "integrity": "sha1-WiAL+S4ON3UXUv5FsKszD9S2vpk=",
"dev": true,
- "optional": true
- },
- "redis-parser": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz",
- "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=",
- "dev": true,
- "optional": true
+ "requires": {
+ "balanced-match": "0.4.2"
+ },
+ "dependencies": {
+ "balanced-match": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz",
+ "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=",
+ "dev": true
+ }
+ }
},
"reflect-metadata": {
"version": "0.1.12",
@@ -8555,14 +8999,6 @@
"dev": true,
"requires": {
"jsesc": "0.5.0"
- },
- "dependencies": {
- "jsesc": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
- "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
- "dev": true
- }
}
},
"relateurl": {
@@ -8649,33 +9085,19 @@
"uuid": "3.2.1"
}
},
- "requestretry": {
- "version": "1.13.0",
- "resolved": "https://registry.npmjs.org/requestretry/-/requestretry-1.13.0.tgz",
- "integrity": "sha512-Lmh9qMvnQXADGAQxsXHP4rbgO6pffCfuR8XUBdP9aitJcLQJxhp7YZK4xAVYXnPJ5E52mwrfiKQtKonPL8xsmg==",
+ "request-progress": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz",
+ "integrity": "sha1-XTa7V5YcZzqlt4jbyBQf3yO0Tgg=",
"dev": true,
- "optional": true,
"requires": {
- "extend": "3.0.1",
- "lodash": "4.17.10",
- "request": "2.81.0",
- "when": "3.7.8"
- },
- "dependencies": {
- "when": {
- "version": "3.7.8",
- "resolved": "https://registry.npmjs.org/when/-/when-3.7.8.tgz",
- "integrity": "sha1-xxMLan6gRpPoQs3J56Hyqjmjn4I=",
- "dev": true,
- "optional": true
- }
+ "throttleit": "1.0.0"
}
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
- "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
- "dev": true
+ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
},
"require-from-string": {
"version": "1.2.1",
@@ -8686,8 +9108,7 @@
"require-main-filename": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
- "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
- "dev": true
+ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE="
},
"requires-port": {
"version": "1.0.0",
@@ -8696,34 +9117,19 @@
"dev": true
},
"reselect": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/reselect/-/reselect-3.0.1.tgz",
- "integrity": "sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc="
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-2.5.4.tgz",
+ "integrity": "sha1-t9I/3wC4P6etAnlUb427vXZccEc="
},
"resolve": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz",
- "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==",
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz",
+ "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==",
"dev": true,
"requires": {
"path-parse": "1.0.5"
}
},
- "resolve-cwd": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz",
- "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=",
- "dev": true,
- "requires": {
- "resolve-from": "3.0.0"
- }
- },
- "resolve-from": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz",
- "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=",
- "dev": true
- },
"resolve-url": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
@@ -8774,11 +9180,11 @@
}
},
"rxjs": {
- "version": "5.5.11",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.11.tgz",
- "integrity": "sha512-3bjO7UwWfA2CV7lmwYMBzj4fQ6Cq+ftHc2MvUe+WMS7wcdJ1LosDWmdjPQanYp2dBRj572p7PeU81JUxHKOcBA==",
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.4.1.tgz",
+ "integrity": "sha1-ti91fyeURdJloYpY+wpw3JDpFiY=",
"requires": {
- "symbol-observable": "1.0.1"
+ "symbol-observable": "1.2.0"
}
},
"safe-buffer": {
@@ -8796,12 +9202,6 @@
"ret": "0.1.15"
}
},
- "safer-buffer": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "dev": true
- },
"sass-graph": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz",
@@ -8813,6 +9213,40 @@
"lodash": "4.17.10",
"scss-tokenizer": "0.2.3",
"yargs": "7.1.0"
+ },
+ "dependencies": {
+ "yargs": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz",
+ "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "camelcase": "3.0.0",
+ "cliui": "3.2.0",
+ "decamelize": "1.2.0",
+ "get-caller-file": "1.0.2",
+ "os-locale": "1.4.0",
+ "read-pkg-up": "1.0.1",
+ "require-directory": "2.1.1",
+ "require-main-filename": "1.0.1",
+ "set-blocking": "2.0.0",
+ "string-width": "1.0.2",
+ "which-module": "1.0.0",
+ "y18n": "3.2.1",
+ "yargs-parser": "5.0.0"
+ }
+ },
+ "yargs-parser": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz",
+ "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "camelcase": "3.0.0"
+ }
+ }
}
},
"sass-loader": {
@@ -8826,6 +9260,14 @@
"lodash.tail": "4.1.1",
"neo-async": "2.5.1",
"pify": "3.0.0"
+ },
+ "dependencies": {
+ "pify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+ "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+ "dev": true
+ }
}
},
"saucelabs": {
@@ -8835,51 +9277,21 @@
"dev": true,
"requires": {
"https-proxy-agent": "1.0.0"
- },
- "dependencies": {
- "agent-base": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-2.1.1.tgz",
- "integrity": "sha1-1t4Q1a9hMtW9aSQn1G/FOFOQlMc=",
- "dev": true,
- "requires": {
- "extend": "3.0.1",
- "semver": "5.0.3"
- }
- },
- "https-proxy-agent": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz",
- "integrity": "sha1-NffabEjOTdv6JkiRrFk+5f+GceY=",
- "dev": true,
- "requires": {
- "agent-base": "2.1.1",
- "debug": "2.6.9",
- "extend": "3.0.1"
- }
- },
- "semver": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.0.3.tgz",
- "integrity": "sha1-d0Zt5YnNXTyV8TiqeLxWmjy10no=",
- "dev": true
- }
}
},
"sax": {
- "version": "0.5.8",
- "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz",
- "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true
},
"schema-utils": {
- "version": "0.4.5",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.5.tgz",
- "integrity": "sha512-yYrjb9TX2k/J1Y5UNy3KYdZq10xhYcF8nMpAW6o3hy6Q8WSIEf9lJHG/ePnOBfziPM3fvQwfOwa13U/Fh8qTfA==",
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz",
+ "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=",
"dev": true,
"requires": {
- "ajv": "6.5.1",
- "ajv-keywords": "3.2.0"
+ "ajv": "5.5.2"
}
},
"scss-tokenizer": {
@@ -8946,8 +9358,7 @@
"semver": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
- "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==",
- "dev": true
+ "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
},
"semver-dsl": {
"version": "1.0.1",
@@ -8958,15 +9369,6 @@
"semver": "5.5.0"
}
},
- "semver-intersect": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/semver-intersect/-/semver-intersect-1.3.1.tgz",
- "integrity": "sha1-j6hKnhAovSOeRTDRo+GB5pjYhLo=",
- "dev": true,
- "requires": {
- "semver": "5.5.0"
- }
- },
"send": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
@@ -9032,8 +9434,7 @@
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
- "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
- "dev": true
+ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
"set-immediate-shim": {
"version": "1.0.1",
@@ -9120,6 +9521,14 @@
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
"dev": true
},
+ "showdown": {
+ "version": "1.6.4",
+ "resolved": "https://registry.npmjs.org/showdown/-/showdown-1.6.4.tgz",
+ "integrity": "sha1-BWu7ZU7NuNhkOuEtbVl4k8yvRsY=",
+ "requires": {
+ "yargs": "6.6.0"
+ }
+ },
"signal-exit": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
@@ -9135,38 +9544,18 @@
"debug": "2.6.9"
}
},
- "slack-node": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/slack-node/-/slack-node-0.2.0.tgz",
- "integrity": "sha1-3kuN3aqLeT9h29KTgQT9q/N9+jA=",
- "dev": true,
- "optional": true,
- "requires": {
- "requestretry": "1.13.0"
- }
- },
"slash": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
"integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=",
"dev": true
},
- "smart-buffer": {
- "version": "1.1.15",
- "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-1.1.15.tgz",
- "integrity": "sha1-fxFLW2X6s+KjWqd1uxLw0cZJvxY=",
+ "slide": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz",
+ "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=",
"dev": true
},
- "smtp-connection": {
- "version": "2.12.0",
- "resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.12.0.tgz",
- "integrity": "sha1-1275EnyyPCJZ7bHoNJwujV4tdME=",
- "dev": true,
- "requires": {
- "httpntlm": "1.6.1",
- "nodemailer-shared": "1.1.0"
- }
- },
"snapdragon": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
@@ -9251,18 +9640,6 @@
"is-data-descriptor": "1.0.0",
"kind-of": "6.0.2"
}
- },
- "isobject": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
- "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
- "dev": true
- },
- "kind-of": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
- "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
- "dev": true
}
}
},
@@ -9273,6 +9650,17 @@
"dev": true,
"requires": {
"kind-of": "3.2.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "1.1.6"
+ }
+ }
}
},
"sntp": {
@@ -9285,82 +9673,163 @@
}
},
"socket.io": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.0.4.tgz",
- "integrity": "sha1-waRZDO/4fs8TxyZS8Eb3FrKeYBQ=",
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.7.2.tgz",
+ "integrity": "sha1-g7u98ueSY7N4kA2kA+eEPgXcO3E=",
"dev": true,
"requires": {
- "debug": "2.6.9",
- "engine.io": "3.1.5",
- "socket.io-adapter": "1.1.1",
- "socket.io-client": "2.0.4",
- "socket.io-parser": "3.1.3"
- }
- },
- "socket.io-adapter": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz",
- "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=",
- "dev": true
- },
- "socket.io-client": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.0.4.tgz",
- "integrity": "sha1-CRilUkBtxeVAs4Dc2Xr8SmQzL44=",
- "dev": true,
- "requires": {
- "backo2": "1.0.2",
- "base64-arraybuffer": "0.1.5",
- "component-bind": "1.0.0",
- "component-emitter": "1.2.1",
- "debug": "2.6.9",
- "engine.io-client": "3.1.6",
- "has-cors": "1.1.0",
- "indexof": "0.0.1",
- "object-component": "0.0.3",
- "parseqs": "0.0.5",
- "parseuri": "0.0.5",
- "socket.io-parser": "3.1.3",
- "to-array": "0.1.4"
- }
- },
- "socket.io-parser": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.1.3.tgz",
- "integrity": "sha512-g0a2HPqLguqAczs3dMECuA1RgoGFPyvDqcbaDEdCWY9g59kdUAz3YRmaJBNKXflrHNwB7Q12Gkf/0CZXfdHR7g==",
- "dev": true,
- "requires": {
- "component-emitter": "1.2.1",
- "debug": "3.1.0",
- "has-binary2": "1.0.3",
- "isarray": "2.0.1"
+ "debug": "2.3.3",
+ "engine.io": "1.8.2",
+ "has-binary": "0.1.7",
+ "object-assign": "4.1.0",
+ "socket.io-adapter": "0.5.0",
+ "socket.io-client": "1.7.2",
+ "socket.io-parser": "2.3.1"
},
"dependencies": {
"debug": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
- "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
+ "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=",
"dev": true,
"requires": {
- "ms": "2.0.0"
+ "ms": "0.7.2"
+ }
+ },
+ "ms": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz",
+ "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=",
+ "dev": true
+ },
+ "object-assign": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz",
+ "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=",
+ "dev": true
+ }
+ }
+ },
+ "socket.io-adapter": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz",
+ "integrity": "sha1-y21LuL7IHhB4uZZ3+c7QBGBmu4s=",
+ "dev": true,
+ "requires": {
+ "debug": "2.3.3",
+ "socket.io-parser": "2.3.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
+ "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=",
+ "dev": true,
+ "requires": {
+ "ms": "0.7.2"
+ }
+ },
+ "ms": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz",
+ "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=",
+ "dev": true
+ }
+ }
+ },
+ "socket.io-client": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.7.2.tgz",
+ "integrity": "sha1-Of2ww91FDjIbfkDP2DYS7FM91kQ=",
+ "dev": true,
+ "requires": {
+ "backo2": "1.0.2",
+ "component-bind": "1.0.0",
+ "component-emitter": "1.2.1",
+ "debug": "2.3.3",
+ "engine.io-client": "1.8.2",
+ "has-binary": "0.1.7",
+ "indexof": "0.0.1",
+ "object-component": "0.0.3",
+ "parseuri": "0.0.5",
+ "socket.io-parser": "2.3.1",
+ "to-array": "0.1.4"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
+ "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=",
+ "dev": true,
+ "requires": {
+ "ms": "0.7.2"
+ }
+ },
+ "ms": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz",
+ "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=",
+ "dev": true
+ }
+ }
+ },
+ "socket.io-parser": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.3.1.tgz",
+ "integrity": "sha1-3VMgJRA85Clpcya+/WQAX8/ltKA=",
+ "dev": true,
+ "requires": {
+ "component-emitter": "1.1.2",
+ "debug": "2.2.0",
+ "isarray": "0.0.1",
+ "json3": "3.3.2"
+ },
+ "dependencies": {
+ "component-emitter": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz",
+ "integrity": "sha1-KWWU8nU9qmOZbSrwjRWpURbJrsM=",
+ "dev": true
+ },
+ "debug": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
+ "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=",
+ "dev": true,
+ "requires": {
+ "ms": "0.7.1"
}
},
"isarray": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
- "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=",
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+ "dev": true
+ },
+ "ms": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz",
+ "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=",
"dev": true
}
}
},
"sockjs": {
- "version": "0.3.19",
- "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz",
- "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==",
+ "version": "0.3.18",
+ "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.18.tgz",
+ "integrity": "sha1-2bKJMWyn33dZXvKZ4HXw+TfrQgc=",
"dev": true,
"requires": {
"faye-websocket": "0.10.0",
- "uuid": "3.2.1"
+ "uuid": "2.0.3"
+ },
+ "dependencies": {
+ "uuid": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz",
+ "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=",
+ "dev": true
+ }
}
},
"sockjs-client": {
@@ -9374,7 +9843,7 @@
"faye-websocket": "0.11.1",
"inherits": "2.0.3",
"json3": "3.3.2",
- "url-parse": "1.4.1"
+ "url-parse": "1.4.0"
},
"dependencies": {
"faye-websocket": {
@@ -9388,24 +9857,13 @@
}
}
},
- "socks": {
- "version": "1.1.10",
- "resolved": "https://registry.npmjs.org/socks/-/socks-1.1.10.tgz",
- "integrity": "sha1-W4t/x8jzQcU+0FbpKbe/Tei6e1o=",
+ "sort-keys": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
+ "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=",
"dev": true,
"requires": {
- "ip": "1.1.5",
- "smart-buffer": "1.1.15"
- }
- },
- "socks-proxy-agent": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-3.0.1.tgz",
- "integrity": "sha512-ZwEDymm204mTzvdqyUqOdovVr2YRd2NYskrYrF2LXyZ9qDiMAoFESGK8CRphiO7rtbo2Y757k2Nia3x2hGtalA==",
- "dev": true,
- "requires": {
- "agent-base": "4.2.0",
- "socks": "1.1.10"
+ "is-plain-obj": "1.1.0"
}
},
"source-list-map": {
@@ -9420,6 +9878,37 @@
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
"dev": true
},
+ "source-map-loader": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-0.2.3.tgz",
+ "integrity": "sha512-MYbFX9DYxmTQFfy2v8FC1XZwpwHKYxg3SK8Wb7VPBKuhDjz8gi9re2819MsG4p49HDyiOSUKlmZ+nQBArW5CGw==",
+ "dev": true,
+ "requires": {
+ "async": "2.6.1",
+ "loader-utils": "0.2.17",
+ "source-map": "0.6.1"
+ },
+ "dependencies": {
+ "loader-utils": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz",
+ "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=",
+ "dev": true,
+ "requires": {
+ "big.js": "3.2.0",
+ "emojis-list": "2.1.0",
+ "json5": "0.5.1",
+ "object-assign": "4.1.1"
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ }
+ }
+ },
"source-map-resolve": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz",
@@ -9452,7 +9941,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz",
"integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==",
- "dev": true,
"requires": {
"spdx-expression-parse": "3.0.0",
"spdx-license-ids": "3.0.0"
@@ -9461,14 +9949,12 @@
"spdx-exceptions": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz",
- "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==",
- "dev": true
+ "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg=="
},
"spdx-expression-parse": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
"integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
- "dev": true,
"requires": {
"spdx-exceptions": "2.1.0",
"spdx-license-ids": "3.0.0"
@@ -9477,8 +9963,7 @@
"spdx-license-ids": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz",
- "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==",
- "dev": true
+ "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA=="
},
"spdy": {
"version": "3.4.7",
@@ -9525,9 +10010,9 @@
"dev": true
},
"sshpk": {
- "version": "1.14.2",
- "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz",
- "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=",
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz",
+ "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=",
"dev": true,
"requires": {
"asn1": "0.2.3",
@@ -9537,7 +10022,6 @@
"ecc-jsbn": "0.1.1",
"getpass": "0.1.7",
"jsbn": "0.1.1",
- "safer-buffer": "2.1.2",
"tweetnacl": "0.14.5"
},
"dependencies": {
@@ -9616,9 +10100,9 @@
}
},
"stream-http": {
- "version": "2.8.3",
- "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz",
- "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==",
+ "version": "2.8.2",
+ "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.2.tgz",
+ "integrity": "sha512-QllfrBhqF1DPcz46WxKTs6Mz1Bpc+8Qm6vbqOpVav5odAXwbyzwnEczoWqtxrsmlO+cJqtPrp/8gWKWjaKLLlA==",
"dev": true,
"requires": {
"builtin-status-codes": "3.0.0",
@@ -9634,34 +10118,16 @@
"integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=",
"dev": true
},
- "streamroller": {
- "version": "0.7.0",
- "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-0.7.0.tgz",
- "integrity": "sha512-WREzfy0r0zUqp3lGO096wRuUp7ho1X6uo/7DJfTlEi0Iv/4gT7YHqXDjKC2ioVGBZtE8QzsQD9nx1nIuoZ57jQ==",
- "dev": true,
- "requires": {
- "date-format": "1.2.0",
- "debug": "3.1.0",
- "mkdirp": "0.5.1",
- "readable-stream": "2.3.6"
- },
- "dependencies": {
- "debug": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
- "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
- }
- }
- }
+ "strict-uri-encode": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
+ "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=",
+ "dev": true
},
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
- "dev": true,
"requires": {
"code-point-at": "1.1.0",
"is-fullwidth-code-point": "1.0.0",
@@ -9687,7 +10153,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
- "dev": true,
"requires": {
"ansi-regex": "2.1.1"
}
@@ -9696,7 +10161,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
"integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
- "dev": true,
"requires": {
"is-utf8": "0.2.1"
}
@@ -9723,36 +10187,12 @@
"dev": true
},
"style-loader": {
- "version": "0.19.1",
- "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.19.1.tgz",
- "integrity": "sha512-IRE+ijgojrygQi3rsqT0U4dd+UcPCqcVvauZpCnQrGAlEe+FUIyrK93bUDScamesjP08JlQNsFJU+KmPedP5Og==",
+ "version": "0.13.2",
+ "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.13.2.tgz",
+ "integrity": "sha1-dFMzhM9pjHEEx5URULSXF63C87s=",
"dev": true,
"requires": {
- "loader-utils": "1.1.0",
- "schema-utils": "0.3.0"
- },
- "dependencies": {
- "ajv": {
- "version": "5.5.2",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
- "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
- "dev": true,
- "requires": {
- "co": "4.6.0",
- "fast-deep-equal": "1.1.0",
- "fast-json-stable-stringify": "2.0.0",
- "json-schema-traverse": "0.3.1"
- }
- },
- "schema-utils": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz",
- "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=",
- "dev": true,
- "requires": {
- "ajv": "5.5.2"
- }
- }
+ "loader-utils": "1.1.0"
}
},
"stylus": {
@@ -9783,6 +10223,12 @@
"path-is-absolute": "1.0.1"
}
},
+ "sax": {
+ "version": "0.5.8",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz",
+ "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=",
+ "dev": true
+ },
"source-map": {
"version": "0.1.43",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
@@ -9806,18 +10252,33 @@
}
},
"supports-color": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz",
- "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=",
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
+ "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=",
"dev": true,
"requires": {
- "has-flag": "2.0.0"
+ "has-flag": "1.0.0"
+ }
+ },
+ "svgo": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.7.2.tgz",
+ "integrity": "sha1-n1dyQTlSE1xv779Ar+ak+qiLS7U=",
+ "dev": true,
+ "requires": {
+ "coa": "1.0.4",
+ "colors": "1.1.2",
+ "csso": "2.3.2",
+ "js-yaml": "3.7.0",
+ "mkdirp": "0.5.1",
+ "sax": "1.2.4",
+ "whet.extend": "0.9.9"
}
},
"symbol-observable": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz",
- "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ="
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
+ "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
},
"tapable": {
"version": "0.2.8",
@@ -9837,6 +10298,22 @@
"inherits": "2.0.3"
}
},
+ "text-mask-addons": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/text-mask-addons/-/text-mask-addons-3.7.2.tgz",
+ "integrity": "sha512-uezfAuWcDJ/B/lu5ooz38DEdI8NfIVX2xKwshLs40Er8MBGSppklOqOYw32wEmCUP7XPVX2u3u2TdQcIRuBLXA=="
+ },
+ "text-mask-core": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/text-mask-core/-/text-mask-core-5.1.1.tgz",
+ "integrity": "sha512-tcHJSs0jlHpIed2flCuZZhUIGN1KfDsN/qCW7lkYxyXkKt4OLd3GXCHPFsY9rOLhmyC43ZRTM+tCxizBzluWpw=="
+ },
+ "throttleit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",
+ "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=",
+ "dev": true
+ },
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
@@ -9853,13 +10330,6 @@
"xtend": "4.0.1"
}
},
- "thunkify": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/thunkify/-/thunkify-2.1.2.tgz",
- "integrity": "sha1-+qDp0jDFGsyVyhOjYawFyn4EVT0=",
- "dev": true,
- "optional": true
- },
"thunky": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.0.2.tgz",
@@ -9881,17 +10351,10 @@
"setimmediate": "1.0.5"
}
},
- "timespan": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/timespan/-/timespan-2.3.0.tgz",
- "integrity": "sha1-SQLOBAvRPYRcj1myfp1ZutbzmSk=",
- "dev": true,
- "optional": true
- },
"tmp": {
- "version": "0.0.33",
- "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
- "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+ "version": "0.0.28",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.28.tgz",
+ "integrity": "sha1-Fyc1t/YU6nrzlmT6hM8N5OUV0SA=",
"dev": true,
"requires": {
"os-tmpdir": "1.0.2"
@@ -9922,6 +10385,17 @@
"dev": true,
"requires": {
"kind-of": "3.2.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "1.1.6"
+ }
+ }
}
},
"to-regex": {
@@ -9944,17 +10418,6 @@
"requires": {
"is-number": "3.0.0",
"repeat-string": "1.6.1"
- },
- "dependencies": {
- "is-number": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
- "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
- "dev": true,
- "requires": {
- "kind-of": "3.2.2"
- }
- }
}
},
"toposort": {
@@ -9970,20 +10433,12 @@
"dev": true,
"requires": {
"punycode": "1.4.1"
- },
- "dependencies": {
- "punycode": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
- "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
- "dev": true
- }
}
},
- "tree-kill": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.0.tgz",
- "integrity": "sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg==",
+ "treeify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz",
+ "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==",
"dev": true
},
"trim-newlines": {
@@ -10024,82 +10479,63 @@
}
}
},
+ "tryer": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.0.tgz",
+ "integrity": "sha1-Antp+oIyJeVRys4+8DsR9qs3wdc=",
+ "dev": true
+ },
"ts-node": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-4.1.0.tgz",
- "integrity": "sha512-xcZH12oVg9PShKhy3UHyDmuDLV3y7iKwX25aMVPt1SIXSuAfWkFiGPEkg+th8R4YKW/QCxDoW7lJdb15lx6QWg==",
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-3.0.6.tgz",
+ "integrity": "sha1-VRJ/95DH7r9rpowebd6UsJqqIeA=",
"dev": true,
"requires": {
"arrify": "1.0.1",
- "chalk": "2.4.1",
+ "chalk": "1.1.3",
"diff": "3.5.0",
"make-error": "1.3.4",
"minimist": "1.2.0",
"mkdirp": "0.5.1",
- "source-map-support": "0.5.6",
- "tsconfig": "7.0.0",
- "v8flags": "3.1.1",
+ "source-map-support": "0.4.18",
+ "tsconfig": "6.0.0",
+ "v8flags": "2.1.1",
"yn": "2.0.0"
},
"dependencies": {
"chalk": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
- "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true,
"requires": {
- "ansi-styles": "3.2.1",
+ "ansi-styles": "2.2.1",
"escape-string-regexp": "1.0.5",
- "supports-color": "5.4.0"
+ "has-ansi": "2.0.0",
+ "strip-ansi": "3.0.1",
+ "supports-color": "2.0.0"
}
},
- "has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
- "dev": true
- },
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
},
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
- },
- "source-map-support": {
- "version": "0.5.6",
- "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz",
- "integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==",
- "dev": true,
- "requires": {
- "buffer-from": "1.1.0",
- "source-map": "0.6.1"
- }
- },
"supports-color": {
- "version": "5.4.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
- "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
- "dev": true,
- "requires": {
- "has-flag": "3.0.0"
- }
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+ "dev": true
}
}
},
"tsconfig": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz",
- "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==",
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-6.0.0.tgz",
+ "integrity": "sha1-aw6DdgA9evGGT434+J3QBZ/80DI=",
"dev": true,
"requires": {
- "@types/strip-bom": "3.0.0",
- "@types/strip-json-comments": "0.0.30",
"strip-bom": "3.0.0",
"strip-json-comments": "2.0.1"
},
@@ -10113,15 +10549,15 @@
}
},
"tsickle": {
- "version": "0.27.5",
- "resolved": "https://registry.npmjs.org/tsickle/-/tsickle-0.27.5.tgz",
- "integrity": "sha512-NP+CjM1EXza/M8mOXBLH3vkFEJiu1zfEAlC5WdJxHPn8l96QPz5eooP6uAgYtw1CcKfuSyIiheNUdKxtDWCNeg==",
+ "version": "0.21.6",
+ "resolved": "https://registry.npmjs.org/tsickle/-/tsickle-0.21.6.tgz",
+ "integrity": "sha1-U7Abl5xcE/2xOvs/uVgXflmRWI0=",
"dev": true,
"requires": {
"minimist": "1.2.0",
"mkdirp": "0.5.1",
- "source-map": "0.6.1",
- "source-map-support": "0.5.6"
+ "source-map": "0.5.7",
+ "source-map-support": "0.4.18"
},
"dependencies": {
"minimist": {
@@ -10129,93 +10565,49 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
- },
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
- },
- "source-map-support": {
- "version": "0.5.6",
- "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz",
- "integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==",
- "dev": true,
- "requires": {
- "buffer-from": "1.1.0",
- "source-map": "0.6.1"
- }
}
}
},
"tslib": {
- "version": "1.9.2",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.2.tgz",
- "integrity": "sha512-AVP5Xol3WivEr7hnssHDsaM+lVrVXWUvd1cfXTRkTj80b//6g2wIFEH6hZG0muGZRnHGrfttpdzRk3YlBkWjKw=="
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.1.tgz",
+ "integrity": "sha512-avfPS28HmGLLc2o4elcc2EIq2FcH++Yo5YxpBZi9Yw93BCTGFthI4HPE4Rpep6vSYQaK8e69PelM44tPj+RaQg=="
},
"tslint": {
- "version": "5.9.1",
- "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.9.1.tgz",
- "integrity": "sha1-ElX4ej/1frCw4fDmEKi0dIBGya4=",
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.2.0.tgz",
+ "integrity": "sha1-FqKt3yDLdIOF9UTpoO2rCGvDQRQ=",
"dev": true,
"requires": {
"babel-code-frame": "6.26.0",
- "builtin-modules": "1.1.1",
- "chalk": "2.4.1",
- "commander": "2.15.1",
+ "colors": "1.1.2",
"diff": "3.5.0",
+ "findup-sync": "0.3.0",
"glob": "7.1.2",
- "js-yaml": "3.12.0",
- "minimatch": "3.0.4",
- "resolve": "1.8.1",
+ "optimist": "0.6.1",
+ "resolve": "1.7.1",
"semver": "5.5.0",
- "tslib": "1.9.2",
- "tsutils": "2.27.1"
+ "tslib": "1.9.1",
+ "tsutils": "1.9.1"
},
"dependencies": {
- "chalk": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
- "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+ "optimist": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
+ "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
"dev": true,
"requires": {
- "ansi-styles": "3.2.1",
- "escape-string-regexp": "1.0.5",
- "supports-color": "5.4.0"
- }
- },
- "has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
- "dev": true
- },
- "supports-color": {
- "version": "5.4.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
- "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
- "dev": true,
- "requires": {
- "has-flag": "3.0.0"
+ "minimist": "0.0.8",
+ "wordwrap": "0.0.3"
}
}
}
},
- "tsscmp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.5.tgz",
- "integrity": "sha1-fcSjOvcVgatDN9qR2FylQn69mpc=",
- "dev": true,
- "optional": true
- },
"tsutils": {
- "version": "2.27.1",
- "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.27.1.tgz",
- "integrity": "sha512-AE/7uzp32MmaHvNNFES85hhUDHFdFZp6OAiZcd6y4ZKKIg6orJTm8keYWBhIhrJQH3a4LzNKat7ZPXZt5aTf6w==",
- "dev": true,
- "requires": {
- "tslib": "1.9.2"
- }
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-1.9.1.tgz",
+ "integrity": "sha1-ufmrROVa+WgYMdXyjQrur1x1DLA=",
+ "dev": true
},
"tty-browserify": {
"version": "0.0.0",
@@ -10239,15 +10631,6 @@
"dev": true,
"optional": true
},
- "type-check": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
- "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
- "dev": true,
- "requires": {
- "prelude-ls": "1.1.2"
- }
- },
"type-is": {
"version": "1.6.16",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
@@ -10265,15 +10648,15 @@
"dev": true
},
"typescript": {
- "version": "2.5.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.3.tgz",
- "integrity": "sha512-ptLSQs2S4QuS6/OD1eAKG+S5G8QQtrU5RT32JULdZQtM1L3WTi34Wsu48Yndzi8xsObRAB9RPt/KhA9wlpEF6w==",
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.3.4.tgz",
+ "integrity": "sha1-PTgyGCgjHkNPKHUUlZw3qCtin0I=",
"dev": true
},
"uglify-js": {
- "version": "3.3.28",
- "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.3.28.tgz",
- "integrity": "sha512-68Rc/aA6cswiaQ5SrE979UJcXX+ADA1z33/ZsPd+fbAiVdjZ16OXdbtGO+rJUUBgK6qdf3SOPhQf3K/ybF5Miw==",
+ "version": "3.3.27",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.3.27.tgz",
+ "integrity": "sha512-O94wxMSb3td/TlofkITYvYIlvIVdldvNXDVRekzK13CQZuL37ua4nrdXX0Ro7MapfUVzglRHs0/+imPRUdOghg==",
"dev": true,
"requires": {
"commander": "2.15.1",
@@ -10296,55 +10679,68 @@
"optional": true
},
"uglifyjs-webpack-plugin": {
- "version": "1.2.6",
- "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.6.tgz",
- "integrity": "sha512-NDP94ahjW7ZH+qzdjxjIV04n5YGnrYD2jeHgKgnpUKmdAfcXEO5DbVo21fXAm/KPMyX9k21zWFBMYm9m9R2ptg==",
+ "version": "0.4.6",
+ "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz",
+ "integrity": "sha1-uVH0q7a9YX5m9j64kUmOORdj4wk=",
"dev": true,
"requires": {
- "cacache": "10.0.4",
- "find-cache-dir": "1.0.0",
- "schema-utils": "0.4.5",
- "serialize-javascript": "1.5.0",
- "source-map": "0.6.1",
- "uglify-es": "3.3.9",
- "webpack-sources": "1.1.0",
- "worker-farm": "1.6.0"
+ "source-map": "0.5.7",
+ "uglify-js": "2.8.29",
+ "webpack-sources": "1.1.0"
},
"dependencies": {
- "commander": {
- "version": "2.13.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz",
- "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==",
+ "camelcase": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz",
+ "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=",
"dev": true
},
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
- },
- "uglify-es": {
- "version": "3.3.9",
- "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz",
- "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==",
+ "cliui": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz",
+ "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=",
"dev": true,
"requires": {
- "commander": "2.13.0",
- "source-map": "0.6.1"
+ "center-align": "0.1.3",
+ "right-align": "0.1.3",
+ "wordwrap": "0.0.2"
+ }
+ },
+ "uglify-js": {
+ "version": "2.8.29",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
+ "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=",
+ "dev": true,
+ "requires": {
+ "source-map": "0.5.7",
+ "uglify-to-browserify": "1.0.2",
+ "yargs": "3.10.0"
+ }
+ },
+ "wordwrap": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz",
+ "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=",
+ "dev": true
+ },
+ "yargs": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
+ "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=",
+ "dev": true,
+ "requires": {
+ "camelcase": "1.2.1",
+ "cliui": "2.1.0",
+ "decamelize": "1.2.0",
+ "window-size": "0.1.0"
}
}
}
},
"ultron": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
- "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==",
- "dev": true
- },
- "underscore": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz",
- "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz",
+ "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=",
"dev": true
},
"union-value": {
@@ -10382,6 +10778,18 @@
}
}
},
+ "uniq": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
+ "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
+ "dev": true
+ },
+ "uniqs": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz",
+ "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=",
+ "dev": true
+ },
"unique-filename": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.0.tgz",
@@ -10401,9 +10809,9 @@
}
},
"universalify": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
- "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz",
+ "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=",
"dev": true
},
"unpipe": {
@@ -10449,12 +10857,6 @@
"resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz",
"integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=",
"dev": true
- },
- "isobject": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
- "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
- "dev": true
}
}
},
@@ -10470,15 +10872,6 @@
"integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=",
"dev": true
},
- "uri-js": {
- "version": "4.2.2",
- "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
- "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
- "dev": true,
- "requires": {
- "punycode": "2.1.1"
- }
- },
"urix": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
@@ -10512,35 +10905,12 @@
"loader-utils": "1.1.0",
"mime": "1.6.0",
"schema-utils": "0.3.0"
- },
- "dependencies": {
- "ajv": {
- "version": "5.5.2",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
- "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
- "dev": true,
- "requires": {
- "co": "4.6.0",
- "fast-deep-equal": "1.1.0",
- "fast-json-stable-stringify": "2.0.0",
- "json-schema-traverse": "0.3.1"
- }
- },
- "schema-utils": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz",
- "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=",
- "dev": true,
- "requires": {
- "ajv": "5.5.2"
- }
- }
}
},
"url-parse": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.1.tgz",
- "integrity": "sha512-x95Td74QcvICAA0+qERaVkRpTGKyBHHYdwL2LXZm5t/gBtCB9KQSO/0zQgSTYEV1p0WcvSg79TLNPSvd5IDJMQ==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.0.tgz",
+ "integrity": "sha512-ERuGxDiQ6Xw/agN4tuoCRbmwRuZP0cJ1lJxJubXr5Q/5cDa78+Dc4wfvtxzhzhkm5VvmW6Mf8EVj9SPGN4l8Lg==",
"dev": true,
"requires": {
"querystringify": "2.0.0",
@@ -10554,41 +10924,39 @@
"dev": true,
"requires": {
"kind-of": "6.0.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
- "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
- "dev": true
- }
}
},
+ "user-home": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz",
+ "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=",
+ "dev": true
+ },
"useragent": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.2.1.tgz",
- "integrity": "sha1-z1k+9PLRdYdei7ZY6pLhik/QbY4=",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz",
+ "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==",
"dev": true,
"requires": {
- "lru-cache": "2.2.4",
- "tmp": "0.0.33"
- },
- "dependencies": {
- "lru-cache": {
- "version": "2.2.4",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.2.4.tgz",
- "integrity": "sha1-bGWGGb7PFAMdDQtZSxYELOTcBj0=",
- "dev": true
- }
+ "lru-cache": "4.1.3",
+ "tmp": "0.0.28"
}
},
"util": {
- "version": "0.10.4",
- "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
- "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
+ "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
"dev": true,
"requires": {
- "inherits": "2.0.3"
+ "inherits": "2.0.1"
+ },
+ "dependencies": {
+ "inherits": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
+ "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
+ "dev": true
+ }
}
},
"util-deprecate": {
@@ -10597,6 +10965,12 @@
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
},
+ "util-extend": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz",
+ "integrity": "sha1-p8IW0mdUUWljeztu3GypEZ4v+T8=",
+ "dev": true
+ },
"utila": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz",
@@ -10615,27 +10989,19 @@
"integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==",
"dev": true
},
- "uws": {
- "version": "9.14.0",
- "resolved": "https://registry.npmjs.org/uws/-/uws-9.14.0.tgz",
- "integrity": "sha512-HNMztPP5A1sKuVFmdZ6BPVpBQd5bUjNC8EFMFiICK+oho/OQsAJy5hnIx4btMHiOk8j04f/DbIlqnEZ9d72dqg==",
- "dev": true,
- "optional": true
- },
"v8flags": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.1.tgz",
- "integrity": "sha512-iw/1ViSEaff8NJ3HLyEjawk/8hjJib3E7pvG4pddVXfUg1983s3VGsiClDjhK64MQVDGqc1Q8r18S4VKQZS9EQ==",
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz",
+ "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=",
"dev": true,
"requires": {
- "homedir-polyfill": "1.0.1"
+ "user-home": "1.1.1"
}
},
"validate-npm-package-license": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz",
"integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==",
- "dev": true,
"requires": {
"spdx-correct": "3.0.0",
"spdx-expression-parse": "3.0.0"
@@ -10647,6 +11013,12 @@
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
"dev": true
},
+ "vendors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.2.tgz",
+ "integrity": "sha512-w/hry/368nO21AN9QljsaIhb9ZiZtZARoVH5f3CsFbawdLdayCgKRPup7CggujvySMxx0I91NOyxdVENohprLQ==",
+ "dev": true
+ },
"verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
@@ -10693,352 +11065,9 @@
"integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==",
"dev": true,
"requires": {
- "chokidar": "2.0.4",
+ "chokidar": "2.0.3",
"graceful-fs": "4.1.11",
"neo-async": "2.5.1"
- },
- "dependencies": {
- "anymatch": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
- "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
- "dev": true,
- "requires": {
- "micromatch": "3.1.10",
- "normalize-path": "2.1.1"
- }
- },
- "arr-diff": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
- "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
- "dev": true
- },
- "array-unique": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
- "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
- "dev": true
- },
- "braces": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
- "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
- "dev": true,
- "requires": {
- "arr-flatten": "1.1.0",
- "array-unique": "0.3.2",
- "extend-shallow": "2.0.1",
- "fill-range": "4.0.0",
- "isobject": "3.0.1",
- "repeat-element": "1.1.2",
- "snapdragon": "0.8.2",
- "snapdragon-node": "2.1.1",
- "split-string": "3.1.0",
- "to-regex": "3.0.2"
- },
- "dependencies": {
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "0.1.1"
- }
- }
- }
- },
- "chokidar": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz",
- "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==",
- "dev": true,
- "requires": {
- "anymatch": "2.0.0",
- "async-each": "1.0.1",
- "braces": "2.3.2",
- "fsevents": "1.2.4",
- "glob-parent": "3.1.0",
- "inherits": "2.0.3",
- "is-binary-path": "1.0.1",
- "is-glob": "4.0.0",
- "lodash.debounce": "4.0.8",
- "normalize-path": "2.1.1",
- "path-is-absolute": "1.0.1",
- "readdirp": "2.1.0",
- "upath": "1.1.0"
- }
- },
- "expand-brackets": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
- "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
- "dev": true,
- "requires": {
- "debug": "2.6.9",
- "define-property": "0.2.5",
- "extend-shallow": "2.0.1",
- "posix-character-classes": "0.1.1",
- "regex-not": "1.0.2",
- "snapdragon": "0.8.2",
- "to-regex": "3.0.2"
- },
- "dependencies": {
- "define-property": {
- "version": "0.2.5",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
- "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
- "dev": true,
- "requires": {
- "is-descriptor": "0.1.6"
- }
- },
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "0.1.1"
- }
- },
- "is-accessor-descriptor": {
- "version": "0.1.6",
- "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
- "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
- "dev": true,
- "requires": {
- "kind-of": "3.2.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "1.1.6"
- }
- }
- }
- },
- "is-data-descriptor": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
- "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
- "dev": true,
- "requires": {
- "kind-of": "3.2.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "1.1.6"
- }
- }
- }
- },
- "is-descriptor": {
- "version": "0.1.6",
- "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
- "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
- "dev": true,
- "requires": {
- "is-accessor-descriptor": "0.1.6",
- "is-data-descriptor": "0.1.4",
- "kind-of": "5.1.0"
- }
- },
- "kind-of": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
- "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
- "dev": true
- }
- }
- },
- "extglob": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
- "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
- "dev": true,
- "requires": {
- "array-unique": "0.3.2",
- "define-property": "1.0.0",
- "expand-brackets": "2.1.4",
- "extend-shallow": "2.0.1",
- "fragment-cache": "0.2.1",
- "regex-not": "1.0.2",
- "snapdragon": "0.8.2",
- "to-regex": "3.0.2"
- },
- "dependencies": {
- "define-property": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
- "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
- "dev": true,
- "requires": {
- "is-descriptor": "1.0.2"
- }
- },
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "0.1.1"
- }
- }
- }
- },
- "fill-range": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
- "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
- "dev": true,
- "requires": {
- "extend-shallow": "2.0.1",
- "is-number": "3.0.0",
- "repeat-string": "1.6.1",
- "to-regex-range": "2.1.1"
- },
- "dependencies": {
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "0.1.1"
- }
- }
- }
- },
- "glob-parent": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
- "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
- "dev": true,
- "requires": {
- "is-glob": "3.1.0",
- "path-dirname": "1.0.2"
- },
- "dependencies": {
- "is-glob": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
- "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
- "dev": true,
- "requires": {
- "is-extglob": "2.1.1"
- }
- }
- }
- },
- "is-accessor-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
- "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
- "dev": true,
- "requires": {
- "kind-of": "6.0.2"
- }
- },
- "is-data-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
- "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
- "dev": true,
- "requires": {
- "kind-of": "6.0.2"
- }
- },
- "is-descriptor": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
- "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
- "dev": true,
- "requires": {
- "is-accessor-descriptor": "1.0.0",
- "is-data-descriptor": "1.0.0",
- "kind-of": "6.0.2"
- }
- },
- "is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
- "dev": true
- },
- "is-glob": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
- "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=",
- "dev": true,
- "requires": {
- "is-extglob": "2.1.1"
- }
- },
- "is-number": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
- "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
- "dev": true,
- "requires": {
- "kind-of": "3.2.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "1.1.6"
- }
- }
- }
- },
- "isobject": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
- "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
- "dev": true
- },
- "kind-of": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
- "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
- "dev": true
- },
- "micromatch": {
- "version": "3.1.10",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
- "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
- "dev": true,
- "requires": {
- "arr-diff": "4.0.0",
- "array-unique": "0.3.2",
- "braces": "2.3.2",
- "define-property": "2.0.2",
- "extend-shallow": "3.0.2",
- "extglob": "2.0.4",
- "fragment-cache": "0.2.1",
- "kind-of": "6.0.2",
- "nanomatch": "1.2.9",
- "object.pick": "1.3.0",
- "regex-not": "1.0.2",
- "snapdragon": "0.8.2",
- "to-regex": "3.0.2"
- }
- }
}
},
"wbuf": {
@@ -11056,7 +11085,7 @@
"integrity": "sha1-gcUzqeM9W/tZe05j4s2yW1R3dRU=",
"dev": true,
"requires": {
- "@types/selenium-webdriver": "2.53.43",
+ "@types/selenium-webdriver": "2.53.36",
"selenium-webdriver": "2.53.3"
},
"dependencies": {
@@ -11081,7 +11110,7 @@
"adm-zip": "0.4.4",
"rimraf": "2.6.2",
"tmp": "0.0.24",
- "ws": "1.1.5",
+ "ws": "1.1.1",
"xml2js": "0.4.4"
}
},
@@ -11091,22 +11120,6 @@
"integrity": "sha1-1qXhmNFKmDXMby18PZ4wJCjIzxI=",
"dev": true
},
- "ultron": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz",
- "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=",
- "dev": true
- },
- "ws": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz",
- "integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==",
- "dev": true,
- "requires": {
- "options": "0.0.6",
- "ultron": "1.0.2"
- }
- },
"xml2js": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.4.tgz",
@@ -11120,15 +11133,15 @@
}
},
"webpack": {
- "version": "3.11.0",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-3.11.0.tgz",
- "integrity": "sha512-3kOFejWqj5ISpJk4Qj/V7w98h9Vl52wak3CLiw/cDOfbVTq7FeoZ0SdoHHY9PYlHr50ZS42OfvzE2vB4nncKQg==",
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-3.6.0.tgz",
+ "integrity": "sha512-OsHT3D0W0KmPPh60tC7asNnOmST6bKTiR90UyEdT9QYoaJ4OYN4Gg7WK1k3VxHK07ZoiYWPsKvlS/gAjwL/vRA==",
"dev": true,
"requires": {
- "acorn": "5.7.1",
+ "acorn": "5.5.3",
"acorn-dynamic-import": "2.0.2",
- "ajv": "6.5.1",
- "ajv-keywords": "3.2.0",
+ "ajv": "5.5.2",
+ "ajv-keywords": "2.1.1",
"async": "2.6.1",
"enhanced-resolve": "3.4.1",
"escope": "3.6.0",
@@ -11156,22 +11169,32 @@
"dev": true
},
"camelcase": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz",
- "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=",
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
+ "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
"dev": true
},
- "cliui": {
+ "find-up": {
"version": "2.1.0",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz",
- "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
+ "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
"dev": true,
"requires": {
- "center-align": "0.1.3",
- "right-align": "0.1.3",
- "wordwrap": "0.0.2"
+ "locate-path": "2.0.0"
}
},
+ "has-flag": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
+ "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "dev": true
+ },
"load-json-file": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
@@ -11204,12 +11227,6 @@
"pify": "2.3.0"
}
},
- "pify": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
- "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
- "dev": true
- },
"read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
@@ -11239,23 +11256,15 @@
"requires": {
"is-fullwidth-code-point": "2.0.0",
"strip-ansi": "4.0.0"
- },
- "dependencies": {
- "is-fullwidth-code-point": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
- "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
- "dev": true
- },
- "strip-ansi": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
- "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
- "dev": true,
- "requires": {
- "ansi-regex": "3.0.0"
- }
- }
+ }
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "3.0.0"
}
},
"strip-bom": {
@@ -11264,40 +11273,13 @@
"integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
"dev": true
},
- "uglify-js": {
- "version": "2.8.29",
- "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
- "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=",
+ "supports-color": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz",
+ "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=",
"dev": true,
"requires": {
- "source-map": "0.5.7",
- "uglify-to-browserify": "1.0.2",
- "yargs": "3.10.0"
- },
- "dependencies": {
- "yargs": {
- "version": "3.10.0",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
- "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=",
- "dev": true,
- "requires": {
- "camelcase": "1.2.1",
- "cliui": "2.1.0",
- "decamelize": "1.2.0",
- "window-size": "0.1.0"
- }
- }
- }
- },
- "uglifyjs-webpack-plugin": {
- "version": "0.4.6",
- "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz",
- "integrity": "sha1-uVH0q7a9YX5m9j64kUmOORdj4wk=",
- "dev": true,
- "requires": {
- "source-map": "0.5.7",
- "uglify-js": "2.8.29",
- "webpack-sources": "1.1.0"
+ "has-flag": "2.0.0"
}
},
"which-module": {
@@ -11306,12 +11288,6 @@
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
"dev": true
},
- "y18n": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
- "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
- "dev": true
- },
"yargs": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz",
@@ -11331,38 +11307,6 @@
"which-module": "2.0.0",
"y18n": "3.2.1",
"yargs-parser": "7.0.0"
- },
- "dependencies": {
- "camelcase": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
- "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
- "dev": true
- },
- "cliui": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
- "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
- "dev": true,
- "requires": {
- "string-width": "1.0.2",
- "strip-ansi": "3.0.1",
- "wrap-ansi": "2.1.0"
- },
- "dependencies": {
- "string-width": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
- "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
- "dev": true,
- "requires": {
- "code-point-at": "1.1.0",
- "is-fullwidth-code-point": "1.0.0",
- "strip-ansi": "3.0.1"
- }
- }
- }
- }
}
},
"yargs-parser": {
@@ -11372,41 +11316,96 @@
"dev": true,
"requires": {
"camelcase": "4.1.0"
- },
- "dependencies": {
- "camelcase": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
- "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
- "dev": true
- }
}
}
}
},
- "webpack-core": {
- "version": "0.6.9",
- "resolved": "https://registry.npmjs.org/webpack-core/-/webpack-core-0.6.9.tgz",
- "integrity": "sha1-/FcViMhVjad76e+23r3Fo7FyvcI=",
+ "webpack-bundle-analyzer": {
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.13.1.tgz",
+ "integrity": "sha512-rwxyfecTAxoarCC9VlHlIpfQCmmJ/qWD5bpbjkof+7HrNhTNZIwZITxN6CdlYL2axGmwNUQ+tFgcSOiNXMf/sQ==",
"dev": true,
"requires": {
- "source-list-map": "0.1.8",
- "source-map": "0.4.4"
+ "acorn": "5.5.3",
+ "bfj-node4": "5.3.1",
+ "chalk": "2.4.1",
+ "commander": "2.15.1",
+ "ejs": "2.6.1",
+ "express": "4.16.3",
+ "filesize": "3.6.1",
+ "gzip-size": "4.1.0",
+ "lodash": "4.17.10",
+ "mkdirp": "0.5.1",
+ "opener": "1.4.3",
+ "ws": "4.1.0"
},
"dependencies": {
- "source-list-map": {
- "version": "0.1.8",
- "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-0.1.8.tgz",
- "integrity": "sha1-xVCyq1Qn9rPyH1r+rYjE9Vh7IQY=",
- "dev": true
- },
- "source-map": {
- "version": "0.4.4",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
- "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
+ "ws": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz",
+ "integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==",
"dev": true,
"requires": {
- "amdefine": "1.0.1"
+ "async-limiter": "1.0.0",
+ "safe-buffer": "5.1.2"
+ }
+ }
+ }
+ },
+ "webpack-concat-plugin": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/webpack-concat-plugin/-/webpack-concat-plugin-1.4.0.tgz",
+ "integrity": "sha512-Ym9Qm5Sw9oXJYChNJk09I/yaXDaV3UDxsa07wcCvILzIeSJTnSUZjhS4y2YkULzgE8VHOv9X04KtlJPZGwXqMg==",
+ "dev": true,
+ "requires": {
+ "md5": "2.2.1",
+ "uglify-js": "2.8.29"
+ },
+ "dependencies": {
+ "camelcase": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz",
+ "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=",
+ "dev": true
+ },
+ "cliui": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz",
+ "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=",
+ "dev": true,
+ "requires": {
+ "center-align": "0.1.3",
+ "right-align": "0.1.3",
+ "wordwrap": "0.0.2"
+ }
+ },
+ "uglify-js": {
+ "version": "2.8.29",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
+ "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=",
+ "dev": true,
+ "requires": {
+ "source-map": "0.5.7",
+ "uglify-to-browserify": "1.0.2",
+ "yargs": "3.10.0"
+ }
+ },
+ "wordwrap": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz",
+ "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=",
+ "dev": true
+ },
+ "yargs": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
+ "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=",
+ "dev": true,
+ "requires": {
+ "camelcase": "1.2.1",
+ "cliui": "2.1.0",
+ "decamelize": "1.2.0",
+ "window-size": "0.1.0"
}
}
}
@@ -11425,462 +11424,177 @@
}
},
"webpack-dev-server": {
- "version": "2.11.2",
- "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-2.11.2.tgz",
- "integrity": "sha512-zrPoX97bx47vZiAXfDrkw8pe9QjJ+lunQl3dypojyWwWr1M5I2h0VSrMPfTjopHQPRNn+NqfjcMmhoLcUJe2gA==",
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-2.7.1.tgz",
+ "integrity": "sha1-IVgPWgjNBlxxFEz29hw0W8pZqLg=",
"dev": true,
"requires": {
"ansi-html": "0.0.7",
- "array-includes": "3.0.3",
"bonjour": "3.5.0",
- "chokidar": "2.0.4",
+ "chokidar": "1.7.0",
"compression": "1.7.2",
"connect-history-api-fallback": "1.5.0",
- "debug": "3.1.0",
"del": "3.0.0",
"express": "4.16.3",
"html-entities": "1.2.1",
"http-proxy-middleware": "0.17.4",
- "import-local": "1.0.0",
"internal-ip": "1.2.0",
"ip": "1.1.5",
- "killable": "1.0.0",
"loglevel": "1.6.1",
- "opn": "5.1.0",
+ "opn": "4.0.2",
"portfinder": "1.0.13",
"selfsigned": "1.10.3",
"serve-index": "1.9.1",
- "sockjs": "0.3.19",
+ "sockjs": "0.3.18",
"sockjs-client": "1.1.4",
"spdy": "3.4.7",
"strip-ansi": "3.0.1",
- "supports-color": "5.4.0",
+ "supports-color": "3.2.3",
"webpack-dev-middleware": "1.12.2",
"yargs": "6.6.0"
},
"dependencies": {
"anymatch": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
- "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz",
+ "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==",
"dev": true,
"requires": {
- "micromatch": "3.1.10",
+ "micromatch": "2.3.11",
"normalize-path": "2.1.1"
}
},
"arr-diff": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
- "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
- "dev": true
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
+ "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
+ "dev": true,
+ "requires": {
+ "arr-flatten": "1.1.0"
+ }
},
"array-unique": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
- "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
+ "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
"dev": true
},
"braces": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
- "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "version": "1.8.5",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz",
+ "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=",
"dev": true,
"requires": {
- "arr-flatten": "1.1.0",
- "array-unique": "0.3.2",
- "extend-shallow": "2.0.1",
- "fill-range": "4.0.0",
- "isobject": "3.0.1",
- "repeat-element": "1.1.2",
- "snapdragon": "0.8.2",
- "snapdragon-node": "2.1.1",
- "split-string": "3.1.0",
- "to-regex": "3.0.2"
- },
- "dependencies": {
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "0.1.1"
- }
- }
+ "expand-range": "1.8.2",
+ "preserve": "0.2.0",
+ "repeat-element": "1.1.2"
}
},
- "camelcase": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
- "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
- "dev": true
- },
"chokidar": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz",
- "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
+ "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=",
"dev": true,
"requires": {
- "anymatch": "2.0.0",
+ "anymatch": "1.3.2",
"async-each": "1.0.1",
- "braces": "2.3.2",
"fsevents": "1.2.4",
- "glob-parent": "3.1.0",
+ "glob-parent": "2.0.0",
"inherits": "2.0.3",
"is-binary-path": "1.0.1",
- "is-glob": "4.0.0",
- "lodash.debounce": "4.0.8",
- "normalize-path": "2.1.1",
+ "is-glob": "2.0.1",
"path-is-absolute": "1.0.1",
- "readdirp": "2.1.0",
- "upath": "1.1.0"
- }
- },
- "debug": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
- "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
+ "readdirp": "2.1.0"
}
},
"expand-brackets": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
- "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz",
+ "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=",
"dev": true,
"requires": {
- "debug": "2.6.9",
- "define-property": "0.2.5",
- "extend-shallow": "2.0.1",
- "posix-character-classes": "0.1.1",
- "regex-not": "1.0.2",
- "snapdragon": "0.8.2",
- "to-regex": "3.0.2"
- },
- "dependencies": {
- "debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
- }
- },
- "define-property": {
- "version": "0.2.5",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
- "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
- "dev": true,
- "requires": {
- "is-descriptor": "0.1.6"
- }
- },
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "0.1.1"
- }
- },
- "is-accessor-descriptor": {
- "version": "0.1.6",
- "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
- "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
- "dev": true,
- "requires": {
- "kind-of": "3.2.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "1.1.6"
- }
- }
- }
- },
- "is-data-descriptor": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
- "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
- "dev": true,
- "requires": {
- "kind-of": "3.2.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "1.1.6"
- }
- }
- }
- },
- "is-descriptor": {
- "version": "0.1.6",
- "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
- "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
- "dev": true,
- "requires": {
- "is-accessor-descriptor": "0.1.6",
- "is-data-descriptor": "0.1.4",
- "kind-of": "5.1.0"
- }
- },
- "kind-of": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
- "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
- "dev": true
- }
+ "is-posix-bracket": "0.1.1"
}
},
"extglob": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
- "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
+ "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=",
"dev": true,
"requires": {
- "array-unique": "0.3.2",
- "define-property": "1.0.0",
- "expand-brackets": "2.1.4",
- "extend-shallow": "2.0.1",
- "fragment-cache": "0.2.1",
- "regex-not": "1.0.2",
- "snapdragon": "0.8.2",
- "to-regex": "3.0.2"
- },
- "dependencies": {
- "define-property": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
- "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
- "dev": true,
- "requires": {
- "is-descriptor": "1.0.2"
- }
- },
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "0.1.1"
- }
- }
- }
- },
- "fill-range": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
- "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
- "dev": true,
- "requires": {
- "extend-shallow": "2.0.1",
- "is-number": "3.0.0",
- "repeat-string": "1.6.1",
- "to-regex-range": "2.1.1"
- },
- "dependencies": {
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "0.1.1"
- }
- }
+ "is-extglob": "1.0.0"
}
},
"glob-parent": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
- "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
+ "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
"dev": true,
"requires": {
- "is-glob": "3.1.0",
- "path-dirname": "1.0.2"
- },
- "dependencies": {
- "is-glob": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
- "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
- "dev": true,
- "requires": {
- "is-extglob": "2.1.1"
- }
- }
- }
- },
- "has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
- "dev": true
- },
- "is-accessor-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
- "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
- "dev": true,
- "requires": {
- "kind-of": "6.0.2"
- }
- },
- "is-data-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
- "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
- "dev": true,
- "requires": {
- "kind-of": "6.0.2"
- }
- },
- "is-descriptor": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
- "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
- "dev": true,
- "requires": {
- "is-accessor-descriptor": "1.0.0",
- "is-data-descriptor": "1.0.0",
- "kind-of": "6.0.2"
+ "is-glob": "2.0.1"
}
},
"is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+ "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
"dev": true
},
"is-glob": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
- "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+ "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
"dev": true,
"requires": {
- "is-extglob": "2.1.1"
+ "is-extglob": "1.0.0"
}
},
- "is-number": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
- "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
- "dev": true,
- "requires": {
- "kind-of": "3.2.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "1.1.6"
- }
- }
- }
- },
- "isobject": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
- "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
- "dev": true
- },
"kind-of": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
- "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
- "dev": true
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "1.1.6"
+ }
},
"micromatch": {
- "version": "3.1.10",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
- "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "version": "2.3.11",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
+ "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
"dev": true,
"requires": {
- "arr-diff": "4.0.0",
- "array-unique": "0.3.2",
- "braces": "2.3.2",
- "define-property": "2.0.2",
- "extend-shallow": "3.0.2",
- "extglob": "2.0.4",
- "fragment-cache": "0.2.1",
- "kind-of": "6.0.2",
- "nanomatch": "1.2.9",
- "object.pick": "1.3.0",
- "regex-not": "1.0.2",
- "snapdragon": "0.8.2",
- "to-regex": "3.0.2"
+ "arr-diff": "2.0.0",
+ "array-unique": "0.2.1",
+ "braces": "1.8.5",
+ "expand-brackets": "0.1.5",
+ "extglob": "0.3.2",
+ "filename-regex": "2.0.1",
+ "is-extglob": "1.0.0",
+ "is-glob": "2.0.1",
+ "kind-of": "3.2.2",
+ "normalize-path": "2.1.1",
+ "object.omit": "2.0.1",
+ "parse-glob": "3.0.4",
+ "regex-cache": "0.4.4"
}
},
- "supports-color": {
- "version": "5.4.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
- "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
+ "opn": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/opn/-/opn-4.0.2.tgz",
+ "integrity": "sha1-erwi5kTf9jsKltWrfyeQwPAavJU=",
"dev": true,
"requires": {
- "has-flag": "3.0.0"
- }
- },
- "y18n": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
- "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
- "dev": true
- },
- "yargs": {
- "version": "6.6.0",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz",
- "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=",
- "dev": true,
- "requires": {
- "camelcase": "3.0.0",
- "cliui": "3.2.0",
- "decamelize": "1.2.0",
- "get-caller-file": "1.0.2",
- "os-locale": "1.4.0",
- "read-pkg-up": "1.0.1",
- "require-directory": "2.1.1",
- "require-main-filename": "1.0.1",
- "set-blocking": "2.0.0",
- "string-width": "1.0.2",
- "which-module": "1.0.0",
- "y18n": "3.2.1",
- "yargs-parser": "4.2.1"
- }
- },
- "yargs-parser": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz",
- "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=",
- "dev": true,
- "requires": {
- "camelcase": "3.0.0"
+ "object-assign": "4.1.1",
+ "pinkie-promise": "2.0.1"
}
}
}
},
"webpack-merge": {
- "version": "4.1.3",
- "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.1.3.tgz",
- "integrity": "sha512-zxwAIGK7nKdu5CIZL0BjTQoq3elV0t0MfB7rUC1zj668geid52abs6hN/ACwZdK6LeMS8dC9B6WmtF978zH5mg==",
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.1.2.tgz",
+ "integrity": "sha512-/0QYwW/H1N/CdXYA2PNPVbsxO3u2Fpz34vs72xm03SRfg6bMNGfMJIQEpQjKRvkG2JvT6oRJFpDtSrwbX8Jzvw==",
"dev": true,
"requires": {
"lodash": "4.17.10"
@@ -11904,15 +11618,6 @@
}
}
},
- "webpack-subresource-integrity": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-1.0.4.tgz",
- "integrity": "sha1-j6yKfo61n8ahZ2ioXJ2U7n+dDts=",
- "dev": true,
- "requires": {
- "webpack-core": "0.6.9"
- }
- },
"websocket-driver": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz",
@@ -11935,10 +11640,16 @@
"integrity": "sha1-RztRfsFZ4rhQBUl6E5g/CVQS404=",
"dev": true
},
+ "whet.extend": {
+ "version": "0.9.9",
+ "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz",
+ "integrity": "sha1-+HfVv2SMl+WqVC+twW1qJZucEaE=",
+ "dev": true
+ },
"which": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
- "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz",
+ "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==",
"dev": true,
"requires": {
"isexe": "2.0.0"
@@ -11947,13 +11658,12 @@
"which-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz",
- "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=",
- "dev": true
+ "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8="
},
"wide-align": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
- "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz",
+ "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==",
"dev": true,
"requires": {
"string-width": "1.0.2"
@@ -11965,33 +11675,16 @@
"integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=",
"dev": true
},
- "with-callback": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/with-callback/-/with-callback-1.0.2.tgz",
- "integrity": "sha1-oJYpuakgAo1yFAT7Q1vc/1yRvCE=",
- "dev": true,
- "optional": true
- },
"wordwrap": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz",
- "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=",
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
+ "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
"dev": true
},
- "worker-farm": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz",
- "integrity": "sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==",
- "dev": true,
- "requires": {
- "errno": "0.1.7"
- }
- },
"wrap-ansi": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
"integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
- "dev": true,
"requires": {
"string-width": "1.0.2",
"strip-ansi": "3.0.1"
@@ -12004,16 +11697,26 @@
"dev": true
},
"ws": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
- "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.1.tgz",
+ "integrity": "sha1-CC3bbGQehdS7RR8D1S8G6r2x8Bg=",
"dev": true,
"requires": {
- "async-limiter": "1.0.0",
- "safe-buffer": "5.1.2",
- "ultron": "1.1.1"
+ "options": "0.0.6",
+ "ultron": "1.0.2"
}
},
+ "wtf-8": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/wtf-8/-/wtf-8-1.0.0.tgz",
+ "integrity": "sha1-OS2LotDxw00e4tYw8V0O+2jhBIo=",
+ "dev": true
+ },
+ "xhr2": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.1.4.tgz",
+ "integrity": "sha1-f4dliEdxbbUCYyOBL4GMras4el8="
+ },
"xml2js": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
@@ -12022,14 +11725,6 @@
"requires": {
"sax": "1.2.4",
"xmlbuilder": "9.0.7"
- },
- "dependencies": {
- "sax": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
- "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
- "dev": true
- }
}
},
"xmlbuilder": {
@@ -12038,18 +11733,17 @@
"integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=",
"dev": true
},
- "xmlhttprequest-ssl": {
- "version": "1.5.5",
- "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz",
- "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=",
+ "xmldom": {
+ "version": "0.1.27",
+ "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz",
+ "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=",
"dev": true
},
- "xregexp": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz",
- "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=",
- "dev": true,
- "optional": true
+ "xmlhttprequest-ssl": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz",
+ "integrity": "sha1-GFqIjATspGw+QHDZn3tJ3jUomS0=",
+ "dev": true
},
"xtend": {
"version": "4.0.1",
@@ -12057,20 +11751,10 @@
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
"dev": true
},
- "xxhashjs": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz",
- "integrity": "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==",
- "dev": true,
- "requires": {
- "cuint": "0.2.2"
- }
- },
"y18n": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
- "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
- "dev": true
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
+ "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE="
},
"yallist": {
"version": "2.1.2",
@@ -12079,11 +11763,9 @@
"dev": true
},
"yargs": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz",
- "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=",
- "dev": true,
- "optional": true,
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz",
+ "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=",
"requires": {
"camelcase": "3.0.0",
"cliui": "3.2.0",
@@ -12097,42 +11779,24 @@
"string-width": "1.0.2",
"which-module": "1.0.0",
"y18n": "3.2.1",
- "yargs-parser": "5.0.0"
- },
- "dependencies": {
- "camelcase": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
- "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
- "dev": true,
- "optional": true
- },
- "y18n": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
- "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
- "dev": true,
- "optional": true
- }
+ "yargs-parser": "4.2.1"
}
},
"yargs-parser": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz",
- "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=",
- "dev": true,
- "optional": true,
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz",
+ "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=",
"requires": {
"camelcase": "3.0.0"
- },
- "dependencies": {
- "camelcase": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
- "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
- "dev": true,
- "optional": true
- }
+ }
+ },
+ "yauzl": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz",
+ "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=",
+ "dev": true,
+ "requires": {
+ "fd-slicer": "1.0.1"
}
},
"yeast": {
@@ -12148,9 +11812,9 @@
"dev": true
},
"zone.js": {
- "version": "0.8.26",
- "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.26.tgz",
- "integrity": "sha512-W9Nj+UmBJG251wkCacIkETgra4QgBo/vgoEkb4a2uoLzpQG7qF9nzwoLXWU5xj3Fg2mxGvEDh47mg24vXccYjA=="
+ "version": "0.8.17",
+ "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.17.tgz",
+ "integrity": "sha1-TF5RhahX2o2nk9rzkZNxxaNrKgs="
}
}
}
diff --git a/package.json b/package.json
index 96fc2b0..5b9a040 100644
--- a/package.json
+++ b/package.json
@@ -1,59 +1,92 @@
{
- "name": "fineract-cn-web-app",
- "version": "0.0.0",
- "license": "MIT",
- "scripts": {
- "ng": "ng",
- "start": "ng serve",
- "build": "ng build --prod",
- "test": "ng test",
- "lint": "ng lint",
- "e2e": "ng e2e"
- },
+ "name": "fims",
+ "version": "0.1.0",
"private": true,
+ "description": "fims backoffice",
+ "keywords": [],
+ "scripts": {
+ "e2e": "protractor",
+ "e2e-test": "protractor ./protractor.conf.js",
+ "tslint": "tslint -c ./tslint.json \"./src/**/*.ts\" -e \"./src/**/typings.d.ts\" -e \"./src/environments/**\"",
+ "postinstall": "webdriver-manager update",
+ "test": "ng test --single-run",
+ "karma": "karma start ./karma.conf.js --single-run",
+ "devTest": "ng test",
+ "dev": "ng serve --verbose --proxy-config proxy.conf.json",
+ "runProd": "ng serve --proxy-config proxy.conf.json --prod",
+ "build": "ng build --prod",
+ "checkLicenses": "license-to-fail ./license.config.js",
+ "bundle-report": "webpack-bundle-analyzer dist/stats.json"
+ },
+ "engines": {
+ "node": ">4.4 < 7",
+ "npm": ">3"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/apache/fineract-cn-fims-web-app.git"
+ },
+ "license": "Apache-2.0",
+ "author": "Mark van Veen",
+ "contributors": [
+ "Mark van Veen <mvanveen@mifos.org>",
+ "Myrle Krantz <myrle@apache.org>",
+ "Pembe Miriam <pembemiriam007@gmail.com"
+ ],
"dependencies": {
- "@angular/animations": "^5.2.9",
- "@angular/cdk": "^5.2.4",
- "@angular/common": "^5.2.0",
- "@angular/compiler": "^5.2.0",
- "@angular/core": "^5.2.0",
- "@angular/forms": "^5.2.0",
- "@angular/http": "^5.2.0",
- "@angular/material": "^5.2.4",
- "@angular/platform-browser": "^5.2.0",
- "@angular/platform-browser-dynamic": "^5.2.0",
- "@angular/router": "^5.2.0",
- "@covalent/core": "^1.0.1",
- "@ngrx/effects": "^5.2.0",
- "@ngrx/store": "^5.2.0",
- "@ngx-translate/core": "^10.0.2",
+ "@angular/animations": "4.4.5",
+ "@angular/cdk": "2.0.0-beta.12",
+ "@angular/common": "4.4.5",
+ "@angular/compiler": "4.4.5",
+ "@angular/core": "4.4.5",
+ "@angular/forms": "4.4.5",
+ "@angular/http": "4.4.5",
+ "@angular/material": "2.0.0-beta.12",
+ "@angular/platform-browser": "4.4.5",
+ "@angular/platform-browser-dynamic": "4.4.5",
+ "@angular/platform-server": "4.4.5",
+ "@angular/router": "4.4.5",
+ "@covalent/core": "1.0.0-beta.8-1",
+ "@ngrx/core": "1.2.0",
+ "@ngrx/effects": "2.0.3",
+ "@ngrx/store": "2.2.2",
+ "@ngrx/store-devtools": "3.2.4",
+ "@ngx-translate/core": "7.0.0",
"@ngx-translate/http-loader": "0.1.0",
- "core-js": "^2.4.1",
- "hammerjs": "^2.0.8",
- "material-design-icons": "^3.0.1",
- "ngrx-store-localstorage": "^5.0.0",
- "reselect": "^3.0.1",
- "rxjs": "^5.5.6",
- "zone.js": "^0.8.19"
+ "angular2-text-mask": "^8.0.2",
+ "core-js": "2.4.1",
+ "hammerjs": "2.0.8",
+ "highlight.js": "9.10.0",
+ "ngrx-store-localstorage": "0.1.8",
+ "reselect": "2.5.4",
+ "rxjs": "5.4.1",
+ "showdown": "1.6.4",
+ "text-mask-addons": "^3.6.0",
+ "zone.js": "0.8.17"
},
"devDependencies": {
- "@angular/cli": "~1.7.2",
- "@angular/compiler-cli": "^5.2.0",
- "@angular/language-service": "^5.2.0",
- "@types/jasmine": "~2.8.3",
- "@types/jasminewd2": "~2.0.2",
- "@types/node": "~6.0.60",
- "codelyzer": "^4.0.1",
- "jasmine-core": "~2.8.0",
- "jasmine-spec-reporter": "~4.2.1",
- "karma": "~2.0.0",
- "karma-chrome-launcher": "~2.2.0",
- "karma-coverage-istanbul-reporter": "^1.2.1",
- "karma-jasmine": "~1.1.0",
- "karma-jasmine-html-reporter": "^0.2.2",
- "protractor": "~5.1.2",
- "ts-node": "~4.1.0",
- "tslint": "~5.9.1",
- "typescript": "~2.5.3"
+ "@angular/cli": "1.4.7",
+ "@angular/compiler-cli": "4.4.5",
+ "@types/hammerjs": "2.0.30",
+ "@types/jasmine": "2.5.38",
+ "@types/node": "6.0.78",
+ "@types/selenium-webdriver": "2.53.36",
+ "codelyzer": "3.0.0",
+ "jasmine-core": "2.5.2",
+ "jasmine-spec-reporter": "3.2.0",
+ "karma": "1.4.1",
+ "karma-chrome-launcher": "2.0.0",
+ "karma-cli": "1.0.1",
+ "karma-coverage-istanbul-reporter": "0.2.2",
+ "karma-firefox-launcher": "1.0.0",
+ "karma-jasmine": "1.1.0",
+ "karma-jasmine-html-reporter": "0.2.2",
+ "license-to-fail": "2.2.0",
+ "phantomjs-prebuilt": "2.1.7",
+ "protractor": "5.1.0",
+ "ts-node": "3.0.6",
+ "tslint": "5.2.0",
+ "typescript": "2.3.4",
+ "webpack-bundle-analyzer": "^2.3.1"
}
}
diff --git a/protractor.conf.js b/protractor.conf.js
index 584292a..3a3ffdb 100644
--- a/protractor.conf.js
+++ b/protractor.conf.js
@@ -1,28 +1,49 @@
-// Protractor configuration file, see link for more information
-// https://github.com/angular/protractor/blob/master/lib/config.ts
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */// Protractor configuration file, see link for more information
+// https://github.com/angular/protractor/blob/master/docs/referenceConf.js
-const { SpecReporter } = require('jasmine-spec-reporter');
+/*global jasmine */
+var SpecReporter = require('jasmine-spec-reporter');
exports.config = {
allScriptsTimeout: 11000,
specs: [
- './e2e/**/*.e2e-spec.ts'
+ './e2e/**/*.e2e.ts'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
- baseUrl: 'http://163.172.130.175:8888/',
+ baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
- onPrepare() {
+ useAllAngular2AppRoots: true,
+ beforeLaunch: function() {
require('ts-node').register({
- project: 'e2e/tsconfig.e2e.json'
+ project: 'e2e'
});
- jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
+ },
+ onPrepare: function() {
+ jasmine.getEnv().addReporter(new SpecReporter());
}
};
diff --git a/proxy.conf.json b/proxy.conf.json
index a5b96e0..45b82ad 100644
--- a/proxy.conf.json
+++ b/proxy.conf.json
@@ -1,69 +1,69 @@
{
"/identity": {
- "target": "http://163.172.130.175:2021",
+ "target": "http://51.15.140.45:2021",
"secure": false,
"pathRewrite": {
"/identity": "/identity"
}
},
"/api/office": {
- "target": "http://163.172.130.175:2023",
+ "target": "http://51.15.140.45:2023",
"secure": false,
"pathRewrite": {
"^/api/office": "/office"
}
},
"/api/customer": {
- "target": "http://163.172.130.175:2024",
+ "target": "http://51.15.140.45:2024",
"secure": false,
"pathRewrite": {
"^/api/customer": "/customer"
}
},
"/api/accounting": {
- "target": "http://163.172.130.175:2025",
+ "target": "http://51.15.140.45:2025",
"secure": false,
"pathRewrite": {
"^/api/accounting": "/accounting"
}
},
"/api/portfolio": {
- "target": "http://163.172.130.175:2026",
+ "target": "http://51.15.140.45:2026",
"secure": false,
"pathRewrite": {
"^/api/portfolio": "/portfolio"
}
},
"/api/deposit": {
- "target": "http://163.172.130.175:2027",
+ "target": "http://51.15.140.45:2027",
"secure": false,
"pathRewrite": {
"^/api/deposit": "/deposit"
}
},
"/api/teller": {
- "target": "http://163.172.130.175:2028",
+ "target": "http://51.15.140.45:2028",
"secure": false,
"pathRewrite": {
"^/api/teller": "/teller"
}
},
"/api/reporting": {
- "target": "http://163.172.130.175:2029",
+ "target": "http://51.15.140.45:2029",
"secure": false,
"pathRewrite": {
"^/api/reporting": "/reporting"
}
},
"/api/cheques": {
- "target": "http://163.172.130.175:2030",
+ "target": "http://51.15.140.45:2030",
"secure": false,
"pathRewrite": {
"^/api/cheques": "/cheques"
}
},
"/api/payroll": {
- "target": "http://163.172.130.175:2031",
+ "target": "http://51.15.140.45:2031",
"secure": false,
"pathRewrite": {
"^/api/payroll": "/payroll"
diff --git a/scripts/ghpages-deploy b/scripts/ghpages-deploy
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/scripts/ghpages-deploy
diff --git a/scripts/license/HEADER_HTML b/scripts/license/HEADER_HTML
new file mode 100644
index 0000000..96b8abc
--- /dev/null
+++ b/scripts/license/HEADER_HTML
@@ -0,0 +1,16 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
diff --git a/scripts/license/HEADER_SCSS b/scripts/license/HEADER_SCSS
new file mode 100644
index 0000000..7243427
--- /dev/null
+++ b/scripts/license/HEADER_SCSS
@@ -0,0 +1,18 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
diff --git a/scripts/license/HEADER_TS b/scripts/license/HEADER_TS
new file mode 100644
index 0000000..51da6c0
--- /dev/null
+++ b/scripts/license/HEADER_TS
@@ -0,0 +1,18 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
diff --git a/scripts/license/README.md b/scripts/license/README.md
new file mode 100644
index 0000000..953717d
--- /dev/null
+++ b/scripts/license/README.md
@@ -0,0 +1,9 @@
+# License header generation
+
+This script prepends the Apache License Header to all ts, html and scss files in the src folder.
+
+## Setup
+
+* Install gulp `npm i -g gulp`
+* Install gulp-header-license `npm i gulp-header-license`
+* Run default task `gulp`
diff --git a/scripts/license/gulpfile.js b/scripts/license/gulpfile.js
new file mode 100644
index 0000000..3b2071a
--- /dev/null
+++ b/scripts/license/gulpfile.js
@@ -0,0 +1,24 @@
+var gulp = require('gulp');
+var license = require('gulp-header-license');
+var fs = require('fs');
+
+gulp.task('licenseTS', function () {
+ gulp.src('../../src/**/*.ts')
+ .pipe(license(fs.readFileSync('HEADER_TS', 'utf8')))
+ .pipe(gulp.dest('../../src/'));
+});
+
+gulp.task('licenseHTML', function () {
+ gulp.src('../../src/**/*.html')
+ .pipe(license(fs.readFileSync('HEADER_HTML', 'utf8')))
+ .pipe(gulp.dest('../../src/'));
+});
+
+gulp.task('licenseSCSS', function () {
+ gulp.src('../../src/**/*.scss')
+ .pipe(license(fs.readFileSync('HEADER_SCSS', 'utf8')))
+ .pipe(gulp.dest('../../src/'));
+});
+
+gulp.task('default', ['licenseTS', 'licenseHTML', 'licenseSCSS']);
+
diff --git a/scripts/nginx/README.md b/scripts/nginx/README.md
new file mode 100644
index 0000000..403edf5
--- /dev/null
+++ b/scripts/nginx/README.md
@@ -0,0 +1,15 @@
+# nginx configuration
+
+Config to use nginx for hosting fims.
+
+This config assumes that all needed services are started.
+
+## Setup
+
+* Install nginx
+* Run `npm run build` from root folder
+* Copy all contents of /dist into your nginx root folder under /fims
+* Copy/Overwrite nginx.conf into /conf of your nginx root folder
+* Change proxy_pass configuration of each service to point to the right url and port
+* Start nginx with `nginx`
+* Navigate with your browser to `http://localhost:8888`
diff --git a/scripts/nginx/nginx.conf b/scripts/nginx/nginx.conf
new file mode 100644
index 0000000..c348ce5
--- /dev/null
+++ b/scripts/nginx/nginx.conf
@@ -0,0 +1,124 @@
+events {
+ worker_connections 4096; ## Default: 1024
+}
+
+http {
+
+ server {
+ listen 8888;
+ server_name localhost;
+ large_client_header_buffers 4 32k;
+
+ root fims;
+ index index.html index.htm;
+ include mime.types;
+
+ location / {
+ try_files $uri /index.html;
+ }
+
+ # /api will server your proxied API that is running on same machine different port
+ # or another machine. So you can protect your API endpoint not get hit by public directly
+ location /identity/ {
+ proxy_pass http://localhost:2021;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_set_header Host $host;
+ proxy_cache_bypass $http_upgrade;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ location /api/office/ {
+ proxy_pass http://localhost:2022/office/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_set_header Host $host;
+ proxy_cache_bypass $http_upgrade;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ location /api/customer/ {
+ proxy_pass http://localhost:2023/customer/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_set_header Host $host;
+ proxy_cache_bypass $http_upgrade;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ location /api/accounting/ {
+ proxy_pass http://localhost:2024/accounting/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_set_header Host $host;
+ proxy_cache_bypass $http_upgrade;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ location /api/portfolio/ {
+ proxy_pass http://localhost:2025/portfolio/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_set_header Host $host;
+ proxy_cache_bypass $http_upgrade;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ location /api/deposit/ {
+ proxy_pass http://localhost:2026/deposit/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_set_header Host $host;
+ proxy_cache_bypass $http_upgrade;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ location /api/teller/ {
+ proxy_pass http://localhost:2027/teller/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_set_header Host $host;
+ proxy_cache_bypass $http_upgrade;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ location /api/reporting/ {
+ proxy_pass http://localhost:2028/reporting/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_set_header Host $host;
+ proxy_cache_bypass $http_upgrade;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ #Static File Caching. All static files with the following extension will be cached for 1 day
+ location ~* .(jpg|jpeg|png|gif|ico|css|js)$ {
+ expires 1d;
+ }
+
+ gzip on;
+ gzip_http_version 1.1;
+ gzip_disable "MSIE [1-6]\.";
+ gzip_min_length 1100;
+ gzip_vary on;
+ gzip_proxied expired no-cache no-store private auth;
+ gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
+ gzip_comp_level 9;
+ }
+}
diff --git a/src/app/accounting/account-types.model.ts b/src/app/accounting/account-types.model.ts
new file mode 100644
index 0000000..973cabf
--- /dev/null
+++ b/src/app/accounting/account-types.model.ts
@@ -0,0 +1,32 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {AccountType} from '../services/accounting/domain/account-type.model';
+
+export interface AccountTypeOption {
+ type: AccountType;
+ label: string;
+}
+
+export const accountTypes: AccountTypeOption[] = [
+ { type: 'ASSET', label: 'Asset' },
+ { type: 'LIABILITY', label: 'Liability' },
+ { type: 'EQUITY', label: 'Equity' },
+ { type: 'EXPENSE', label: 'Expense' },
+ { type: 'REVENUE', label: 'Revenue' }
+];
diff --git a/src/app/accounting/accounting.module.ts b/src/app/accounting/accounting.module.ts
new file mode 100644
index 0000000..6a9ad10
--- /dev/null
+++ b/src/app/accounting/accounting.module.ts
@@ -0,0 +1,188 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {FimsSharedModule} from '../common/common.module';
+import {AccountingRoutes} from './accounting.routing';
+import {RouterModule} from '@angular/router';
+import {NgModule} from '@angular/core';
+import {GeneralLedgerComponent} from './general-ledger.component';
+import {TrailBalanceComponent} from './trailBalance/trial-balance.component';
+import {SubLedgerDetailComponent} from './subLedger/sub-ledger.detail.component';
+import {AccountDetailComponent} from './accounts/account.detail.component';
+import {AccountStatusComponent} from './status/status.component';
+import {AccountActivityComponent} from './activity/activity.component';
+import {CommandsResolver} from './activity/commands.resolver';
+import {AccountFormComponent} from './accounts/form/form.component';
+import {CreateAccountFormComponent} from './accounts/form/create/create.form.component';
+import {SubLedgerComponent} from './subLedger/sub-ledger.component';
+import {EditAccountFormComponent} from './accounts/form/edit/edit.form.component';
+import {JournalEntryListComponent} from './journalEntries/journal-entry.list.component';
+import {JournalEntryFormComponent} from './journalEntries/form/form.component';
+import {AccountEntryListComponent} from './accounts/entries/account-entry.list.component';
+import {LedgerFormComponent} from './form/form.component';
+import {EditLedgerFormComponent} from './form/edit/edit.form.component';
+import {CreateLedgerFormComponent} from './form/create/create.form.component';
+import {LedgerExistsGuard} from './ledger-exists.guard';
+import {AccountExistsGuard} from './accounts/account-exists.guard';
+import {AccountingStore, accountingStoreFactory} from './store/index';
+import {Store} from '@ngrx/store';
+import {AccountCommandNotificationEffects} from './store/account/task/effects/notification.effects';
+import {EffectsModule} from '@ngrx/effects';
+import {AccountCommandApiEffects} from './store/account/task/effects/service.effects';
+import {AccountNotificationEffects} from './store/account/effects/notification.effects';
+import {AccountEntryApiEffects} from './store/account/entries/effects/service.effect';
+import {AccountRouteEffects} from './store/account/effects/route.effects';
+import {AccountApiEffects} from './store/account/effects/service.effects';
+import {JournalEntryNotificationEffects} from './store/ledger/journal-entry/effects/notification.effects';
+import {JournalEntryRouteEffects} from './store/ledger/journal-entry/effects/route.effects';
+import {JournalEntryApiEffects} from './store/ledger/journal-entry/effects/service.effects';
+import {LedgerNotificationEffects} from './store/ledger/effects/notification.effects';
+import {LedgerRouteEffects} from './store/ledger/effects/route.effects';
+import {LedgerApiEffects} from './store/ledger/effects/service.effects';
+import {AccountCommandRouteEffects} from './store/account/task/effects/route.effects';
+import {ChartOfAccountComponent} from './chartOfAccounts/chart-of-accounts.component';
+import {ChartOfAccountTableComponent} from './chartOfAccounts/chart-of-account-table.component';
+import {SubLedgerListComponent} from './subLedger/sub-ledger.list.component';
+import {TranslateModule} from '@ngx-translate/core';
+import {
+ MatAutocompleteModule,
+ MatButtonModule,
+ MatCardModule,
+ MatCheckboxModule,
+ MatIconModule,
+ MatInputModule,
+ MatListModule,
+ MatOptionModule,
+ MatRadioModule,
+ MatToolbarModule
+} from '@angular/material';
+import {CommonModule} from '@angular/common';
+import {CovalentDataTableModule, CovalentStepsModule} from '@covalent/core';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {TransactionTypeListComponent} from './transactionTypes/transaction-types.list.component';
+import {TransactionTypeApiEffects} from './store/ledger/transaction-type/effects/service.effects';
+import {TransactionTypeRouteEffects} from './store/ledger/transaction-type/effects/route.effects';
+import {TransactionTypeNotificationEffects} from './store/ledger/transaction-type/effects/notification.effects';
+import {TransactionTypeFormComponent} from './transactionTypes/form/transaction-type-form.component';
+import {CreateTransactionTypeFormComponent} from './transactionTypes/form/create/create.form.component';
+import {EditTransactionTypeFormComponent} from './transactionTypes/form/edit/edit.form.component';
+import {TransactionTypeExistsGuard} from './transactionTypes/transaction-type-exists.guard';
+import {TransactionTypeSelectComponent} from './journalEntries/form/transaction-type-select/transaction-type-select.component';
+import {ChequeApiEffects} from './store/cheques/effects/service.effects';
+import {ChequesListComponent} from './cheques/cheques.list.component';
+import {PayrollCollectionApiEffects} from './store/payroll/effects/service.effects';
+import {PayrollListComponent} from './payroll/payroll.list.component';
+import {CreatePayrollFormComponent} from './payroll/form/create.form.component';
+import {PayrollFormComponent} from './payroll/form/form.component';
+import {PayrollCollectionRouteEffects} from './store/payroll/effects/route.effects';
+import {PayrollCollectionNotificationEffects} from './store/payroll/effects/notification.effects';
+import {PaymentsListComponent} from './payroll/payments.list.component';
+import {CreateJournalEntryFormComponent} from './journalEntries/form/create.form.component';
+import {IncomeStatementComponent} from './incomeStatement/income-statement.component';
+import {FinancialConditionComponent} from './financialCondition/financial-condition.component';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(AccountingRoutes),
+ FimsSharedModule,
+ TranslateModule,
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ MatCardModule,
+ MatIconModule,
+ MatListModule,
+ MatToolbarModule,
+ MatInputModule,
+ MatButtonModule,
+ MatRadioModule,
+ MatCheckboxModule,
+ MatAutocompleteModule,
+ MatOptionModule,
+ CovalentDataTableModule,
+ CovalentStepsModule,
+
+ EffectsModule.run(LedgerApiEffects),
+ EffectsModule.run(LedgerRouteEffects),
+ EffectsModule.run(LedgerNotificationEffects),
+
+ EffectsModule.run(JournalEntryApiEffects),
+ EffectsModule.run(JournalEntryRouteEffects),
+ EffectsModule.run(JournalEntryNotificationEffects),
+
+ EffectsModule.run(TransactionTypeApiEffects),
+ EffectsModule.run(TransactionTypeRouteEffects),
+ EffectsModule.run(TransactionTypeNotificationEffects),
+
+ EffectsModule.run(AccountApiEffects),
+ EffectsModule.run(AccountRouteEffects),
+ EffectsModule.run(AccountNotificationEffects),
+ EffectsModule.run(AccountEntryApiEffects),
+ EffectsModule.run(AccountCommandApiEffects),
+ EffectsModule.run(AccountCommandRouteEffects),
+ EffectsModule.run(AccountCommandNotificationEffects),
+
+ EffectsModule.run(ChequeApiEffects),
+
+ EffectsModule.run(PayrollCollectionApiEffects),
+ EffectsModule.run(PayrollCollectionRouteEffects),
+ EffectsModule.run(PayrollCollectionNotificationEffects)
+ ],
+ declarations: [
+ GeneralLedgerComponent,
+ SubLedgerComponent,
+ SubLedgerListComponent,
+ SubLedgerDetailComponent,
+ LedgerFormComponent,
+ CreateLedgerFormComponent,
+ EditLedgerFormComponent,
+ TrailBalanceComponent,
+ ChartOfAccountComponent,
+ ChartOfAccountTableComponent,
+ AccountEntryListComponent,
+ AccountDetailComponent,
+ AccountStatusComponent,
+ AccountActivityComponent,
+ AccountFormComponent,
+ CreateAccountFormComponent,
+ EditAccountFormComponent,
+ JournalEntryListComponent,
+ CreateJournalEntryFormComponent,
+ JournalEntryFormComponent,
+ TransactionTypeListComponent,
+ TransactionTypeFormComponent,
+ CreateTransactionTypeFormComponent,
+ EditTransactionTypeFormComponent,
+ TransactionTypeSelectComponent,
+ ChequesListComponent,
+ PayrollListComponent,
+ CreatePayrollFormComponent,
+ PayrollFormComponent,
+ PaymentsListComponent,
+ IncomeStatementComponent,
+ FinancialConditionComponent
+ ],
+ providers: [
+ CommandsResolver,
+ LedgerExistsGuard,
+ AccountExistsGuard,
+ TransactionTypeExistsGuard,
+ { provide: AccountingStore, useFactory: accountingStoreFactory, deps: [Store]}
+ ]
+})
+export class AccountingModule {}
diff --git a/src/app/accounting/accounting.routing.ts b/src/app/accounting/accounting.routing.ts
new file mode 100644
index 0000000..1ada363
--- /dev/null
+++ b/src/app/accounting/accounting.routing.ts
@@ -0,0 +1,175 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Routes} from '@angular/router';
+import {GeneralLedgerComponent} from './general-ledger.component';
+import {TrailBalanceComponent} from './trailBalance/trial-balance.component';
+import {SubLedgerDetailComponent} from './subLedger/sub-ledger.detail.component';
+import {AccountDetailComponent} from './accounts/account.detail.component';
+import {AccountStatusComponent} from './status/status.component';
+import {CommandsResolver} from './activity/commands.resolver';
+import {AccountActivityComponent} from './activity/activity.component';
+import {CreateAccountFormComponent} from './accounts/form/create/create.form.component';
+import {SubLedgerComponent} from './subLedger/sub-ledger.component';
+import {EditAccountFormComponent} from './accounts/form/edit/edit.form.component';
+import {JournalEntryListComponent} from './journalEntries/journal-entry.list.component';
+import {AccountEntryListComponent} from './accounts/entries/account-entry.list.component';
+import {CreateLedgerFormComponent} from './form/create/create.form.component';
+import {EditLedgerFormComponent} from './form/edit/edit.form.component';
+import {LedgerExistsGuard} from './ledger-exists.guard';
+import {AccountExistsGuard} from './accounts/account-exists.guard';
+import {ChartOfAccountComponent} from './chartOfAccounts/chart-of-accounts.component';
+import {SubLedgerListComponent} from './subLedger/sub-ledger.list.component';
+import {TransactionTypeListComponent} from './transactionTypes/transaction-types.list.component';
+import {EditTransactionTypeFormComponent} from './transactionTypes/form/edit/edit.form.component';
+import {CreateTransactionTypeFormComponent} from './transactionTypes/form/create/create.form.component';
+import {TransactionTypeExistsGuard} from './transactionTypes/transaction-type-exists.guard';
+import {ChequesListComponent} from './cheques/cheques.list.component';
+import {PayrollListComponent} from './payroll/payroll.list.component';
+import {CreatePayrollFormComponent} from './payroll/form/create.form.component';
+import {PaymentsListComponent} from './payroll/payments.list.component';
+import {CreateJournalEntryFormComponent} from './journalEntries/form/create.form.component';
+import {IncomeStatementComponent} from './incomeStatement/income-statement.component';
+import {FinancialConditionComponent} from './financialCondition/financial-condition.component';
+
+export const AccountingRoutes: Routes = [
+ {path: '', component: GeneralLedgerComponent},
+ {
+ path: 'ledgers/detail/:id',
+ component: SubLedgerComponent,
+ canActivate: [LedgerExistsGuard],
+ data: {
+ hasPermission: { id: 'accounting_ledgers', accessLevel: 'READ' }
+ },
+ children: [
+ {
+ path: '',
+ component: SubLedgerDetailComponent,
+ },
+ {
+ path: 'edit',
+ component: EditLedgerFormComponent,
+ data: { hasPermission: { id: 'accounting_ledgers', accessLevel: 'CHANGE' }}
+ },
+ {
+ path: 'ledgers',
+ component: SubLedgerListComponent,
+ },
+ {
+ path: 'ledgers/edit',
+ component: EditLedgerFormComponent,
+ data: { hasPermission: { id: 'accounting_ledgers', accessLevel: 'CHANGE' }}
+ },
+ {
+ path: 'ledgers/create',
+ component: CreateLedgerFormComponent,
+ data: { hasPermission: { id: 'accounting_ledgers', accessLevel: 'CHANGE' }}
+ },
+ {
+ path: 'accounts/create',
+ component: CreateAccountFormComponent,
+ data: {
+ hasPermission: {id: 'accounting_accounts', accessLevel: 'CHANGE'}
+ }
+ }
+ ]
+ },
+ {
+ path: 'create',
+ component: CreateLedgerFormComponent,
+ data: {hasPermission: {id: 'accounting_ledgers', accessLevel: 'CHANGE'}}
+ },
+ {
+ path: 'accounts/detail/:id',
+ component: AccountDetailComponent,
+ canActivate: [AccountExistsGuard],
+ data: {hasPermission: {id: 'accounting_accounts', accessLevel: 'READ'}}
+ },
+ {
+ path: 'accounts/detail/:id/edit',
+ component: EditAccountFormComponent,
+ canActivate: [AccountExistsGuard],
+ data: {hasPermission: {id: 'accounting_accounts', accessLevel: 'CHANGE'}}
+ },
+ {
+ path: 'accounts/detail/:id/tasks',
+ component: AccountStatusComponent,
+ canActivate: [AccountExistsGuard],
+ data: {hasPermission: {id: 'accounting_accounts', accessLevel: 'READ'}}
+ },
+ {
+ path: 'accounts/detail/:id/activities',
+ component: AccountActivityComponent,
+ canActivate: [AccountExistsGuard],
+ resolve: {commands: CommandsResolver},
+ data: {hasPermission: {id: 'accounting_accounts', accessLevel: 'READ'}}
+ },
+ {
+ path: 'accounts/detail/:id/entries',
+ component: AccountEntryListComponent,
+ canActivate: [AccountExistsGuard],
+ data: {hasPermission: {id: 'accounting_accounts', accessLevel: 'READ'}}
+ },
+
+ {path: 'trialBalance', component: TrailBalanceComponent, data: {hasPermission: {id: 'accounting_ledgers', accessLevel: 'READ'}}},
+ {
+ path: 'incomeStatement',
+ component: IncomeStatementComponent,
+ data: {hasPermission: {id: 'accounting_income_statement', accessLevel: 'READ'}}
+ },
+ {
+ path: 'financialCondition',
+ component: FinancialConditionComponent,
+ data: {hasPermission: {id: 'accounting_fin_condition', accessLevel: 'READ'}}
+ },
+ {
+ path: 'transactiontypes',
+ component: TransactionTypeListComponent,
+ data: {hasPermission: {id: 'accounting_tx_types', accessLevel: 'READ'}}
+ },
+ {
+ path: 'transactiontypes/create',
+ component: CreateTransactionTypeFormComponent,
+ data: {hasPermission: {id: 'accounting_tx_types', accessLevel: 'CHANGE'}}
+ },
+ {
+ path: 'transactiontypes/edit/:code',
+ component: EditTransactionTypeFormComponent,
+ canActivate: [TransactionTypeExistsGuard],
+ data: {hasPermission: {id: 'accounting_tx_types', accessLevel: 'CHANGE'}}
+ },
+ {path: 'chartOfAccounts', component: ChartOfAccountComponent, data: {hasPermission: {id: 'accounting_ledgers', accessLevel: 'READ'}}},
+ {path: 'journalEntries', component: JournalEntryListComponent, data: {hasPermission: {id: 'accounting_journals', accessLevel: 'READ'}}},
+ {
+ path: 'journalEntries/create',
+ component: CreateJournalEntryFormComponent,
+ data: {hasPermission: {id: 'accounting_journals', accessLevel: 'CHANGE'}}
+ },
+ {path: 'cheques', component: ChequesListComponent, data: {hasPermission: {id: 'cheque_management', accessLevel: 'READ'}}},
+ {path: 'payrolls', component: PayrollListComponent, data: {hasPermission: {id: 'payroll_distribution', accessLevel: 'READ'}}},
+ {
+ path: 'payrolls/create',
+ component: CreatePayrollFormComponent,
+ data: {hasPermission: {id: 'payroll_distribution', accessLevel: 'CHANGE'}}
+ },
+ {
+ path: 'payrolls/payments/:id',
+ component: PaymentsListComponent,
+ data: {hasPermission: {id: 'payroll_distribution', accessLevel: 'READ'}}
+ }
+];
diff --git a/src/app/accounting/accounts/account-exists.guard.ts b/src/app/accounting/accounts/account-exists.guard.ts
new file mode 100644
index 0000000..c8d48d0
--- /dev/null
+++ b/src/app/accounting/accounts/account-exists.guard.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import {Injectable} from '@angular/core';
+import * as fromAccounting from '../store';
+import {Observable} from 'rxjs/Observable';
+import {of} from 'rxjs/observable/of';
+import {AccountingService} from '../../services/accounting/accounting.service';
+import {LoadAction} from '../store/account/account.actions';
+import {AccountingStore} from '../store/index';
+import {ExistsGuardService} from '../../common/guards/exists-guard';
+
+@Injectable()
+export class AccountExistsGuard implements CanActivate {
+
+ constructor(private store: AccountingStore,
+ private accountingService: AccountingService,
+ private existsGuardService: ExistsGuardService) {}
+
+ hasAccountInStore(id: string): Observable<boolean> {
+ const timestamp$: Observable<number> = this.store.select(fromAccounting.getAccountsLoadedAt)
+ .map(loadedAt => loadedAt[id]);
+
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ hasAccountInApi(id: string): Observable<boolean> {
+ const getAccount$ = this.accountingService.findAccount(id)
+ .map(accountEntity => new LoadAction({
+ resource: accountEntity
+ }))
+ .do((action: LoadAction) => this.store.dispatch(action))
+ .map(employee => !!employee);
+
+ return this.existsGuardService.routeTo404OnError(getAccount$);
+ }
+
+ hasAccount(id: string): Observable<boolean> {
+ return this.hasAccountInStore(id)
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+
+ return this.hasAccountInApi(id);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasAccount(route.params['id']);
+ }
+}
diff --git a/src/app/accounting/accounts/account.detail.component.html b/src/app/accounting/accounts/account.detail.component.html
new file mode 100644
index 0000000..9237ae9
--- /dev/null
+++ b/src/app/accounting/accounts/account.detail.component.html
@@ -0,0 +1,62 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Account with value' | translate:{ value: account.identifier } }}" [subTitle]="account.name" [navigateBackTo]="['../../../ledgers/detail', account.ledger]">
+ <fims-layout-card-over-header-menu>
+ <button mat-icon-button (click)="deleteAccount()" title="{{'Delete this account' | translate}}" *ngIf="canDelete$ | async"><mat-icon>delete</mat-icon></button>
+ </fims-layout-card-over-header-menu>
+ <fims-two-column-layout>
+ <mat-nav-list left>
+ <h3 mat-subheader translate>Management</h3>
+ <a mat-list-item [routerLink]="['./entries']">
+ <mat-icon matListAvatar>assignment</mat-icon>
+ <h3 matLine translate>Account entries</h3>
+ <p matLine translate>Account entries</p>
+ </a>
+ <a mat-list-item [routerLink]="['./tasks']">
+ <mat-icon matListAvatar>playlist_add_check</mat-icon>
+ <h3 matLine translate>Tasks</h3>
+ <p matLine translate>Change the status of this account</p>
+ </a>
+ <a mat-list-item [routerLink]="['./activities']">
+ <mat-icon matListAvatar>event</mat-icon>
+ <h3 matLine translate>Activities</h3>
+ <p matLine translate>Recent activities</p>
+ </a>
+ <a *ngIf="account.referenceAccount" mat-list-item [routerLink]="['../', account.referenceAccount]">
+ <mat-icon matListAvatar>view_module</mat-icon>
+ <h3 matLine translate>Reference account</h3>
+ <p matLine translate>Navigate to reference account</p>
+ </a>
+ </mat-nav-list>
+ <mat-list right>
+ <h3 mat-subheader translate>Current status</h3>
+ <fims-state-display [state]="account.state"></fims-state-display>
+ <mat-list-item>
+ <mat-icon matListAvatar>account_balance</mat-icon>
+ <h3 matLine translate>Type</h3>
+ <p matLine>{{account.type}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <mat-icon matListAvatar>payment</mat-icon>
+ <h3 matLine translate>Balance</h3>
+ <p matLine>{{account.balance | number}}</p>
+ </mat-list-item>
+ </mat-list>
+ </fims-two-column-layout>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Edit account' | translate}}" icon="mode_edit" [link]="['edit']" [permission]="{ id: 'accounting_accounts', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/accounting/accounts/account.detail.component.ts b/src/app/accounting/accounts/account.detail.component.ts
new file mode 100644
index 0000000..693d509
--- /dev/null
+++ b/src/app/accounting/accounts/account.detail.component.ts
@@ -0,0 +1,110 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {Account} from '../../services/accounting/domain/account.model';
+import {ActivatedRoute} from '@angular/router';
+import * as fromAccounting from '../store';
+import * as fromRoot from '../../store';
+import {Subscription} from 'rxjs/Subscription';
+import {AccountingStore} from '../store/index';
+import {DELETE, SelectAction} from '../store/account/account.actions';
+import {Observable} from 'rxjs/Observable';
+import {FimsPermission} from '../../services/security/authz/fims-permission.model';
+import {TranslateService} from '@ngx-translate/core';
+import {TdDialogService} from '@covalent/core';
+
+@Component({
+ templateUrl: './account.detail.component.html'
+})
+export class AccountDetailComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ private accountSubscription: Subscription;
+
+ canDelete$: Observable<boolean>;
+
+ account: Account;
+
+ constructor(private route: ActivatedRoute, private dialogService: TdDialogService, private translate: TranslateService,
+ private store: AccountingStore) {
+ }
+
+ ngOnInit(): void {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectAction(params['id']))
+ .subscribe(this.store);
+
+ const account$: Observable<Account> = this.store.select(fromAccounting.getSelectedAccount)
+ .filter(account => !!account);
+
+ this.canDelete$ = Observable.combineLatest(
+ this.store.select(fromRoot.getPermissions),
+ account$,
+ (permissions, account: Account) => ({
+ hasPermission: this.hasDeletePermission(permissions),
+ isAccountClosed: account.state === 'CLOSED'
+ }))
+ .map(result => result.hasPermission && result.isAccountClosed);
+
+ this.accountSubscription = account$
+ .subscribe(account => this.account = account);
+ }
+
+ private hasDeletePermission(permissions: FimsPermission[]): boolean {
+ return permissions.filter(permission =>
+ permission.id === 'accounting_accounts' &&
+ permission.accessLevel === 'DELETE'
+ ).length > 0;
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ this.accountSubscription.unsubscribe();
+ }
+
+ confirmDeletion(): Observable<boolean> {
+ const message = 'Do you want to delete this account?';
+ const title = 'Confirm deletion';
+ const button = 'DELETE ACCOUNT';
+
+ return this.translate.get([title, message, button])
+ .flatMap(result =>
+ this.dialogService.openConfirm({
+ message: result[message],
+ title: result[title],
+ acceptButton: result[button]
+ }).afterClosed()
+ );
+ }
+
+ deleteAccount(): void {
+ this.confirmDeletion()
+ .filter(accept => accept)
+ .subscribe(() => {
+ this.store.dispatch({
+ type: DELETE, payload: {
+ account: this.account,
+ activatedRoute: this.route
+ }
+ });
+ });
+ }
+
+}
diff --git a/src/app/accounting/accounts/entries/account-entry.list.component.html b/src/app/accounting/accounts/entries/account-entry.list.component.html
new file mode 100644
index 0000000..af2046e
--- /dev/null
+++ b/src/app/accounting/accounts/entries/account-entry.list.component.html
@@ -0,0 +1,43 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over
+ title="{{'Account entries for account' | translate:{ value: account.identifier} }}"
+ [navigateBackTo]="['../']"
+ *ngIf="account$ | async as account">
+ <form [formGroup]="form">
+ <mat-form-field layout-margin>
+ <input matInput type="date" placeholder="{{'Start date' | translate}}" formControlName="startDate">
+ </mat-form-field>
+ <mat-form-field layout-margin>
+ <input matInput type="date" placeholder="{{'End date' | translate}}" formControlName="endDate">
+ <mat-error *ngIf="form.hasError('rangeInvalid')" class="tc-red-600" translate>Invalid date range</mat-error>
+ </mat-form-field>
+ <button layout-margin mat-button mat-icon-button (click)="fetchAccountsEntries(account.identifier)" [disabled]="!form.valid">
+ <mat-icon>search</mat-icon>
+ </button>
+ </form>
+ <fims-data-table flex
+ (onFetch)="fetchAccountsEntries(account.identifier, $event)"
+ [columns]="columns"
+ [data]="accountEntryData$ | async"
+ [actionColumn]="false"
+ [sortable]="true"
+ [sortBy]="'transactionDate'"
+ [pageable]="true">
+ </fims-data-table>
+</fims-layout-card-over>
diff --git a/src/app/accounting/accounts/entries/account-entry.list.component.ts b/src/app/accounting/accounts/entries/account-entry.list.component.ts
new file mode 100644
index 0000000..da896d6
--- /dev/null
+++ b/src/app/accounting/accounts/entries/account-entry.list.component.ts
@@ -0,0 +1,111 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {TableData, TableFetchRequest} from '../../../common/data-table/data-table.component';
+import {Account} from '../../../services/accounting/domain/account.model';
+import {ActivatedRoute} from '@angular/router';
+import {FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {todayAsISOString, toShortISOString} from '../../../services/domain/date.converter';
+import {FimsValidators} from '../../../common/validator/validators';
+import * as fromAccounting from '../../store';
+import {Observable} from 'rxjs/Observable';
+import {Subscription} from 'rxjs/Subscription';
+import {AccountingStore} from '../../store/index';
+import {SEARCH} from '../../store/account/entries/entries.actions';
+import {SelectAction} from '../../store/account/account.actions';
+import {DatePipe} from '@angular/common';
+import {FetchRequest} from '../../../services/domain/paging/fetch-request.model';
+
+@Component({
+ templateUrl: './account-entry.list.component.html',
+ providers: [DatePipe]
+})
+export class AccountEntryListComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ private lastFetchRequest: FetchRequest = {};
+
+ form: FormGroup;
+
+ account$: Observable<Account>;
+
+ accountEntryData$: Observable<TableData>;
+
+ columns: any[] = [
+ {
+ name: 'transactionDate', label: 'Transaction date', tooltip: 'Transaction date', format: (v: any) => {
+ return this.datePipe.transform(v, 'short');
+ }
+ },
+ {name: 'type', label: 'Type', tooltip: 'Type'},
+ {name: 'message', label: 'Message', tooltip: 'Message'},
+ {name: 'amount', label: 'Amount', tooltip: 'Amount'},
+ {name: 'balance', label: 'Balance', tooltip: 'Balance'}
+ ];
+
+ constructor(private route: ActivatedRoute, private formBuilder: FormBuilder, private store: AccountingStore, private datePipe: DatePipe) {
+ }
+
+ ngOnInit(): void {
+ const today = todayAsISOString();
+ this.form = this.formBuilder.group({
+ 'startDate': [today, [Validators.required]],
+ 'endDate': [today, [Validators.required]],
+ }, {validator: FimsValidators.matchRange('startDate', 'endDate')});
+
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectAction(params['id']))
+ .subscribe(this.store);
+
+ this.account$ = this.store.select(fromAccounting.getSelectedAccount)
+ .filter(account => !!account)
+ .do(account => this.fetchAccountsEntries(account.identifier));
+
+ this.accountEntryData$ = this.store.select(fromAccounting.getAccountEntrySearchResults)
+ .map(accountEntryPage => ({
+ totalElements: accountEntryPage.totalElements,
+ totalPages: accountEntryPage.totalPages,
+ data: accountEntryPage.entries
+ }));
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ }
+
+ fetchAccountsEntries(accountId: string, fetchRequest?: TableFetchRequest): void {
+ if (fetchRequest) {
+ this.lastFetchRequest = fetchRequest;
+ }
+
+ const startDate = toShortISOString(this.form.get('startDate').value);
+ const endDate = toShortISOString(this.form.get('endDate').value);
+
+ this.store.dispatch({
+ type: SEARCH, payload: {
+ accountId,
+ startDate,
+ endDate,
+ fetchRequest: this.lastFetchRequest
+ }
+ });
+
+ }
+}
diff --git a/src/app/accounting/accounts/form/create/create.form.component.html b/src/app/accounting/accounts/form/create/create.form.component.html
new file mode 100644
index 0000000..a9ff595
--- /dev/null
+++ b/src/app/accounting/accounts/form/create/create.form.component.html
@@ -0,0 +1,24 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Create new account' | translate}}">
+ <fims-account-form-component #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [account]="account">
+ </fims-account-form-component>
+</fims-layout-card-over>
diff --git a/src/app/accounting/accounts/form/create/create.form.component.ts b/src/app/accounting/accounts/form/create/create.form.component.ts
new file mode 100644
index 0000000..80deed1
--- /dev/null
+++ b/src/app/accounting/accounts/form/create/create.form.component.ts
@@ -0,0 +1,86 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {AccountFormComponent} from '../form.component';
+import {Account} from '../../../../services/accounting/domain/account.model';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Ledger} from '../../../../services/accounting/domain/ledger.model';
+import * as fromAccounting from '../../../store';
+import {Store} from '@ngrx/store';
+import {Subscription} from 'rxjs/Subscription';
+import {Error} from '../../../../services/domain/error.model';
+import {CREATE, RESET_FORM} from '../../../store/account/account.actions';
+
+@Component({
+ templateUrl: './create.form.component.html'
+})
+export class CreateAccountFormComponent implements OnInit, OnDestroy {
+
+ private formStateSubscription: Subscription;
+ private selectedLedgerSubscription: Subscription;
+
+ ledger: Ledger;
+
+ @ViewChild('form') formComponent: AccountFormComponent;
+
+ account: Account = {
+ identifier: '',
+ name: '',
+ ledger: '',
+ balance: 0
+ };
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: Store<fromAccounting.State>) {}
+
+ ngOnInit() {
+ this.formStateSubscription = this.store.select(fromAccounting.getAccountFormError)
+ .filter((error: Error) => !!error)
+ .subscribe((error: Error) => this.formComponent.showIdentifierValidationError());
+
+ this.selectedLedgerSubscription = this.store.select(fromAccounting.getSelectedLedger)
+ .filter(ledger => !!ledger)
+ .subscribe(ledger => {
+ this.ledger = ledger;
+ this.account.ledger = this.ledger.identifier;
+ this.account.type = this.ledger.type;
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.formStateSubscription.unsubscribe();
+ this.selectedLedgerSubscription.unsubscribe();
+
+ this.store.dispatch({ type: RESET_FORM });
+ }
+
+ onSave(account: Account) {
+ this.store.dispatch({ type: CREATE, payload: {
+ account,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel() {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/accounting/accounts/form/edit/edit.form.component.html b/src/app/accounting/accounts/form/edit/edit.form.component.html
new file mode 100644
index 0000000..09009a0
--- /dev/null
+++ b/src/app/accounting/accounts/form/edit/edit.form.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Edit account' | translate}}">
+ <fims-account-form-component #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [account]="account"
+ [editMode]="true">
+ </fims-account-form-component>
+</fims-layout-card-over>
diff --git a/src/app/accounting/accounts/form/edit/edit.form.component.ts b/src/app/accounting/accounts/form/edit/edit.form.component.ts
new file mode 100644
index 0000000..76bb0aa
--- /dev/null
+++ b/src/app/accounting/accounts/form/edit/edit.form.component.ts
@@ -0,0 +1,72 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Account} from '../../../../services/accounting/domain/account.model';
+import {AccountFormComponent} from '../form.component';
+import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Subscription} from 'rxjs/Subscription';
+import * as fromAccounting from '../../../store';
+import {AccountingStore} from '../../../store/index';
+import {SelectAction, UPDATE} from '../../../store/account/account.actions';
+
+@Component({
+ templateUrl: './edit.form.component.html'
+})
+export class EditAccountFormComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ private accountSubscription: Subscription;
+
+ account: Account;
+
+ @ViewChild('form') formComponent: AccountFormComponent;
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: AccountingStore) {}
+
+ ngOnInit() {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectAction(params['id']))
+ .subscribe(this.store);
+
+ this.accountSubscription = this.store.select(fromAccounting.getSelectedAccount)
+ .filter(account => !!account)
+ .subscribe(account => this.account = account);
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ this.accountSubscription.unsubscribe();
+ }
+
+ onSave(account: Account) {
+ this.store.dispatch({ type: UPDATE, payload: {
+ account,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel() {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/accounting/accounts/form/form.component.html b/src/app/accounting/accounts/form/form.component.html
new file mode 100644
index 0000000..c43e3da
--- /dev/null
+++ b/src/app/accounting/accounts/form/form.component.html
@@ -0,0 +1,40 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Account details' | translate}}" [state]="">
+ <form [formGroup]="form" layout="column">
+ <mat-radio-group formControlName="type">
+ <mat-radio-button *ngFor="let type of accountTypeOptions" [value]="type.type" layout-margin>
+ {{type.label}}
+ </mat-radio-button>
+ </mat-radio-group>
+ <fims-id-input flex [form]="form" controlName="identifier" [readonly]="editMode"></fims-id-input>
+ <fims-text-input [form]="form" controlName="name" placeholder="{{'Name' | translate}}"></fims-text-input>
+ <fims-text-input type="number" [form]="form" controlName="balance" placeholder="{{'Balance' | translate}}"></fims-text-input>
+ </form>
+ <ng-template td-step-actions>
+ <fims-form-final-action
+ [resourceName]="'ACCOUNT'"
+ [editMode]="editMode"
+ [disabled]="!valid"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/accounting/accounts/form/form.component.spec.ts b/src/app/accounting/accounts/form/form.component.spec.ts
new file mode 100644
index 0000000..25d1f56
--- /dev/null
+++ b/src/app/accounting/accounts/form/form.component.spec.ts
@@ -0,0 +1,111 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, DebugElement} from '@angular/core';
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {AccountFormComponent} from './form.component';
+import {By} from '@angular/platform-browser';
+import {Account} from '../../../services/accounting/domain/account.model';
+import {TranslateModule} from '@ngx-translate/core';
+import {ReactiveFormsModule} from '@angular/forms';
+import {FimsSharedModule} from '../../../common/common.module';
+import {MatCheckboxModule, MatInputModule, MatRadioModule} from '@angular/material';
+import {CovalentStepsModule} from '@covalent/core';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+
+describe('Test account form', () => {
+
+ let fixture: ComponentFixture<TestComponent>;
+
+ let testComponent: TestComponent;
+
+ beforeEach( async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot(),
+ MatCheckboxModule,
+ MatRadioModule,
+ MatInputModule,
+ CovalentStepsModule,
+ FimsSharedModule,
+ ReactiveFormsModule,
+ NoopAnimationsModule
+ ],
+ declarations: [
+ AccountFormComponent,
+ TestComponent
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TestComponent);
+ testComponent = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should trigger save event', () => {
+ const button: DebugElement = fixture.debugElement.query(By.css('button[mat-raised-button]'));
+
+ button.nativeElement.click();
+
+ expect(testComponent.savedAccount).toEqual(testComponent.account);
+ });
+
+ it('should trigger cancel event', () => {
+ const button: DebugElement = fixture.debugElement.query(By.css('button[mat-button]'));
+
+ button.nativeElement.click();
+
+ expect(testComponent.canceled).toBeTruthy();
+ });
+
+});
+
+@Component({
+ template: `
+ <fims-account-form-component
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel($event)"
+ [account]="account"
+ [editMode]="true">
+ </fims-account-form-component>`
+})
+class TestComponent {
+
+ account: Account = {
+ identifier: 'test',
+ type: 'ASSET',
+ name: 'test',
+ ledger: 'test',
+ balance: 10
+ };
+
+ savedAccount: Account;
+
+ canceled: boolean;
+
+ onSave(account: Account): void {
+ this.savedAccount = account;
+ }
+
+ onCancel(): void {
+ this.canceled = true;
+ }
+
+}
diff --git a/src/app/accounting/accounts/form/form.component.ts b/src/app/accounting/accounts/form/form.component.ts
new file mode 100644
index 0000000..fcff1fa
--- /dev/null
+++ b/src/app/accounting/accounts/form/form.component.ts
@@ -0,0 +1,94 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
+import {Account} from '../../../services/accounting/domain/account.model';
+import {FormBuilder, Validators} from '@angular/forms';
+import {FormComponent} from '../../../common/forms/form.component';
+import {TdStepComponent} from '@covalent/core';
+import {Observable} from 'rxjs/Observable';
+import {AccountTypeOption, accountTypes} from '../../account-types.model';
+import {FimsValidators} from '../../../common/validator/validators';
+
+@Component({
+ selector: 'fims-account-form-component',
+ templateUrl: './form.component.html'
+})
+export class AccountFormComponent extends FormComponent<Account> implements OnInit {
+
+ @ViewChild('detailsStep') step: TdStepComponent;
+
+ accounts: Observable<Account[]>;
+
+ @Input() account: Account;
+
+ @Input() editMode: boolean;
+
+ @Output('onSave') onSave = new EventEmitter<Account>();
+
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ accountTypeOptions: AccountTypeOption[] = accountTypes;
+
+ constructor(private formBuilder: FormBuilder) {
+ super();
+ }
+
+ ngOnInit() {
+ this.openDetailStep();
+ this.form = this.formBuilder.group({
+ 'identifier': [this.account.identifier, [Validators.required, Validators.minLength(3),
+ Validators.maxLength(34), FimsValidators.urlSafe]],
+ 'name': [this.account.name, [Validators.required, Validators.maxLength(256)]],
+ 'type': [{value: this.account.type, disabled: true}, [Validators.required]],
+ 'ledger': [this.account.ledger, [Validators.required]],
+ 'balance': [{value: this.account.balance, disabled: this.editMode}, [Validators.required]],
+ });
+ }
+
+ openDetailStep(): void {
+ this.step.open();
+ }
+
+ showIdentifierValidationError(): void {
+ this.setError('identifier', 'unique', true);
+ this.openDetailStep();
+ }
+
+ get formData(): Account {
+ // Not needed
+ return;
+ }
+
+ save(): void {
+ const account: Account = {
+ identifier: this.form.get('identifier').value,
+ name: this.form.get('name').value,
+ type: this.form.get('type').value,
+ ledger: this.form.get('ledger').value,
+ balance: this.form.get('balance').value
+ };
+
+ this.onSave.emit(account);
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+}
diff --git a/src/app/accounting/activity/activity.component.html b/src/app/accounting/activity/activity.component.html
new file mode 100644
index 0000000..ae2f48d
--- /dev/null
+++ b/src/app/accounting/activity/activity.component.html
@@ -0,0 +1,26 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Activities' | translate}}" [navigateBackTo]="['../']">
+ <mat-list>
+ <h3 mat-subheader translate>Latest activity</h3>
+ <ng-template let-item let-last="last" ngFor [ngForOf]="commands">
+ <fims-command-display [command]="item"></fims-command-display>
+ <mat-divider *ngIf="!last" mat-inset></mat-divider>
+ </ng-template>
+ </mat-list>
+</fims-layout-card-over>
diff --git a/src/app/accounting/activity/activity.component.ts b/src/app/accounting/activity/activity.component.ts
new file mode 100644
index 0000000..08d73d6
--- /dev/null
+++ b/src/app/accounting/activity/activity.component.ts
@@ -0,0 +1,35 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {AccountCommand} from '../../services/accounting/domain/account-command.model';
+
+@Component({
+ templateUrl: './activity.component.html'
+})
+export class AccountActivityComponent implements OnInit {
+
+ commands: AccountCommand[];
+
+ constructor(private route: ActivatedRoute) {}
+
+ ngOnInit(): void {
+ this.route.data.subscribe(( data: { commands: AccountCommand[]}) => this.commands = data.commands );
+ }
+}
diff --git a/src/app/accounting/activity/commands.resolver.ts b/src/app/accounting/activity/commands.resolver.ts
new file mode 100644
index 0000000..4d10dcb
--- /dev/null
+++ b/src/app/accounting/activity/commands.resolver.ts
@@ -0,0 +1,33 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router';
+import {Observable} from 'rxjs/Observable';
+import {AccountingService} from '../../services/accounting/accounting.service';
+import {AccountCommand} from '../../services/accounting/domain/account-command.model';
+
+@Injectable()
+export class CommandsResolver implements Resolve<AccountCommand[]> {
+
+ constructor(private accountingService: AccountingService) {}
+
+ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<AccountCommand[]> {
+ return this.accountingService.fetchAccountCommands(route.params['id']);
+ }
+}
diff --git a/src/app/accounting/chartOfAccounts/chart-of-account-table.component.html b/src/app/accounting/chartOfAccounts/chart-of-account-table.component.html
new file mode 100644
index 0000000..db58506
--- /dev/null
+++ b/src/app/accounting/chartOfAccounts/chart-of-account-table.component.html
@@ -0,0 +1,43 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<table td-data-table>
+ <thead>
+ <tr td-data-table-column-row>
+ <th td-data-table-column translate>Code</th>
+ <th td-data-table-column translate>Name</th>
+ <th td-data-table-column translate>Description</th>
+ <th td-data-table-column translate>Type</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr td-data-table-row *ngFor="let entry of chartOfAccountEntries">
+ <td td-data-table-cell [ngStyle]="{'padding-left.px': 24+(15*entry.level)}">{{entry.code}}</td>
+ <td td-data-table-cell>{{entry.name}}</td>
+ <td td-data-table-cell>{{entry.description}}</td>
+ <td td-data-table-cell>{{entry.type}}</td>
+ </tr>
+ </tbody>
+</table>
+
+<div class="mat-padding" *ngIf="!(chartOfAccountEntries.length > 0) && !loading" layout="row" layout-align="center center">
+ <h3 translate>No data for chart of accounts available.</h3>
+</div>
+
+<div class="mat-padding" *ngIf="loading" layout="row" layout-align="center center">
+ <h3 translate>Fetching data for chart of accounts...</h3>
+</div>
diff --git a/src/app/accounting/chartOfAccounts/chart-of-account-table.component.ts b/src/app/accounting/chartOfAccounts/chart-of-account-table.component.ts
new file mode 100644
index 0000000..3ae57de
--- /dev/null
+++ b/src/app/accounting/chartOfAccounts/chart-of-account-table.component.ts
@@ -0,0 +1,32 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input} from '@angular/core';
+import {ChartOfAccountEntry} from '../../services/accounting/domain/chart-of-account-entry.model';
+
+@Component({
+ selector: 'fims-chart-of-account-table',
+ templateUrl: './chart-of-account-table.component.html'
+})
+export class ChartOfAccountTableComponent {
+
+ @Input() chartOfAccountEntries: ChartOfAccountEntry[] = [];
+
+ @Input() loading: boolean;
+
+}
diff --git a/src/app/accounting/chartOfAccounts/chart-of-accounts.component.html b/src/app/accounting/chartOfAccounts/chart-of-accounts.component.html
new file mode 100644
index 0000000..354df54
--- /dev/null
+++ b/src/app/accounting/chartOfAccounts/chart-of-accounts.component.html
@@ -0,0 +1,20 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Chart of accounts' | translate}}" [navigateBackTo]="['../']">
+ <fims-chart-of-account-table [chartOfAccountEntries]="chartOfAccountEntries$ | async" [loading]="loading$ | async"></fims-chart-of-account-table>
+</fims-layout-card-over>
diff --git a/src/app/accounting/chartOfAccounts/chart-of-accounts.component.ts b/src/app/accounting/chartOfAccounts/chart-of-accounts.component.ts
new file mode 100644
index 0000000..aa16e5a
--- /dev/null
+++ b/src/app/accounting/chartOfAccounts/chart-of-accounts.component.ts
@@ -0,0 +1,45 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import {ChartOfAccountEntry} from '../../services/accounting/domain/chart-of-account-entry.model';
+import {Observable} from 'rxjs/Observable';
+import {AccountingStore} from '../store/index';
+import * as fromAccounting from '../store';
+import {LOAD_CHART_OF_ACCOUNTS} from '../store/ledger/ledger.actions';
+
+@Component({
+ templateUrl: './chart-of-accounts.component.html'
+})
+export class ChartOfAccountComponent implements OnInit {
+
+ chartOfAccountEntries$: Observable<ChartOfAccountEntry[]>;
+
+ loading$: Observable<boolean>;
+
+ constructor(private store: AccountingStore) {}
+
+ ngOnInit(): void {
+ this.chartOfAccountEntries$ = this.store.select(fromAccounting.getChartOfAccountEntries);
+
+ this.loading$ = this.store.select(fromAccounting.getChartOfAccountLoading);
+
+ this.store.dispatch({ type: LOAD_CHART_OF_ACCOUNTS });
+ }
+
+}
diff --git a/src/app/accounting/cheques/cheques.list.component.html b/src/app/accounting/cheques/cheques.list.component.html
new file mode 100644
index 0000000..25b3d4e
--- /dev/null
+++ b/src/app/accounting/cheques/cheques.list.component.html
@@ -0,0 +1,51 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Cheque clearing' | translate}}" [navigateBackTo]="['../']">
+ <div class="mat-content">
+ <table td-data-table>
+ <thead>
+ <tr td-data-table-column-row>
+ <th td-data-table-column
+ *ngFor="let column of columns"
+ [name]="column.name">
+ {{column.label}}
+ </th>
+ <th td-data-table-column *hasPermission="{ id: 'cheque_management', accessLevel: 'CHANGE' }" translate>APPROVE</th>
+ <th td-data-table-column *hasPermission="{ id: 'cheque_management', accessLevel: 'CHANGE' }" translate>CANCEL</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr td-data-table-row *ngFor="let row of cheques$ | async">
+ <td td-data-table-cell [numeric]="column.numeric" *ngFor="let column of columns">
+ {{column.format ? column.format(row[column.name]) : row[column.name]}}
+ </td>
+ <td td-data-table-cell *hasPermission="{ id: 'cheque_management', accessLevel: 'CHANGE' }">
+ <button mat-button mat-raised-button color="primary" [disabled]="row.state !== 'PENDING'" (click)="approveCheque(row)">
+ {{ 'APPROVE' | translate}}
+ </button>
+ </td>
+ <td td-data-table-cell *hasPermission="{ id: 'cheque_management', accessLevel: 'CHANGE' }">
+ <button mat-button mat-raised-button color="warn" [disabled]="row.state !== 'PENDING'" (click)="cancelCheque(row)">
+ {{ 'CANCEL' | translate}}
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+</fims-layout-card-over>
diff --git a/src/app/accounting/cheques/cheques.list.component.ts b/src/app/accounting/cheques/cheques.list.component.ts
new file mode 100644
index 0000000..61db892
--- /dev/null
+++ b/src/app/accounting/cheques/cheques.list.component.ts
@@ -0,0 +1,99 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {ITdDataTableColumn, TdDialogService} from '@covalent/core';
+import {AccountingStore} from '../store/index';
+import * as fromAccounting from '../store';
+import {ChequeCRUDActions, ProcessAction} from '../store/cheques/cheque.actions';
+import {TranslateService} from '@ngx-translate/core';
+import {FimsCheque} from '../../services/cheque/domain/fims-cheque.model';
+import {DatePipe} from '@angular/common';
+
+@Component({
+ templateUrl: './cheques.list.component.html',
+ providers: [DatePipe]
+})
+export class ChequesListComponent implements OnInit {
+
+ cheques$: Observable<FimsCheque[]>;
+
+ columns: ITdDataTableColumn[] = [
+ {name: 'identifier', label: 'Identifier'},
+ {name: 'drawee', label: 'Drawee'},
+ {name: 'drawer', label: 'Drawer'},
+ {name: 'payee', label: 'Payee'},
+ {name: 'amount', label: 'Amount'},
+ {name: 'dateIssued', label: 'Date issued', format: value => this.datePipe.transform(value, 'shortDate')},
+ {name: 'state', label: 'State'}
+ ];
+
+ constructor(private store: AccountingStore, private dialogService: TdDialogService,
+ private translate: TranslateService, private datePipe: DatePipe) {
+ this.cheques$ = store.select(fromAccounting.getAllChequeEntities);
+ }
+
+ ngOnInit(): void {
+ this.store.dispatch(ChequeCRUDActions.loadAllAction({
+ state: 'PENDING'
+ }));
+ }
+
+ confirmAction(action: string): Observable<boolean> {
+ const message = `Do you want to ${action} this cheque?`;
+ const title = 'Confirm action';
+ const button = `${action} cheque`;
+
+ return this.translate.get([title, message, button])
+ .flatMap(result =>
+ this.dialogService.openConfirm({
+ message: result[message],
+ title: result[title],
+ acceptButton: result[button]
+ }).afterClosed()
+ );
+ }
+
+ approveCheque(cheque: FimsCheque): void {
+ this.confirmAction('APPROVE')
+ .filter(accept => accept)
+ .subscribe(() => {
+ this.store.dispatch(new ProcessAction({
+ chequeIdentifier: cheque.identifier,
+ command: {
+ action: 'APPROVE'
+ }
+ }));
+ });
+ }
+
+ cancelCheque(cheque: FimsCheque): void {
+ this.confirmAction('CANCEL')
+ .filter(accept => accept)
+ .subscribe(() => {
+ this.store.dispatch(new ProcessAction({
+ chequeIdentifier: cheque.identifier,
+ command: {
+ action: 'CANCEL'
+ }
+ }));
+ });
+ }
+
+}
diff --git a/src/app/accounting/financialCondition/financial-condition.component.html b/src/app/accounting/financialCondition/financial-condition.component.html
new file mode 100644
index 0000000..8ffac70
--- /dev/null
+++ b/src/app/accounting/financialCondition/financial-condition.component.html
@@ -0,0 +1,136 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Financial condition' | translate}}" [navigateBackTo]="['../']">
+ <div layout="row">
+ <table td-data-table *ngIf="financialCondition$ | async as financialCondition">
+ <ng-container *ngIf="assets$ | async as assets">
+ <tr td-data-table-row>
+ <td td-data-table-cell>
+ <b translate>Assets</b>
+ </td>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell></td>
+ </tr>
+ <tr td-data-table-row *ngFor="let entry of assets.financialConditionEntries">
+ <td td-data-table-cell></td>
+ <td td-data-table-cell>
+ {{entry.description}}
+ </td>
+ <td td-data-table-cell>
+ {{entry.value | displayFimsFinancialNumber}}
+ </td>
+ </tr>
+ <tr td-data-table-row>
+ <td td-data-table-cell>
+ <i translate>Subtotal</i>
+ </td>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell>
+ <i class="double-underline">
+ {{assets.subtotal | displayFimsFinancialNumber}}
+ </i>
+ </td>
+ </tr>
+ <tr td-data-table-row></tr>
+ <tr td-data-table-row>
+ <td td-data-table-cell>
+ <b translate>Total Assets</b>
+ </td>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell>
+ <b class="double-underline">
+ {{financialCondition.totalAssets | displayFimsFinancialNumber}}
+ </b>
+ </td>
+ </tr>
+ <tr td-data-table-row></tr>
+ </ng-container>
+ <ng-container *ngIf="equities$ | async as equities">
+ <tr td-data-table-row>
+ <td td-data-table-cell>
+ <b translate>Equities</b>
+ </td>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell></td>
+ </tr>
+ <tr td-data-table-row *ngFor="let entry of equities.financialConditionEntries">
+ <td td-data-table-cell></td>
+ <td td-data-table-cell>
+ {{entry.description}}
+ </td>
+ <td td-data-table-cell>
+ {{entry.value | displayFimsFinancialNumber}}
+ </td>
+ </tr>
+ <tr td-data-table-row>
+ <td td-data-table-cell>
+ <i translate>Subtotal</i>
+ </td>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell>
+ <i class="double-underline">
+ {{equities.subtotal | displayFimsFinancialNumber}}
+ </i>
+ </td>
+ </tr>
+ <tr td-data-table-row></tr>
+ </ng-container>
+ <ng-container *ngIf="liabilities$ | async as liabilities">
+ <tr td-data-table-row>
+ <td td-data-table-cell>
+ <b translate>Liabilities</b>
+ </td>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell></td>
+ </tr>
+ <tr td-data-table-row *ngFor="let entry of liabilities.financialConditionEntries">
+ <td td-data-table-cell></td>
+ <td td-data-table-cell>
+ {{entry.description}}
+ </td>
+ <td td-data-table-cell>
+ {{entry.value | displayFimsFinancialNumber}}
+ </td>
+ </tr>
+ <tr td-data-table-row>
+ <td td-data-table-cell>
+ <i translate>Subtotal</i>
+ </td>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell>
+ <i class="double-underline">
+ {{liabilities.subtotal | displayFimsFinancialNumber}}
+ </i>
+ </td>
+ </tr>
+ <tr td-data-table-row></tr>
+ <tr td-data-table-row>
+ <td td-data-table-cell>
+ <b translate>Total Equities and Liabilities</b>
+ </td>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell>
+ <b class="double-underline">
+ {{financialCondition.totalEquitiesAndLiabilities | displayFimsFinancialNumber}}
+ </b>
+ </td>
+ </tr>
+ </ng-container>
+ </table>
+ </div>
+</fims-layout-card-over>
diff --git a/src/app/accounting/financialCondition/financial-condition.component.scss b/src/app/accounting/financialCondition/financial-condition.component.scss
new file mode 100644
index 0000000..9580096
--- /dev/null
+++ b/src/app/accounting/financialCondition/financial-condition.component.scss
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+.double-underline {
+ border-bottom: 3px double;
+}
diff --git a/src/app/accounting/financialCondition/financial-condition.component.ts b/src/app/accounting/financialCondition/financial-condition.component.ts
new file mode 100644
index 0000000..040a286
--- /dev/null
+++ b/src/app/accounting/financialCondition/financial-condition.component.ts
@@ -0,0 +1,57 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component} from '@angular/core';
+import {AccountingService} from '../../services/accounting/accounting.service';
+import {FinancialCondition} from '../../services/accounting/domain/financial-condition.model';
+import {Observable} from 'rxjs/Observable';
+import {FinancialConditionSection} from '../../services/accounting/domain/financial-condition-section.model';
+
+@Component({
+ templateUrl: './financial-condition.component.html',
+ styleUrls: ['./financial-condition.component.scss']
+})
+export class FinancialConditionComponent {
+
+ financialCondition$: Observable<FinancialCondition>;
+
+ assets$: Observable<FinancialConditionSection>;
+
+ equities$: Observable<FinancialConditionSection>;
+
+ liabilities$: Observable<FinancialConditionSection>;
+
+ constructor(private accountingService: AccountingService) {
+ this.financialCondition$ = accountingService.getFinancialCondition().share();
+
+ this.assets$ = this.financialCondition$
+ .map(statement => statement.financialConditionSections
+ .find(section => section.type === 'ASSET')
+ );
+
+ this.equities$ = this.financialCondition$
+ .map(statement => statement.financialConditionSections
+ .find(section => section.type === 'EQUITY')
+ );
+
+ this.liabilities$ = this.financialCondition$
+ .map(statement => statement.financialConditionSections
+ .find(section => section.type === 'LIABILITY')
+ );
+ }
+}
diff --git a/src/app/accounting/form/create/create.form.component.html b/src/app/accounting/form/create/create.form.component.html
new file mode 100644
index 0000000..6280e1b
--- /dev/null
+++ b/src/app/accounting/form/create/create.form.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Create new ledger' | translate}}">
+ <fims-ledger-form-component #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [ledger]="ledger"
+ [parentLedger]="parentLedger">
+ </fims-ledger-form-component>
+</fims-layout-card-over>
diff --git a/src/app/accounting/form/create/create.form.component.ts b/src/app/accounting/form/create/create.form.component.ts
new file mode 100644
index 0000000..576fb94
--- /dev/null
+++ b/src/app/accounting/form/create/create.form.component.ts
@@ -0,0 +1,95 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {Ledger} from '../../../services/accounting/domain/ledger.model';
+import {LedgerFormComponent} from '../form.component';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Error} from '../../../services/domain/error.model';
+import * as fromAccounting from '../../store';
+import {Subscription} from 'rxjs/Subscription';
+import {CREATE, CREATE_SUB_LEDGER, RESET_FORM} from '../../store/ledger/ledger.actions';
+import {AccountingStore} from '../../store/index';
+
+@Component({
+ templateUrl: './create.form.component.html'
+})
+export class CreateLedgerFormComponent implements OnInit, OnDestroy {
+
+ private formStateSubscription: Subscription;
+
+ private ledgerSubscription: Subscription;
+
+ @ViewChild('form') formComponent: LedgerFormComponent;
+
+ parentLedger: Ledger;
+
+ ledger: Ledger = {
+ identifier: '',
+ type: 'ASSET',
+ name: '',
+ showAccountsInChart: true,
+ subLedgers: [],
+ };
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: AccountingStore) {
+ }
+
+ ngOnInit() {
+ this.formStateSubscription = this.store.select(fromAccounting.getLedgerFormError)
+ .filter((error: Error) => !!error)
+ .subscribe((error: Error) => this.formComponent.showIdentifierValidationError());
+
+ this.ledgerSubscription = this.store.select(fromAccounting.getSelectedLedger)
+ .subscribe(ledger => this.parentLedger = ledger);
+ }
+
+ ngOnDestroy(): void {
+ this.formStateSubscription.unsubscribe();
+ this.ledgerSubscription.unsubscribe();
+
+ this.store.dispatch({type: RESET_FORM});
+ }
+
+ onSave(ledger: Ledger) {
+ if (this.parentLedger) {
+ this.store.dispatch({
+ type: CREATE_SUB_LEDGER, payload: {
+ parentLedgerId: this.parentLedger.identifier,
+ ledger,
+ activatedRoute: this.route
+ }
+ });
+ } else {
+ this.store.dispatch({
+ type: CREATE, payload: {
+ ledger,
+ activatedRoute: this.route
+ }
+ });
+ }
+ }
+
+ onCancel() {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], {relativeTo: this.route});
+ }
+}
diff --git a/src/app/accounting/form/edit/edit.form.component.html b/src/app/accounting/form/edit/edit.form.component.html
new file mode 100644
index 0000000..14883bc
--- /dev/null
+++ b/src/app/accounting/form/edit/edit.form.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Edit ledger' | translate}}">
+ <fims-ledger-form-component #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [ledger]="ledger"
+ [editMode]="true">
+ </fims-ledger-form-component>
+</fims-layout-card-over>
diff --git a/src/app/accounting/form/edit/edit.form.component.ts b/src/app/accounting/form/edit/edit.form.component.ts
new file mode 100644
index 0000000..5676d19
--- /dev/null
+++ b/src/app/accounting/form/edit/edit.form.component.ts
@@ -0,0 +1,66 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {Ledger} from '../../../services/accounting/domain/ledger.model';
+import {LedgerFormComponent} from '../form.component';
+import {ActivatedRoute, Router} from '@angular/router';
+import {UPDATE} from '../../store/ledger/ledger.actions';
+import * as fromAccounting from '../../store';
+import {Subscription} from 'rxjs/Subscription';
+import {AccountingStore} from '../../store/index';
+
+@Component({
+ templateUrl: './edit.form.component.html'
+})
+export class EditLedgerFormComponent implements OnInit, OnDestroy {
+
+ private ledgerSubscription: Subscription;
+
+ ledger: Ledger;
+
+ @ViewChild('form') formComponent: LedgerFormComponent;
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: AccountingStore) {}
+
+ ngOnInit() {
+ this.ledgerSubscription = this.store.select(fromAccounting.getSelectedLedger)
+ .subscribe(ledger => this.ledger = ledger);
+ }
+
+ ngOnDestroy(): void {
+ this.ledgerSubscription.unsubscribe();
+ }
+
+ onSave(ledger: Ledger) {
+ ledger.subLedgers = this.ledger.subLedgers;
+
+ this.store.dispatch({ type: UPDATE, payload: {
+ ledger,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel() {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/accounting/form/form.component.html b/src/app/accounting/form/form.component.html
new file mode 100644
index 0000000..6b19dc4
--- /dev/null
+++ b/src/app/accounting/form/form.component.html
@@ -0,0 +1,47 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Ledger details' | translate}}"
+ [state]="form.valid ? 'complete' : form.pristine ? 'none' : 'required'">
+ <form [formGroup]="form" layout="column">
+ <fims-id-input flex [form]="form" placeholder="Ledger Number" controlName="identifier" [readonly]="editMode"></fims-id-input>
+ <mat-radio-group formControlName="type">
+ <mat-radio-button *ngFor="let type of accountTypeOptions" [value]="type.type" layout-margin>
+ {{type.label}}
+ </mat-radio-button>
+ </mat-radio-group>
+ <fims-text-input [form]="form" controlName="name" placeholder="{{'Name' | translate}}"></fims-text-input>
+ <mat-form-field layout-margin flex>
+ <textarea matInput placeholder="{{'Description(Optional)' | translate}}" formControlName="description"></textarea>
+ <mat-error *ngIf="form.get('description').hasError('maxlength')">
+ {{ 'Only characters allowed.' | translate:{ value: form.get('description').getError('maxlength')['requiredLength']} }}
+ </mat-error>
+ </mat-form-field>
+ <mat-checkbox formControlName="showAccountsInChart" layout-margin translate>Show accounts when displayed in chart?</mat-checkbox>
+ </form>
+ <ng-template td-step-actions>
+ <fims-form-final-action
+ [resourceName]="'LEDGER'"
+ [editMode]="editMode"
+ [disabled]="!form.valid"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/accounting/form/form.component.spec.ts b/src/app/accounting/form/form.component.spec.ts
new file mode 100644
index 0000000..895f77c
--- /dev/null
+++ b/src/app/accounting/form/form.component.spec.ts
@@ -0,0 +1,108 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, DebugElement} from '@angular/core';
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {By} from '@angular/platform-browser';
+import {TranslateModule} from '@ngx-translate/core';
+import {ReactiveFormsModule} from '@angular/forms';
+import {CovalentStepsModule} from '@covalent/core';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {FimsSharedModule} from '../../common/common.module';
+import {LedgerFormComponent} from './form.component';
+import {Ledger} from '../../services/accounting/domain/ledger.model';
+import {MatCheckboxModule, MatInputModule, MatRadioModule} from '@angular/material';
+
+describe('Test ledger form', () => {
+
+ let fixture: ComponentFixture<TestComponent>;
+
+ let testComponent: TestComponent;
+
+ beforeEach( async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot(),
+ MatCheckboxModule,
+ MatRadioModule,
+ MatInputModule,
+ CovalentStepsModule,
+ FimsSharedModule,
+ ReactiveFormsModule,
+ NoopAnimationsModule
+ ],
+ declarations: [
+ LedgerFormComponent,
+ TestComponent
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TestComponent);
+ testComponent = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should trigger save event', () => {
+ const button: DebugElement = fixture.debugElement.query(By.css('button[mat-raised-button]'));
+
+ button.nativeElement.click();
+
+ expect(testComponent.savedLedger).toEqual(testComponent.ledger);
+ });
+
+ it('should trigger cancel event', () => {
+ const button: DebugElement = fixture.debugElement.query(By.css('button[mat-button]'));
+
+ button.nativeElement.click();
+
+ expect(testComponent.canceled).toBeTruthy();
+ });
+
+});
+
+@Component({
+ template: `
+ <fims-ledger-form-component (onSave)="onSave($event)" (onCancel)="onCancel($event)" [ledger]="ledger" [editMode]="true">
+ </fims-ledger-form-component>`
+})
+class TestComponent {
+
+ ledger: Ledger = {
+ identifier: 'test',
+ type: 'ASSET',
+ name: 'test',
+ description: 'test',
+ showAccountsInChart: true,
+ subLedgers: []
+ };
+
+ savedLedger: Ledger;
+
+ canceled: boolean;
+
+ onSave(ledger: Ledger): void {
+ this.savedLedger = ledger;
+ }
+
+ onCancel(): void {
+ this.canceled = true;
+ }
+
+}
diff --git a/src/app/accounting/form/form.component.ts b/src/app/accounting/form/form.component.ts
new file mode 100644
index 0000000..881347b
--- /dev/null
+++ b/src/app/accounting/form/form.component.ts
@@ -0,0 +1,98 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {AccountTypeOption, accountTypes} from '../account-types.model';
+import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
+import {FormComponent} from '../../common/forms/form.component';
+import {Ledger} from '../../services/accounting/domain/ledger.model';
+import {TdStepComponent} from '@covalent/core';
+import {FormBuilder, Validators} from '@angular/forms';
+import {FimsValidators} from '../../common/validator/validators';
+
+@Component({
+ selector: 'fims-ledger-form-component',
+ templateUrl: './form.component.html'
+})
+export class LedgerFormComponent extends FormComponent<Ledger> implements OnInit {
+
+ @ViewChild('detailsStep') step: TdStepComponent;
+
+ @Input() parentLedger: Ledger;
+
+ @Input() ledger: Ledger;
+
+ @Input() editMode: boolean;
+
+ @Output('onSave') onSave = new EventEmitter<Ledger>();
+
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ accountTypeOptions: AccountTypeOption[] = accountTypes;
+
+ constructor(private formBuilder: FormBuilder) {
+ super();
+ }
+
+ get formData(): Ledger {
+ return null;
+ }
+
+ ngOnInit(): void {
+ this.openDetailStep();
+
+ const typeValue = {
+ value: this.parentLedger ? this.parentLedger.type : this.ledger.type,
+ disabled: this.parentLedger || this.editMode
+ };
+
+ this.form = this.formBuilder.group({
+ 'identifier': [this.ledger.identifier, [Validators.required, Validators.minLength(3), Validators.maxLength(32),
+ FimsValidators.urlSafe]],
+ 'type': [typeValue, [Validators.required]],
+ 'name': [this.ledger.name, [Validators.required, Validators.maxLength(256)]],
+ 'showAccountsInChart': [this.ledger.showAccountsInChart, [Validators.required]],
+ 'description': [this.ledger.description, Validators.maxLength(2048)],
+ });
+ }
+
+ showIdentifierValidationError(): void {
+ this.setError('identifier', 'unique', true);
+ this.openDetailStep();
+ }
+
+ openDetailStep(): void {
+ this.step.open();
+ }
+
+ save(): void {
+ const ledger: Ledger = {
+ identifier: this.form.get('identifier').value,
+ type: this.form.get('type').value,
+ name: this.form.get('name').value,
+ showAccountsInChart: this.form.get('showAccountsInChart').value,
+ description: this.form.get('description').value,
+ subLedgers: []
+ };
+
+ this.onSave.emit(ledger);
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+}
diff --git a/src/app/accounting/general-ledger.component.html b/src/app/accounting/general-ledger.component.html
new file mode 100644
index 0000000..a226703
--- /dev/null
+++ b/src/app/accounting/general-ledger.component.html
@@ -0,0 +1,69 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'General Ledger' | translate}}">
+ <fims-two-column-layout>
+ <mat-nav-list left>
+ <h3 mat-subheader translate>Details</h3>
+ <a mat-list-item [routerLink]="['./chartOfAccounts']">
+ <mat-icon matListAvatar>list</mat-icon>
+ <h3 matLine translate>Chart of accounts</h3>
+ <p matLine translate>View chart of accounts</p>
+ </a>
+ <a mat-list-item [routerLink]="['./journalEntries']" *hasPermission="{ id: 'accounting_journals', accessLevel: 'READ' }">
+ <mat-icon matListAvatar>assignment</mat-icon>
+ <h3 matLine translate>Journal entries</h3>
+ <p matLine translate>View journal entries</p>
+ </a>
+ <a mat-list-item [routerLink]="['./transactiontypes']" *hasPermission="{ id: 'accounting_tx_types', accessLevel: 'READ' }">
+ <mat-icon matListAvatar>swap_horiz</mat-icon>
+ <h3 matLine translate>Transaction types</h3>
+ <p matLine translate>View transaction types</p>
+ </a>
+ <a mat-list-item [routerLink]="['./cheques']" *hasPermission="{ id: 'cheque_management', accessLevel: 'READ' }">
+ <mat-icon matListAvatar>import_contacts</mat-icon>
+ <h3 matLine translate>Cheque clearing</h3>
+ <p matLine translate>View and clear cheques</p>
+ </a>
+ <a mat-list-item [routerLink]="['./trialBalance']">
+ <mat-icon matListAvatar>account_balance</mat-icon>
+ <h3 matLine translate>Trial balance</h3>
+ <p matLine translate>View trial balance</p>
+ </a>
+ <a mat-list-item [routerLink]="['./incomeStatement']" *hasPermission="{ id: 'accounting_income_statement', accessLevel: 'READ'}">
+ <mat-icon matListAvatar>timeline</mat-icon>
+ <h3 matLine translate>Income statement</h3>
+ <p matLine translate>View income statement</p>
+ </a>
+ <a mat-list-item [routerLink]="['./financialCondition']" *hasPermission="{ id: 'accounting_fin_condition', accessLevel: 'READ'}">
+ <mat-icon matListAvatar>timeline</mat-icon>
+ <h3 matLine translate>Financial condition</h3>
+ <p matLine translate>View financial condition</p>
+ </a>
+ <a mat-list-item [routerLink]="['./payrolls']" *hasPermission="{ id: 'payroll_distribution', accessLevel: 'READ' }">
+ <mat-icon matListAvatar>receipt</mat-icon>
+ <h3 matLine translate>Payrolls</h3>
+ <p matLine translate>Manage payrolls</p>
+ </a>
+ </mat-nav-list>
+ <div layout="column" flex-gt-md="100" right>
+ <h3 translate>Ledger</h3>
+ <fims-data-table flex (onActionCellClick)="rowSelect($event)" [columns]="columns" [data]="ledgerData | async"></fims-data-table>
+ </div>
+ </fims-two-column-layout>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Create ledger' | translate}}" icon="add" [link]="['create']" [permission]="{ id: 'accounting_ledgers', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/accounting/general-ledger.component.ts b/src/app/accounting/general-ledger.component.ts
new file mode 100644
index 0000000..d688a50
--- /dev/null
+++ b/src/app/accounting/general-ledger.component.ts
@@ -0,0 +1,61 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import {ActivatedRoute, Params, Router} from '@angular/router';
+import {Ledger} from '../services/accounting/domain/ledger.model';
+import {TableData} from '../common/data-table/data-table.component';
+import * as fromAccounting from './store';
+import {LOAD_ALL_TOP_LEVEL} from './store/ledger/ledger.actions';
+import {Observable} from 'rxjs/Observable';
+import {AccountingStore} from './store/index';
+
+@Component({
+ templateUrl: './general-ledger.component.html'
+})
+export class GeneralLedgerComponent implements OnInit {
+
+ ledgerData: Observable<TableData>;
+
+ columns: any[] = [
+ { name: 'identifier', label: 'Id', tooltip: 'Id' },
+ { name: 'name', label: 'Name', tooltip: 'Name' },
+ { name: 'description', label: 'Description', tooltip: 'Description' },
+ { name: 'totalValue', label: 'Balance', tooltip: 'Balance', format: value => value ? value.toFixed(2) : '-' }
+ ];
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: AccountingStore) {}
+
+ ngOnInit(): void {
+ this.ledgerData = this.store.select(fromAccounting.getAllTopLevelLedgerEntities)
+ .map(ledgers => ({
+ data: ledgers,
+ totalElements: ledgers.length,
+ totalPages: 1
+ }));
+
+ this.route.queryParams.subscribe((params: Params) => {
+ this.store.dispatch({ type: LOAD_ALL_TOP_LEVEL });
+ });
+ }
+
+ rowSelect(ledger: Ledger): void {
+ this.router.navigate(['ledgers/detail', ledger.identifier, 'ledgers'], { relativeTo: this.route });
+ }
+
+}
diff --git a/src/app/accounting/incomeStatement/income-statement.component.html b/src/app/accounting/incomeStatement/income-statement.component.html
new file mode 100644
index 0000000..2b008a9
--- /dev/null
+++ b/src/app/accounting/incomeStatement/income-statement.component.html
@@ -0,0 +1,118 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Income statement' | translate}}" [navigateBackTo]="['../']">
+ <div layout="row">
+ <table td-data-table *ngIf="incomeStatement$ | async as incomeStatement">
+ <ng-container *ngIf="income$ | async as income">
+ <tr td-data-table-row>
+ <td td-data-table-cell>
+ <b translate>Income</b>
+ </td>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell></td>
+ </tr>
+ <tr td-data-table-row *ngFor="let entry of income.incomeStatementEntries">
+ <td td-data-table-cell></td>
+ <td td-data-table-cell>
+ {{entry.description}}
+ </td>
+ <td td-data-table-cell>
+ {{entry.value | displayFimsFinancialNumber}}
+ </td>
+ </tr>
+ <tr td-data-table-row>
+ <td td-data-table-cell>
+ <i translate>Subtotal</i>
+ </td>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell>
+ <i class="double-underline">
+ {{income.subtotal | displayFimsFinancialNumber}}
+ </i>
+ </td>
+ </tr>
+ <tr td-data-table-row></tr>
+ <tr td-data-table-row>
+ <td td-data-table-cell>
+ <b translate>Gross Profit</b>
+ </td>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell>
+ <b class="double-underline">
+ {{incomeStatement.grossProfit | displayFimsFinancialNumber}}
+ </b>
+ </td>
+ </tr>
+ <tr td-data-table-row></tr>
+ </ng-container>
+ <ng-container *ngIf="expenses$ | async as expenses">
+ <tr td-data-table-row>
+ <td td-data-table-cell>
+ <b translate>Expenses</b>
+ </td>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell></td>
+ </tr>
+ <tr td-data-table-row *ngFor="let entry of expenses.incomeStatementEntries">
+ <td td-data-table-cell></td>
+ <td td-data-table-cell>
+ {{entry.description}}
+ </td>
+ <td td-data-table-cell>
+ {{entry.value | displayFimsFinancialNumber}}
+ </td>
+ </tr>
+ <tr td-data-table-row>
+ <td td-data-table-cell>
+ <i translate>Subtotal</i>
+ </td>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell>
+ <i class="double-underline">
+ {{expenses.subtotal | displayFimsFinancialNumber}}
+ </i>
+ </td>
+ </tr>
+ <tr td-data-table-row></tr>
+ <tr td-data-table-row>
+ <td td-data-table-cell>
+ <b translate>Total expenses</b>
+ </td>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell>
+ <b class="double-underline">
+ {{incomeStatement.totalExpenses | displayFimsFinancialNumber}}
+ </b>
+ </td>
+ </tr>
+ <tr td-data-table-row></tr>
+ <tr td-data-table-row>
+ <td td-data-table-cell>
+ <b translate>Net Income (Loss)</b>
+ </td>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell>
+ <b class="double-underline">
+ {{incomeStatement.netIncome | displayFimsFinancialNumber}}
+ </b>
+ </td>
+ </tr>
+ </ng-container>
+ </table>
+ </div>
+</fims-layout-card-over>
diff --git a/src/app/accounting/incomeStatement/income-statement.component.scss b/src/app/accounting/incomeStatement/income-statement.component.scss
new file mode 100644
index 0000000..9580096
--- /dev/null
+++ b/src/app/accounting/incomeStatement/income-statement.component.scss
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+.double-underline {
+ border-bottom: 3px double;
+}
diff --git a/src/app/accounting/incomeStatement/income-statement.component.ts b/src/app/accounting/incomeStatement/income-statement.component.ts
new file mode 100644
index 0000000..6588e2e
--- /dev/null
+++ b/src/app/accounting/incomeStatement/income-statement.component.ts
@@ -0,0 +1,50 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component} from '@angular/core';
+import {AccountingService} from '../../services/accounting/accounting.service';
+import {IncomeStatement} from '../../services/accounting/domain/income-statement.model';
+import {Observable} from 'rxjs/Observable';
+import {IncomeStatementSection} from '../../services/accounting/domain/income-statement-section.model';
+
+@Component({
+ templateUrl: './income-statement.component.html',
+ styleUrls: ['./income-statement.component.scss']
+})
+export class IncomeStatementComponent {
+
+ incomeStatement$: Observable<IncomeStatement>;
+
+ income$: Observable<IncomeStatementSection>;
+
+ expenses$: Observable<IncomeStatementSection>;
+
+ constructor(private accountingService: AccountingService) {
+ this.incomeStatement$ = accountingService.getIncomeStatement().share();
+
+ this.income$ = this.incomeStatement$
+ .map(statement => statement.incomeStatementSections
+ .find(section => section.type === 'INCOME')
+ );
+
+ this.expenses$ = this.incomeStatement$
+ .map(statement => statement.incomeStatementSections
+ .find(section => section.type === 'EXPENSES')
+ );
+ }
+}
diff --git a/src/app/accounting/journalEntries/form/create.form.component.html b/src/app/accounting/journalEntries/form/create.form.component.html
new file mode 100644
index 0000000..1a2bccb
--- /dev/null
+++ b/src/app/accounting/journalEntries/form/create.form.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Create new journal entry' | translate}}">
+ <fims-journal-entry-form
+ [journalEntry]="journalEntry$ | async"
+ [error]="error$ | async"
+ (onSave)="save($event)"
+ (onCancel)="cancel()">
+ </fims-journal-entry-form>
+</fims-layout-card-over>
diff --git a/src/app/accounting/journalEntries/form/create.form.component.ts b/src/app/accounting/journalEntries/form/create.form.component.ts
new file mode 100644
index 0000000..77400c5
--- /dev/null
+++ b/src/app/accounting/journalEntries/form/create.form.component.ts
@@ -0,0 +1,83 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {JournalEntry} from '../../../services/accounting/domain/journal-entry.model';
+import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {TdStepComponent} from '@covalent/core';
+import * as fromAccounting from '../../store';
+import * as fromRoot from '../../../store';
+import {CREATE, RESET_FORM} from '../../store/ledger/journal-entry/journal-entry.actions';
+import {Error} from '../../../services/domain/error.model';
+import {AccountingStore} from '../../store/index';
+import {Observable} from 'rxjs/Observable';
+import {todayAsISOString} from '../../../services/domain/date.converter';
+
+@Component({
+ templateUrl: './create.form.component.html'
+})
+export class CreateJournalEntryFormComponent implements OnInit, OnDestroy {
+
+ @ViewChild('detailsStep') detailsStep: TdStepComponent;
+
+ journalEntry$: Observable<JournalEntry>;
+
+ error$: Observable<Error>;
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: AccountingStore) {
+ }
+
+ ngOnInit(): void {
+ this.error$ = this.store.select(fromAccounting.getJournalEntryFormError)
+ .filter((error: Error) => !!error);
+
+ this.journalEntry$ = this.store.select(fromRoot.getUsername)
+ .map(username => ({
+ transactionIdentifier: '',
+ transactionDate: todayAsISOString(),
+ transactionType: '',
+ clerk: username,
+ debtors: [
+ { accountNumber: '', amount: '0' }
+ ],
+ creditors: [
+ { accountNumber: '', amount: '0' }
+ ]
+ }));
+ }
+
+ ngOnDestroy(): void {
+ this.store.dispatch({ type: RESET_FORM });
+ }
+
+ save(journalEntry: JournalEntry): void {
+ this.store.dispatch({ type: CREATE, payload: {
+ journalEntry,
+ activatedRoute: this.route
+ } });
+ }
+
+ cancel() {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], {relativeTo: this.route});
+ }
+
+}
diff --git a/src/app/accounting/journalEntries/form/form.component.html b/src/app/accounting/journalEntries/form/form.component.html
new file mode 100644
index 0000000..163ed24
--- /dev/null
+++ b/src/app/accounting/journalEntries/form/form.component.html
@@ -0,0 +1,121 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+ <form [formGroup]="form">
+ <td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Journal Entry details' | translate}}" layout="column">
+ <fims-id-input [form]="form" controlName="transactionIdentifier" [readonly]="false"></fims-id-input>
+ <fims-date-input placeholder="{{'Transaction date' | translate}}" [form]="form" controlName="transactionDate"></fims-date-input>
+ <fims-transaction-type-select title="{{'Transaction type' | translate}}" formControlName="transactionType">
+ <ng-container *ngIf="!form.get('transactionType').pristine && form.get('transactionType').hasError('required')" translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="form.get('transactionType').hasError('invalidTransactionType')" translate>
+ Invalid transaction type
+ </ng-container>
+ </fims-transaction-type-select>
+ <div layout="row">
+ <mat-form-field layout-margin flex>
+ <textarea matInput placeholder="{{'Note(Optional)' | translate}}" formControlName="note"></textarea>
+ </mat-form-field>
+ </div>
+ <div layout="row">
+ <mat-form-field layout-margin flex>
+ <textarea matInput flex placeholder="{{'Message(Optional)' | translate}}" formControlName="message"></textarea>
+ </mat-form-field>
+ </div>
+ <ng-template td-step-actions>
+ <fims-form-continue-action (onContinue)="accountsStep.open()"></fims-form-continue-action>
+ </ng-template>
+ </td-step>
+ <td-step #accountsStep label="{{'Affected Accounts' | translate}}">
+ <div layout-gt-xs="column">
+ <div layout="row">
+ <mat-card flex formArrayName="debtors">
+ <mat-card-title>Debit</mat-card-title>
+ <mat-card-content>
+ <div layout="row" layout-align="start center" [formGroupName]="i" *ngFor="let debtor of debtors; let i = index;">
+ <fims-account-select formControlName="accountNumber" [title]="'Account' | translate" flex="50">
+ <ng-container *ngIf="!debtor.get('accountNumber').pristine && debtor.get('accountNumber').hasError('required')"
+ translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="debtor.get('accountNumber').hasError('invalidAccount')" translate>
+ Invalid account
+ </ng-container>
+ </fims-account-select>
+ <fims-text-input type="number" [form]="debtor" controlName="amount"
+ placeholder="{{'Amount' | translate}}"></fims-text-input>
+ <button mat-raised-button color="warn" title="{{'Remove' | translate}}" (click)="removeDebtor(i)">{{'Remove' |
+ translate}}
+ </button>
+ </div>
+ <p *ngIf="form.get('debtors').hasError('minItemsInvalid')" class="tc-red-600" translate>
+ Please add at least one debit.
+ </p>
+ </mat-card-content>
+ <mat-card-actions>
+ <button mat-button mat-raised-button (click)="addDebtor()">{{'ADD DEBIT' | translate}}</button>
+ </mat-card-actions>
+ </mat-card>
+ <mat-card flex formArrayName="creditors">
+ <mat-card-title>Credit</mat-card-title>
+ <mat-card-content>
+ <div layout="row" layout-align="start center" [formGroupName]="i" *ngFor="let creditor of creditors; let i = index;">
+ <fims-account-select formControlName="accountNumber" [title]="'Account' | translate" flex="50">
+ <ng-container *ngIf="!creditor.get('accountNumber').pristine && creditor.get('accountNumber').hasError('required')"
+ translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="creditor.get('accountNumber').hasError('invalidAccount')" translate>
+ Invalid account
+ </ng-container>
+ </fims-account-select>
+ <fims-text-input type="number" [form]="creditor" controlName="amount"
+ placeholder="{{'Amount' | translate}}"></fims-text-input>
+ <button mat-raised-button color="warn" title="{{'Remove' | translate}}" (click)="removeCreditor(i)">{{'Remove' |
+ translate}}
+ </button>
+ </div>
+ <p *ngIf="form.get('creditors').hasError('minItemsInvalid')" class="tc-red-600" translate>
+ Please add at least one credit.
+ </p>
+ </mat-card-content>
+ <mat-card-actions>
+ <button mat-button mat-raised-button (click)="addCreditor()">{{'ADD CREDIT' | translate}}</button>
+ </mat-card-actions>
+ </mat-card>
+
+ </div>
+ <p *ngIf="!form.pristine && form.hasError('sumInvalid')" class="tc-red-600" translate>
+ Sum of debit and sum of credit must match.
+ </p>
+ </div>
+ </td-step>
+ <td-step label="{{'Final step' | translate}}" [state]="'complete'">
+ <ng-template td-step-summary>
+ <fims-form-final-action
+ [resourceName]="'JOURNAL ENTRY'"
+ [editMode]="false"
+ [disabled]="!form.valid"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+ </td-steps>
+ </form>
diff --git a/src/app/accounting/journalEntries/form/form.component.spec.ts b/src/app/accounting/journalEntries/form/form.component.spec.ts
new file mode 100644
index 0000000..f90d337
--- /dev/null
+++ b/src/app/accounting/journalEntries/form/form.component.spec.ts
@@ -0,0 +1,179 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, DebugElement, ViewChild} from '@angular/core';
+import {JournalEntryFormComponent} from './form.component';
+import {JournalEntry} from '../../../services/accounting/domain/journal-entry.model';
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {FimsSharedModule} from '../../../common/common.module';
+import {ReactiveFormsModule} from '@angular/forms';
+import {CovalentStepsModule} from '@covalent/core';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {AccountingService} from '../../../services/accounting/accounting.service';
+import {Observable} from 'rxjs/Observable';
+import {By} from '@angular/platform-browser';
+import {MatAutocompleteModule, MatCardModule, MatInputModule, MatOptionModule} from '@angular/material';
+import {TransactionTypeSelectComponent} from './transaction-type-select/transaction-type-select.component';
+
+describe('Test JournalEntryFormComponent', () => {
+
+ let fixture: ComponentFixture<TestComponent>;
+
+ let testComponent: TestComponent;
+
+ let baseDate: Date;
+
+ function mockValidJournalEntry(date: Date): JournalEntry {
+ const journalEntry: JournalEntry = {
+ transactionIdentifier: 'testId',
+ transactionDate: date.toISOString(),
+ transactionType: 'transactionType',
+ note: 'testNote',
+ message: 'testMessage',
+ debtors: [
+ { accountNumber: '1234', amount: '11' }
+ ],
+ creditors: [
+ { accountNumber: '5678', amount: '11' }
+ ],
+ clerk: 'testClerk'
+ };
+
+ return journalEntry;
+ }
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [
+ TestComponent,
+ TransactionTypeSelectComponent,
+ JournalEntryFormComponent
+ ],
+ imports: [
+ TranslateModule.forRoot(),
+ FimsSharedModule,
+ ReactiveFormsModule,
+ MatAutocompleteModule,
+ MatOptionModule,
+ MatInputModule,
+ MatCardModule,
+ ReactiveFormsModule,
+ CovalentStepsModule,
+ NoopAnimationsModule
+ ],
+ providers: [
+ {
+ provide: AccountingService, useClass: class {
+ findTransactionType = jasmine.createSpy('findTransactionType').and.returnValue(Observable.of(null));
+ fetchTransactionTypes = jasmine.createSpy('fetchTransactionTypes').and.returnValue(Observable.of([
+ { code: 'transactionType', name: 'transactionType' }
+ ]));
+ findAccount = jasmine.createSpy('findAccount').and.returnValue(Observable.of(null));
+ fetchAccounts = jasmine.createSpy('fetchAccounts').and.returnValue(Observable.of([
+ { identifier: '1234', name: '1234' },
+ { identifier: '5678', name: '1234' }
+ ]));
+ }}
+ ]
+ });
+
+ fixture = TestBed.createComponent(TestComponent);
+ testComponent = fixture.componentInstance;
+ });
+
+ beforeEach(() => {
+ jasmine.clock().install();
+ baseDate = new Date(2017, 1, 1);
+ baseDate.setUTCHours(0, 0, 0, 1);
+ jasmine.clock().mockDate(baseDate);
+ });
+
+ afterEach(() => {
+ jasmine.clock().uninstall();
+ });
+
+ function clickSaveButton(): void {
+ const button: DebugElement = fixture.debugElement.query(By.css('.mat-raised-button.mat-primary'));
+
+ expect(button.properties['disabled']).toBeFalsy('Button should be enabled');
+
+ button.nativeElement.click();
+ }
+
+ it('should save correct values', () => {
+ const journalEntry: JournalEntry = mockValidJournalEntry(baseDate);
+
+ testComponent.journalEntry = journalEntry;
+
+ fixture.detectChanges();
+
+ clickSaveButton();
+
+ expect(testComponent.savedJournalEntry).toEqual(journalEntry);
+ });
+
+ it('should disable button when form is invalid', () => {
+ const journalEntry: JournalEntry = mockValidJournalEntry(baseDate);
+
+ journalEntry.transactionType = '';
+
+ testComponent.journalEntry = journalEntry;
+
+ fixture.detectChanges();
+
+ const button: DebugElement = fixture.debugElement.query(By.css('.mat-raised-button.mat-primary'));
+
+ expect(button.properties['disabled']).toBeTruthy('Button should be disabled');
+ });
+
+ it('should render accounts', () => {
+ const journalEntry: JournalEntry = mockValidJournalEntry(baseDate);
+
+ testComponent.journalEntry = journalEntry;
+
+ fixture.detectChanges();
+
+ // Choose placeholder as selector as I could not find any other attribute to select on
+ const debugElement: DebugElement[] = fixture.debugElement.queryAll(By.css('input[placeholder="Account"]'));
+
+ // 1 debtor, 1 creditor
+ expect(debugElement.length).toEqual(2);
+ });
+});
+
+@Component({
+ template: `
+ <fims-journal-entry-form #form (onSave)="onSave($event)" (onCancel)="onCancel()" [journalEntry]="journalEntry">
+ </fims-journal-entry-form>`
+})
+class TestComponent {
+
+ @ViewChild('form') formComponent: JournalEntryFormComponent;
+
+ journalEntry: JournalEntry;
+
+ savedJournalEntry: JournalEntry;
+
+ user: 'test';
+
+ onSave(journalEntry: JournalEntry): void {
+ this.savedJournalEntry = journalEntry;
+ }
+
+}
diff --git a/src/app/accounting/journalEntries/form/form.component.ts b/src/app/accounting/journalEntries/form/form.component.ts
new file mode 100644
index 0000000..78e927e
--- /dev/null
+++ b/src/app/accounting/journalEntries/form/form.component.ts
@@ -0,0 +1,157 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {JournalEntry} from '../../../services/accounting/domain/journal-entry.model';
+import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild} from '@angular/core';
+import {AbstractControl, FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {TdStepComponent} from '@covalent/core';
+import {addCurrentTime, parseDate} from '../../../services/domain/date.converter';
+import {FimsValidators} from '../../../common/validator/validators';
+import {Error} from '../../../services/domain/error.model';
+import {JournalEntryValidators} from './journal-entry.validator';
+import {AccountingService} from '../../../services/accounting/accounting.service';
+import {transactionTypeExists} from './transaction-type-select/validator/transaction-type-exists.validator';
+import {accountExists} from '../../../common/validator/account-exists.validator';
+import {Creditor} from '../../../services/accounting/domain/creditor.model';
+import {Debtor} from '../../../services/accounting/domain/debtor.model';
+
+@Component({
+ selector: 'fims-journal-entry-form',
+ templateUrl: './form.component.html'
+})
+export class JournalEntryFormComponent implements OnInit, OnChanges {
+
+ @ViewChild('detailsStep') detailsStep: TdStepComponent;
+
+ @Input() journalEntry: JournalEntry;
+
+ @Input() error: Error;
+
+ @Output('onSave') onSave = new EventEmitter<JournalEntry>();
+
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ form: FormGroup;
+
+ constructor(private formBuilder: FormBuilder, private accountingService: AccountingService) {
+
+ this.form = this.formBuilder.group({
+ transactionIdentifier: ['', [Validators.required, Validators.minLength(3), Validators.maxLength(32), FimsValidators.urlSafe]],
+ transactionType: ['', [Validators.required], transactionTypeExists(this.accountingService)],
+ transactionDate: ['', Validators.required],
+ note: [''],
+ message: [''],
+ creditors: this.formBuilder.array([], JournalEntryValidators.minItems(1)),
+ debtors: this.formBuilder.array([], JournalEntryValidators.minItems(1))
+ }, { validator: JournalEntryValidators.equalSum('creditors', 'debtors') });
+
+ }
+
+ ngOnInit(): void {
+ this.detailsStep.open();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes.journalEntry) {
+ this.form.reset({
+ transactionIdentifier: this.journalEntry.transactionIdentifier,
+ transactionType: this.journalEntry.transactionType,
+ transactionDate: this.journalEntry.transactionDate,
+ note: this.journalEntry.note,
+ message: this.journalEntry.message
+ });
+
+ this.journalEntry.debtors.forEach(debtor => this.addDebtor(debtor));
+ this.journalEntry.creditors.forEach(creditor => this.addCreditor(creditor));
+ }
+
+ if (changes.error) {
+ this.form.get('transactionIdentifier').setErrors({
+ unique: true
+ });
+ this.detailsStep.open();
+ }
+ }
+
+ save(): void {
+ const date: Date = parseDate(this.form.get('transactionDate').value);
+ const dateWithTime = addCurrentTime(date);
+
+ const journalEntry: JournalEntry = {
+ transactionIdentifier: this.form.get('transactionIdentifier').value,
+ transactionType: this.form.get('transactionType').value,
+ transactionDate: dateWithTime.toISOString(),
+ clerk: this.journalEntry.clerk,
+ note: this.form.get('note').value,
+ message: this.form.get('message').value,
+ creditors: this.form.get('creditors').value,
+ debtors: this.form.get('debtors').value,
+ };
+
+ this.onSave.emit(journalEntry);
+ }
+
+ addCreditor(creditor?: Creditor): void {
+ const control: FormArray = this.form.get('creditors') as FormArray;
+ control.push(this.initCreditor(creditor));
+ }
+
+ removeCreditor(index: number): void {
+ const control: FormArray = this.form.get('creditors') as FormArray;
+ control.removeAt(index);
+ }
+
+ addDebtor(debtor?: Debtor): void {
+ const control: FormArray = this.form.get('debtors') as FormArray;
+ control.push(this.initDebtor(debtor));
+ }
+
+ removeDebtor(index: number): void {
+ const control: FormArray = this.form.get('debtors') as FormArray;
+ control.removeAt(index);
+ }
+
+ cancel() {
+ this.onCancel.emit();
+ }
+
+ get debtors(): AbstractControl[] {
+ const debtors: FormArray = this.form.get('debtors') as FormArray;
+ return debtors.controls;
+ }
+
+ get creditors(): AbstractControl[] {
+ const creditors: FormArray = this.form.get('creditors') as FormArray;
+ return creditors.controls;
+ }
+
+ private initCreditor(creditor: Creditor = { accountNumber: '', amount: '0' }): FormGroup {
+ return this.formBuilder.group({
+ accountNumber: [creditor.accountNumber, [Validators.required], accountExists(this.accountingService)],
+ amount: [creditor.amount, [Validators.required, FimsValidators.greaterThanValue(0)]]
+ });
+ }
+
+ private initDebtor(debtor: Debtor = { accountNumber: '', amount: '0' }): FormGroup {
+ return this.formBuilder.group({
+ accountNumber: [debtor.accountNumber, [Validators.required], accountExists(this.accountingService)],
+ amount: [debtor.amount, [Validators.required, FimsValidators.greaterThanValue(0)]]
+ });
+ }
+
+}
diff --git a/src/app/accounting/journalEntries/form/journal-entry.validator.spec.ts b/src/app/accounting/journalEntries/form/journal-entry.validator.spec.ts
new file mode 100644
index 0000000..ed3156f
--- /dev/null
+++ b/src/app/accounting/journalEntries/form/journal-entry.validator.spec.ts
@@ -0,0 +1,90 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {FormArray, FormBuilder} from '@angular/forms';
+import {JournalEntryValidators} from './journal-entry.validator';
+
+describe('JournalEntryValidators', () => {
+
+ const formBuilder: FormBuilder = new FormBuilder();
+
+ describe('minItems', () => {
+
+ function setup(): FormArray {
+ return formBuilder.array([
+ formBuilder.group({ amount: [10] }),
+ ]);
+ }
+
+ it('should not return error when array contains items', () => {
+ const validator = JournalEntryValidators.minItems(1);
+
+ const formArray = setup();
+
+ expect(validator(formArray)).toBeNull();
+ });
+
+ it('should return error when array contains less items as specified', () => {
+ const validator = JournalEntryValidators.minItems(2);
+
+ const formArray = setup();
+
+ expect(validator(formArray)).toEqual({minItemsInvalid: true});
+ });
+
+ });
+
+ describe('equalSum', () => {
+
+ it('should not return error when sum equal', () => {
+ const validator = JournalEntryValidators.equalSum('valOne', 'valTwo');
+
+ const formGroup = formBuilder.group({
+ valOne: formBuilder.array([
+ formBuilder.group({ amount: [10] }),
+ formBuilder.group({ amount: [10] }),
+ ]),
+ valTwo: formBuilder.array([
+ formBuilder.group({ amount: [10] }),
+ formBuilder.group({ amount: [10] }),
+ ]),
+ });
+
+ expect(validator(formGroup)).toBeNull();
+ });
+
+ it('should return error when sum not equal', () => {
+ const validator = JournalEntryValidators.equalSum('valOne', 'valTwo');
+
+ const formGroup = formBuilder.group({
+ valOne: formBuilder.array([
+ formBuilder.group({ amount: [10] }),
+ formBuilder.group({ amount: [10] }),
+ ]),
+ valTwo: formBuilder.array([
+ formBuilder.group({ amount: [10] }),
+ ]),
+ });
+
+ expect(validator(formGroup)).toEqual({sumInvalid: true});
+ });
+
+ });
+
+});
diff --git a/src/app/accounting/journalEntries/form/journal-entry.validator.ts b/src/app/accounting/journalEntries/form/journal-entry.validator.ts
new file mode 100644
index 0000000..6e3a28b
--- /dev/null
+++ b/src/app/accounting/journalEntries/form/journal-entry.validator.ts
@@ -0,0 +1,63 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {FormArray, FormGroup} from '@angular/forms';
+
+export class JournalEntryValidators {
+
+ static minItems(min: number = 1) {
+ return (formArray: FormArray): { [key: string]: any } => {
+ const minLength = min - 1;
+ if (formArray.length <= minLength) {
+ return {
+ minItemsInvalid: true
+ };
+ }
+
+ return null;
+ };
+ }
+
+ static equalSum(firstValue: string, secondValue: string) {
+ return (group: FormGroup): { [key: string]: any } => {
+ const firstSum: number = this.sum(group.get(firstValue).value);
+
+ const secondSum: number = this.sum(group.get(secondValue).value);
+
+ if (firstSum !== secondSum) {
+ return {
+ sumInvalid: true
+ };
+ }
+
+ return null;
+ };
+ }
+
+ private static sum(accounts: any[]): number {
+ let sum = 0;
+
+ for (const account of accounts) {
+ sum += parseInt(account.amount, 10);
+ }
+
+ return sum;
+ }
+
+}
diff --git a/src/app/accounting/journalEntries/form/transaction-type-select/transaction-type-select.component.html b/src/app/accounting/journalEntries/form/transaction-type-select/transaction-type-select.component.html
new file mode 100644
index 0000000..95e7aff
--- /dev/null
+++ b/src/app/accounting/journalEntries/form/transaction-type-select/transaction-type-select.component.html
@@ -0,0 +1,31 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<div layout="row">
+ <mat-form-field layout-margin flex>
+ <input matInput [placeholder]="title" [matAutocomplete]="auto" [formControl]="formControl">
+ <mat-hint class="tc-red-600">
+ <ng-content></ng-content>
+ </mat-hint>
+ </mat-form-field>
+</div>
+
+<mat-autocomplete #auto="matAutocomplete">
+ <mat-option *ngFor="let transactionType of transactionTypes | async" [value]="transactionType.code">
+ {{ transactionType.code }}({{ transactionType.name }})
+ </mat-option>
+</mat-autocomplete>
diff --git a/src/app/accounting/journalEntries/form/transaction-type-select/transaction-type-select.component.ts b/src/app/accounting/journalEntries/form/transaction-type-select/transaction-type-select.component.ts
new file mode 100644
index 0000000..bc0b81f
--- /dev/null
+++ b/src/app/accounting/journalEntries/form/transaction-type-select/transaction-type-select.component.ts
@@ -0,0 +1,96 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {Component, forwardRef, Input, OnInit} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';
+import {TransactionType} from '../../../../services/accounting/domain/transaction-type.model';
+import {AccountingService} from '../../../../services/accounting/accounting.service';
+import {FetchRequest} from '../../../../services/domain/paging/fetch-request.model';
+import {TransactionTypePage} from '../../../../services/accounting/domain/transaction-type-page.model';
+
+const noop: () => void = () => {
+ // empty method
+};
+
+@Component({
+ providers: [
+ {provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TransactionTypeSelectComponent), multi: true}
+ ],
+ selector: 'fims-transaction-type-select',
+ templateUrl: './transaction-type-select.component.html'
+})
+export class TransactionTypeSelectComponent implements ControlValueAccessor, OnInit {
+
+ private _onTouchedCallback: () => void = noop;
+
+ private _onChangeCallback: (_: any) => void = noop;
+
+ formControl: FormControl;
+
+ @Input() title: string;
+
+ @Input() required: boolean;
+
+ transactionTypes: Observable<TransactionType[]>;
+
+ constructor(private accountingService: AccountingService) {
+ }
+
+ ngOnInit(): void {
+ this.formControl = new FormControl('');
+
+ this.transactionTypes = this.formControl.valueChanges
+ .distinctUntilChanged()
+ .debounceTime(500)
+ .do(name => this.changeValue(name))
+ .filter(name => name)
+ .switchMap(name => this.onSearch(name));
+ }
+
+ changeValue(value: string): void {
+ this._onChangeCallback(value);
+ }
+
+ writeValue(value: any): void {
+ this.formControl.setValue(value);
+ }
+
+ registerOnChange(fn: any): void {
+ this._onChangeCallback = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ this._onTouchedCallback = fn;
+ }
+
+ onSearch(searchTerm?: string): Observable<TransactionType[]> {
+ const fetchRequest: FetchRequest = {
+ page: {
+ pageIndex: 0,
+ size: 5
+ },
+ searchTerm: searchTerm
+ };
+
+ return this.accountingService.fetchTransactionTypes(fetchRequest)
+ .map((transactionTypePage: TransactionTypePage) => transactionTypePage.transactionTypes);
+ }
+
+}
diff --git a/src/app/accounting/journalEntries/form/transaction-type-select/validator/transaction-type-exists.validator.ts b/src/app/accounting/journalEntries/form/transaction-type-select/validator/transaction-type-exists.validator.ts
new file mode 100644
index 0000000..62f8399
--- /dev/null
+++ b/src/app/accounting/journalEntries/form/transaction-type-select/validator/transaction-type-exists.validator.ts
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {AbstractControl, AsyncValidatorFn} from '@angular/forms';
+import {Observable} from 'rxjs/Observable';
+import {AccountingService} from '../../../../../services/accounting/accounting.service';
+import {isEmptyInputValue, isString} from '../../../../../common/validator/validators';
+
+const invalid = Observable.of({
+ invalidTransactionType: true
+});
+
+export function transactionTypeExists(accountingService: AccountingService): AsyncValidatorFn {
+ return (control: AbstractControl): Observable<any> => {
+ if (!control.dirty || isEmptyInputValue(control.value)) {
+ return Observable.of(null);
+ }
+
+ if (isString(control.value) && control.value.trim().length === 0) {
+ return invalid;
+ }
+
+ return accountingService.findTransactionType(control.value, true)
+ .map(account => null)
+ .catch(() => invalid);
+ };
+}
diff --git a/src/app/accounting/journalEntries/journal-entry.list.component.html b/src/app/accounting/journalEntries/journal-entry.list.component.html
new file mode 100644
index 0000000..91bf533
--- /dev/null
+++ b/src/app/accounting/journalEntries/journal-entry.list.component.html
@@ -0,0 +1,96 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Journal entries' | translate}}" [navigateBackTo]="['../']">
+ <form [formGroup]="form" layout="row">
+ <mat-form-field layout-margin>
+ <input matInput type="date" placeholder="{{'Start date' | translate}}" formControlName="startDate">
+ </mat-form-field>
+ <mat-form-field layout-margin>
+ <input matInput type="date" placeholder="{{'End date' | translate}}" formControlName="endDate">
+ <mat-error *ngIf="form.hasError('rangeInvalid')" translate>Invalid date range</mat-error>
+ </mat-form-field>
+ <fims-text-input [form]="form" controlName="account" placeholder="{{'Account' | translate}}"></fims-text-input>
+ <fims-text-input type="number" [form]="form" controlName="amount" placeholder="{{'Amount' | translate}}"></fims-text-input>
+ <span>
+ <button layout-margin mat-button mat-icon-button (click)="fetchJournalEntries()" [disabled]="!form.valid"><mat-icon>search</mat-icon></button>
+ </span>
+ </form>
+ <fims-two-column-layout>
+ <mat-nav-list left>
+ <a mat-list-item *ngFor="let entry of (journalEntries$ | async)" (click)="select(entry)" [ngClass]="journalEntry == entry ? 'active' : false">
+ <mat-icon matListAvatar>assignment</mat-icon>
+ <h4 matLine translate>{{entry.transactionDate | date:'short'}}</h4>
+ <p matLine translate>{{entry.transactionType}}</p>
+ <p matLine translate>Amount: {{sumDebtors(entry.debtors) | number:numberFormat}}</p>
+ <ng-container [ngSwitch]="entry.state">
+ <mat-icon class="tc-amber-800" *ngSwitchCase="'PENDING'">more_horiz</mat-icon>
+ <mat-icon class="tc-green-800" *ngSwitchCase="'PROCESSED'">done</mat-icon>
+ </ng-container>
+ </a>
+ </mat-nav-list>
+ <mat-card right *ngIf="journalEntry">
+ <mat-list>
+ <mat-list-item>
+ <h3 matLine translate>Clerk</h3>
+ <p matLine>{{journalEntry?.clerk}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Message</h3>
+ <p matLine>{{journalEntry?.message}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Note</h3>
+ <p matLine>{{journalEntry?.note}}</p>
+ </mat-list-item>
+ </mat-list>
+ <div class="mat-content">
+ <table td-data-table>
+ <thead>
+ <tr td-data-table-column-row>
+ <th td-data-table-column translate>
+ Debit
+ </th>
+ <th td-data-table-column translate>
+ Credit
+ </th>
+ <th td-data-table-column translate>
+ Amount
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr td-data-table-row *ngFor="let debit of journalEntry?.debtors">
+ <td td-data-table-cell>
+ {{debit.accountNumber}}
+ </td>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell>{{debit.amount | number:numberFormat}}</td>
+ </tr>
+ <tr td-data-table-row *ngFor="let credit of journalEntry?.creditors">
+ <td td-data-table-cell></td>
+ <td td-data-table-cell>{{credit.accountNumber}}</td>
+ <td td-data-table-cell>{{credit.amount | number:numberFormat}}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </mat-card>
+ </fims-two-column-layout>
+
+</fims-layout-card-over>
+<fims-fab-button title="{{'Add Journal Entry' | translate}}" icon="add" [link]="['create']" [permission]="{ id: 'accounting_journals', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/accounting/journalEntries/journal-entry.list.component.ts b/src/app/accounting/journalEntries/journal-entry.list.component.ts
new file mode 100644
index 0000000..6fc918d
--- /dev/null
+++ b/src/app/accounting/journalEntries/journal-entry.list.component.ts
@@ -0,0 +1,90 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import {FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {todayAsISOString, toShortISOString} from '../../services/domain/date.converter';
+import {FimsValidators} from '../../common/validator/validators';
+import * as fromAccounting from '../store';
+import {SEARCH} from '../store/ledger/journal-entry/journal-entry.actions';
+import {Observable} from 'rxjs/Observable';
+import {AccountingStore} from '../store/index';
+import {DatePipe} from '@angular/common';
+import {JournalEntry} from '../../services/accounting/domain/journal-entry.model';
+import {Debtor} from '../../services/accounting/domain/debtor.model';
+
+@Component({
+ templateUrl: './journal-entry.list.component.html',
+ providers: [DatePipe]
+})
+export class JournalEntryListComponent implements OnInit {
+
+ numberFormat = '1.2-2';
+
+ form: FormGroup;
+
+ journalEntries$: Observable<JournalEntry[]>;
+
+ journalEntry: JournalEntry;
+
+ constructor(private formBuilder: FormBuilder, private store: AccountingStore) {
+ }
+
+ ngOnInit(): void {
+ this.journalEntries$ = this.store.select(fromAccounting.getJournalEntriesSearchResult)
+ .do(journalEntries => this.select(journalEntries.length > 0 ? journalEntries[0] : undefined));
+
+ const today = todayAsISOString();
+
+ this.form = this.formBuilder.group({
+ 'startDate': [today, [Validators.required]],
+ 'endDate': [today, [Validators.required]],
+ 'account': [],
+ 'amount': [],
+ }, {validator: FimsValidators.matchRange('startDate', 'endDate')});
+
+ this.fetchJournalEntries();
+ }
+
+ fetchJournalEntries(): void {
+ const startDate = toShortISOString(this.form.get('startDate').value);
+ const endDate = toShortISOString(this.form.get('endDate').value);
+ const account = this.form.get('account').value;
+ const amount = this.form.get('amount').value;
+
+ this.store.dispatch({
+ type: SEARCH, payload: {
+ startDate,
+ endDate,
+ account,
+ amount: amount ? amount.toFixed(2) : undefined
+ }
+ });
+ }
+
+ select(journalEntry: JournalEntry): void {
+ this.journalEntry = journalEntry;
+ }
+
+ sumDebtors(debtors: Debtor[]): number {
+ return debtors.reduce((sum, debtor) => {
+ return sum + parseFloat(debtor.amount);
+ }, 0);
+ }
+
+}
diff --git a/src/app/accounting/ledger-exists.guard.ts b/src/app/accounting/ledger-exists.guard.ts
new file mode 100644
index 0000000..ac41526
--- /dev/null
+++ b/src/app/accounting/ledger-exists.guard.ts
@@ -0,0 +1,66 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Store} from '@ngrx/store';
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import {Injectable} from '@angular/core';
+import * as fromAccounting from './store';
+import {Observable} from 'rxjs/Observable';
+import {LoadAction} from './store/ledger/ledger.actions';
+import {of} from 'rxjs/observable/of';
+import {AccountingService} from '../services/accounting/accounting.service';
+import {ExistsGuardService} from '../common/guards/exists-guard';
+
+@Injectable()
+export class LedgerExistsGuard implements CanActivate {
+
+ constructor(private store: Store<fromAccounting.State>,
+ private accountingService: AccountingService,
+ private existsGuardService: ExistsGuardService) {}
+
+ hasLedgerInStore(id: string): Observable<boolean> {
+ const timestamp$ = this.store.select(fromAccounting.getLedgersLoadedAt)
+ .map(loadedAt => loadedAt[id]);
+
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ hasLedgerInApi(id: string): Observable<boolean> {
+ const findLedger$ = this.accountingService.findLedger(id)
+ .map(ledgerEntity => new LoadAction(ledgerEntity))
+ .do((action: LoadAction) => this.store.dispatch(action))
+ .map(ledger => !!ledger);
+
+ return this.existsGuardService.routeTo404OnError(findLedger$);
+ }
+
+ hasLedger(id: string): Observable<boolean> {
+ return this.hasLedgerInStore(id)
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+
+ return this.hasLedgerInApi(id);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasLedger(route.params['id']);
+ }
+}
diff --git a/src/app/accounting/ledger.resolver.ts b/src/app/accounting/ledger.resolver.ts
new file mode 100644
index 0000000..d68b67a
--- /dev/null
+++ b/src/app/accounting/ledger.resolver.ts
@@ -0,0 +1,33 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router';
+import {Ledger} from '../services/accounting/domain/ledger.model';
+import {AccountingService} from '../services/accounting/accounting.service';
+import {Observable} from 'rxjs/Observable';
+
+@Injectable()
+export class LedgerResolver implements Resolve<Ledger> {
+
+ constructor(private accountingService: AccountingService) {}
+
+ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Ledger> {
+ return this.accountingService.findLedger(route.params['id']);
+ }
+}
diff --git a/src/app/accounting/payroll/form/create.form.component.html b/src/app/accounting/payroll/form/create.form.component.html
new file mode 100644
index 0000000..1450c54
--- /dev/null
+++ b/src/app/accounting/payroll/form/create.form.component.html
@@ -0,0 +1,23 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Create new payroll' | translate}}">
+ <fims-payroll-form #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()">
+ </fims-payroll-form>
+</fims-layout-card-over>
diff --git a/src/app/accounting/payroll/form/create.form.component.ts b/src/app/accounting/payroll/form/create.form.component.ts
new file mode 100644
index 0000000..ef52820
--- /dev/null
+++ b/src/app/accounting/payroll/form/create.form.component.ts
@@ -0,0 +1,46 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component} from '@angular/core';
+import {AccountingStore} from '../../store/index';
+import {PayrollCollectionSheet} from '../../../services/payroll/domain/payroll-collection-sheet.model';
+import {CREATE} from '../../store/payroll/payroll-collection.actions';
+import {ActivatedRoute, Router} from '@angular/router';
+
+@Component({
+ templateUrl: './create.form.component.html'
+})
+export class CreatePayrollFormComponent {
+
+ constructor(private store: AccountingStore, private router: Router, private route: ActivatedRoute) {}
+
+ onSave(sheet: PayrollCollectionSheet): void {
+ this.store.dispatch({ type: CREATE, payload: {
+ sheet,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel(): void {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/accounting/payroll/form/form.component.html b/src/app/accounting/payroll/form/form.component.html
new file mode 100644
index 0000000..f90bac2
--- /dev/null
+++ b/src/app/accounting/payroll/form/form.component.html
@@ -0,0 +1,57 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Payments' | translate}}" [state]="form.valid ? 'complete' : form.pristine ? 'none' : 'required'">
+ <form [formGroup]="form">
+ <fims-account-select title="{{'From account' | translate}}" formControlName="sourceAccountNumber">
+ <ng-container *ngIf="!form.get('sourceAccountNumber').pristine && form.get('sourceAccountNumber').hasError('required')" translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="form.get('sourceAccountNumber').hasError('invalidAccount')" translate>
+ Invalid account
+ </ng-container>
+ </fims-account-select>
+ <div layout-gt-xs="column" layout-margin formArrayName="payments">
+ <h4 translate>Payments</h4>
+ <div *ngFor="let payment of payments; let i=index" layout="row" [formGroupName]="i">
+ <fims-customer-select title="Member" formControlName="customerIdentifier">
+ <ng-container *ngIf="!payment.get('customerIdentifier').pristine && payment.get('customerIdentifier').hasError('required')" translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="payment.get('customerIdentifier').hasError('invalidCustomer')" translate>
+ Invalid member or has no payroll created
+ </ng-container>
+ </fims-customer-select>
+ <fims-text-input [form]="payment" controlName="employer" placeholder="{{'Employer' | translate}}"></fims-text-input>
+ <fims-number-input placeholder="Salary" [form]="payment" controlName="salary"></fims-number-input>
+ <button mat-button (click)="removePayment(i)" [disabled]="i === 0">{{'Remove' | translate}}</button>
+ </div>
+ <button mat-button (click)="addPayment()">{{'Add payment' | translate}}</button>
+ </div>
+ </form>
+ <ng-template td-step-actions>
+ <fims-form-final-action
+ [resourceName]="'PAYMENTS'"
+ [editMode]="editMode"
+ [disabled]="!form.valid"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/accounting/payroll/form/form.component.ts b/src/app/accounting/payroll/form/form.component.ts
new file mode 100644
index 0000000..726cc7f
--- /dev/null
+++ b/src/app/accounting/payroll/form/form.component.ts
@@ -0,0 +1,99 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {Component, EventEmitter, OnInit, Output, ViewChild} from '@angular/core';
+import {PayrollCollectionSheet} from '../../../services/payroll/domain/payroll-collection-sheet.model';
+import {AbstractControl, FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {accountExists} from '../../../common/validator/account-exists.validator';
+import {AccountingService} from '../../../services/accounting/accounting.service';
+import {CustomerService} from '../../../services/customer/customer.service';
+import {FimsValidators} from '../../../common/validator/validators';
+import {TdStepComponent} from '@covalent/core';
+import {customerWithConfigExists} from './validator/customer-payroll-exists.validator';
+import {PayrollService} from '../../../services/payroll/payroll.service';
+
+@Component({
+ selector: 'fims-payroll-form',
+ templateUrl: './form.component.html'
+})
+export class PayrollFormComponent implements OnInit {
+
+ form: FormGroup;
+
+ @ViewChild('detailsStep') detailsStep: TdStepComponent;
+
+ @Output() onSave = new EventEmitter<PayrollCollectionSheet>();
+
+ @Output() onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder, private accountingService: AccountingService, private customerService: CustomerService,
+ private payrollService: PayrollService) {
+ this.form = this.formBuilder.group({
+ sourceAccountNumber: ['', [Validators.required], accountExists(accountingService)],
+ payments: this.initPayments()
+ });
+ }
+
+ ngOnInit(): void {
+ this.detailsStep.open();
+
+ this.addPayment();
+ }
+
+ save(): void {
+ const sheet: PayrollCollectionSheet = {
+ sourceAccountNumber: this.form.get('sourceAccountNumber').value,
+ payrollPayments: this.form.get('payments').value
+ };
+
+ this.onSave.emit(sheet);
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+ private initPayments(): FormArray {
+ const formControls: FormGroup[] = [];
+ return this.formBuilder.array(formControls);
+ }
+
+ private initPayment(): FormGroup {
+ return this.formBuilder.group({
+ customerIdentifier: ['', [Validators.required], customerWithConfigExists(this.customerService, this.payrollService)],
+ employer: ['', [Validators.required]],
+ salary: ['', [Validators.required, FimsValidators.minValue(0.001), FimsValidators.maxValue(9999999999.99999)]]
+ });
+ }
+
+ addPayment(): void {
+ const commands: FormArray = this.form.get('payments') as FormArray;
+ commands.push(this.initPayment());
+ }
+
+ removePayment(index: number): void {
+ const commands: FormArray = this.form.get('payments') as FormArray;
+ commands.removeAt(index);
+ }
+
+ get payments(): AbstractControl[] {
+ const commands: FormArray = this.form.get('payments') as FormArray;
+ return commands.controls;
+ }
+}
diff --git a/src/app/accounting/payroll/form/validator/customer-payroll-exists.validator.ts b/src/app/accounting/payroll/form/validator/customer-payroll-exists.validator.ts
new file mode 100644
index 0000000..ccd4937
--- /dev/null
+++ b/src/app/accounting/payroll/form/validator/customer-payroll-exists.validator.ts
@@ -0,0 +1,45 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {AbstractControl, AsyncValidatorFn} from '@angular/forms';
+import {Observable} from 'rxjs/Observable';
+import {CustomerService} from '../../../../services/customer/customer.service';
+import {isString} from '../../../../common/validator/validators';
+import {PayrollService} from '../../../../services/payroll/payroll.service';
+
+const invalid = Observable.of({
+ invalidCustomer: true
+});
+
+export function customerWithConfigExists(customerService: CustomerService, payrollService: PayrollService): AsyncValidatorFn {
+ return (control: AbstractControl): Observable<any> => {
+ if (!control.dirty || !control.value || control.value.length === 0) {
+ return Observable.of(null);
+ }
+
+ if (isString(control.value) && control.value.trim().length === 0) {
+ return invalid;
+ }
+
+ return customerService.getCustomer(control.value, true)
+ .switchMap(customer => payrollService.findPayrollConfiguration(customer.identifier, true))
+ .map(config => null)
+ .catch(() => invalid);
+ };
+}
diff --git a/src/app/accounting/payroll/payments.list.component.html b/src/app/accounting/payroll/payments.list.component.html
new file mode 100644
index 0000000..82ff419
--- /dev/null
+++ b/src/app/accounting/payroll/payments.list.component.html
@@ -0,0 +1,27 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="Payments" [navigateBackTo]="['../../']" *ngIf="selectedPayrollCollection$ | async as collection">
+ <fims-data-table flex
+ (onActionCellClick)="rowSelect($event)"
+ (onFetch)="fetchPayments(collection.identifier, $event)"
+ [columns]="columns"
+ [data]="paymentData$ | async"
+ [pageable]="true"
+ [actionColumn]="false">
+ </fims-data-table>
+</fims-layout-card-over>
diff --git a/src/app/accounting/payroll/payments.list.component.ts b/src/app/accounting/payroll/payments.list.component.ts
new file mode 100644
index 0000000..45a6b5b
--- /dev/null
+++ b/src/app/accounting/payroll/payments.list.component.ts
@@ -0,0 +1,75 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {TableData} from '../../common/data-table/data-table.component';
+import * as fromAccounting from '../store/index';
+import {AccountingStore} from '../store/index';
+import {PaymentSearchPayload, SEARCH} from '../store/payroll/payment.actions';
+import {FetchRequest} from '../../services/domain/paging/fetch-request.model';
+import {PayrollCollectionHistory} from '../../services/payroll/domain/payroll-collection-history.model';
+import {SelectAction} from '../store/payroll/payroll-collection.actions';
+import {ActivatedRoute} from '@angular/router';
+import {Subscription} from 'rxjs/Subscription';
+
+@Component({
+ templateUrl: './payments.list.component.html'
+})
+export class PaymentsListComponent implements OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ selectedPayrollCollection$: Observable<PayrollCollectionHistory>;
+
+ paymentData$: Observable<TableData>;
+
+ columns: any[] = [
+ { name: 'customerIdentifier', label: 'Member ID' },
+ { name: 'employer', label: 'Employer' },
+ { name: 'salary', label: 'Salary' }
+ ];
+
+ constructor(private route: ActivatedRoute, private store: AccountingStore) {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectAction(params['id']))
+ .subscribe(this.store);
+
+ this.paymentData$ = this.store.select(fromAccounting.getPayrollPaymentSearchResults);
+
+ this.selectedPayrollCollection$ = this.store.select(fromAccounting.getSelectedPayrollCollection)
+ .do((payrollCollection: PayrollCollectionHistory) => this.fetchPayments(payrollCollection.identifier));
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ }
+
+ fetchPayments(payrollIdentifier: string, fetchRequest?: FetchRequest): void {
+ const payload: PaymentSearchPayload = {
+ payrollIdentifier,
+ fetchRequest
+ };
+
+ this.store.dispatch({
+ type: SEARCH,
+ payload
+ });
+ }
+
+}
diff --git a/src/app/accounting/payroll/payroll.list.component.html b/src/app/accounting/payroll/payroll.list.component.html
new file mode 100644
index 0000000..9b952a5
--- /dev/null
+++ b/src/app/accounting/payroll/payroll.list.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="Manage payrolls" [navigateBackTo]="['../']">
+ <fims-data-table flex
+ (onActionCellClick)="rowSelect($event)"
+ [columns]="columns"
+ [data]="payrollData$ | async">
+ </fims-data-table>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Create payroll' | translate}}" icon="add" [link]="['create']" [permission]="{ id: 'payroll_distribution', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/accounting/payroll/payroll.list.component.ts b/src/app/accounting/payroll/payroll.list.component.ts
new file mode 100644
index 0000000..cd39a27
--- /dev/null
+++ b/src/app/accounting/payroll/payroll.list.component.ts
@@ -0,0 +1,62 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import * as fromAccouting from '../store/index';
+import {AccountingStore} from '../store/index';
+import {Observable} from 'rxjs/Observable';
+import {TableData} from '../../common/data-table/data-table.component';
+import {PayrollCollectionHistory} from '../../services/payroll/domain/payroll-collection-history.model';
+import {ActivatedRoute, Router} from '@angular/router';
+import {DatePipe} from '@angular/common';
+import {LOAD_ALL_COLLECTIONS} from '../store/payroll/payroll-collection.actions';
+
+@Component({
+ providers: [DatePipe],
+ templateUrl: './payroll.list.component.html'
+})
+export class PayrollListComponent implements OnInit {
+
+ payrollData$: Observable<TableData>;
+
+ columns: any[] = [
+ { name: 'createdBy', label: 'Created by' },
+ { name: 'createdOn', label: 'Created on', format: value => this.datePipe.transform(value, 'short') },
+ { name: 'sourceAccountNumber', label: 'Account number' }
+ ];
+
+ constructor(private router: Router, private route: ActivatedRoute, private datePipe: DatePipe,
+ private store: AccountingStore) {
+ this.payrollData$ = this.store.select(fromAccouting.getAllPayrollCollectionEntities)
+ .map((collections: PayrollCollectionHistory[]) => ({
+ data: collections,
+ totalElements: collections.length,
+ totalPages: 1
+ }));
+ }
+
+ ngOnInit(): void {
+ this.store.dispatch({
+ type: LOAD_ALL_COLLECTIONS
+ });
+ }
+
+ rowSelect(collection: PayrollCollectionHistory): void {
+ this.router.navigate(['payments', collection.identifier], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/accounting/status/status.component.html b/src/app/accounting/status/status.component.html
new file mode 100644
index 0000000..2fe2260
--- /dev/null
+++ b/src/app/accounting/status/status.component.html
@@ -0,0 +1,34 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Change state' | translate}}" [navigateBackTo]="['../']">
+ <div layout="row" layout-align="start start" layout-margin>
+ <div layout="column" flex="100">
+ <div layout="row" layout-align="start start" *ngFor="let command of statusCommands">
+ <div layout="column" flex="100">
+ <div layout="row" layout-align="start center">
+ <mat-form-field flex>
+ <textarea matInput placeholder="{{'Enter your comment here...' | translate}}" [(ngModel)]="command.comment"></textarea>
+ </mat-form-field>
+ <button mat-raised-button style="margin-left: 10px;" color="accent" (click)="executeCommand(command)">{{command.action}}</button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</fims-layout-card-over>
+
diff --git a/src/app/accounting/status/status.component.ts b/src/app/accounting/status/status.component.ts
new file mode 100644
index 0000000..edec50c
--- /dev/null
+++ b/src/app/accounting/status/status.component.ts
@@ -0,0 +1,63 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import {ActivatedRoute, Params} from '@angular/router';
+import {AccountCommandAction} from '../../services/accounting/domain/account-command-action.model';
+import {AccountCommand} from '../../services/accounting/domain/account-command.model';
+import {AccountingStore} from '../store/index';
+import {EXECUTE_COMMAND} from '../store/account/task/task.actions';
+
+interface StatusCommand {
+ action: AccountCommandAction;
+ comment?: string;
+}
+
+@Component({
+ templateUrl: './status.component.html'
+})
+export class AccountStatusComponent implements OnInit {
+
+ private accountIdentifier: string;
+
+ statusCommands: StatusCommand[] = [
+ { action: 'LOCK' },
+ { action: 'UNLOCK' },
+ { action: 'CLOSE' },
+ { action: 'REOPEN' }
+ ];
+
+ constructor(private route: ActivatedRoute, private store: AccountingStore) {}
+
+ ngOnInit(): void {
+ this.route.params.subscribe((params: Params) => this.accountIdentifier = params['id']);
+ }
+
+ executeCommand(statusCommand: StatusCommand): void {
+ const command: AccountCommand = {
+ comment: statusCommand.comment,
+ action: statusCommand.action
+ };
+ this.store.dispatch({ type: EXECUTE_COMMAND, payload: {
+ accountId: this.accountIdentifier,
+ command: command,
+ activatedRoute: this.route
+ } });
+ }
+
+}
diff --git a/src/app/accounting/store/account/account.actions.ts b/src/app/accounting/store/account/account.actions.ts
new file mode 100644
index 0000000..ac10027
--- /dev/null
+++ b/src/app/accounting/store/account/account.actions.ts
@@ -0,0 +1,137 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Action} from '@ngrx/store';
+import {type} from '../../../store/util';
+import {Account} from '../../../services/accounting/domain/account.model';
+import {Error} from '../../../services/domain/error.model';
+import {RoutePayload} from '../../../common/store/route-payload';
+import {
+ CreateResourceSuccessPayload,
+ DeleteResourceSuccessPayload,
+ LoadResourcePayload,
+ SelectResourcePayload,
+ UpdateResourceSuccessPayload
+} from '../../../common/store/resource.reducer';
+
+export const LOAD = type('[Account] Load');
+export const SELECT = type('[Account] Select');
+
+export const CREATE = type('[Account] Create');
+export const CREATE_SUCCESS = type('[Account] Create Success');
+export const CREATE_FAIL = type('[Account] Create Fail');
+
+export const UPDATE = type('[Account] Update');
+export const UPDATE_SUCCESS = type('[Account] Update Success');
+export const UPDATE_FAIL = type('[Account] Update Fail');
+
+export const DELETE = type('[Account] Delete');
+export const DELETE_SUCCESS = type('[Account] Delete Success');
+export const DELETE_FAIL = type('[Account] Delete Fail');
+
+export const RESET_FORM = type('[Account] Reset Form');
+
+export interface AccountRoutePayload extends RoutePayload {
+ account: Account;
+}
+
+export class LoadAction implements Action {
+ readonly type = LOAD;
+
+ constructor(public payload: LoadResourcePayload) { }
+}
+
+export class SelectAction implements Action {
+ readonly type = SELECT;
+
+ constructor(public payload: SelectResourcePayload) { }
+}
+
+export class CreateAccountAction implements Action {
+ readonly type = CREATE;
+
+ constructor(public payload: AccountRoutePayload) { }
+}
+
+export class CreateAccountSuccessAction implements Action {
+ readonly type = CREATE_SUCCESS;
+
+ constructor(public payload: CreateResourceSuccessPayload) { }
+}
+
+export class CreateAccountFailAction implements Action {
+ readonly type = CREATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class UpdateAccountAction implements Action {
+ readonly type = UPDATE;
+
+ constructor(public payload: AccountRoutePayload) { }
+}
+
+export class UpdateAccountSuccessAction implements Action {
+ readonly type = UPDATE_SUCCESS;
+
+ constructor(public payload: UpdateResourceSuccessPayload) { }
+}
+
+export class UpdateAccountFailAction implements Action {
+ readonly type = UPDATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class DeleteAccountAction implements Action {
+ readonly type = DELETE;
+
+ constructor(public payload: AccountRoutePayload) { }
+}
+
+export class DeleteAccountSuccessAction implements Action {
+ readonly type = DELETE_SUCCESS;
+
+ constructor(public payload: DeleteResourceSuccessPayload) { }
+}
+
+export class DeleteAccountFailAction implements Action {
+ readonly type = DELETE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class ResetAccountFormAction implements Action {
+ readonly type = RESET_FORM;
+
+ constructor() {}
+}
+
+export type Actions
+ = LoadAction
+ | SelectAction
+ | CreateAccountAction
+ | CreateAccountSuccessAction
+ | CreateAccountFailAction
+ | UpdateAccountAction
+ | UpdateAccountSuccessAction
+ | UpdateAccountFailAction
+ | DeleteAccountAction
+ | DeleteAccountSuccessAction
+ | DeleteAccountFailAction
+ | ResetAccountFormAction;
diff --git a/src/app/accounting/store/account/accounts.reducer.spec.ts b/src/app/accounting/store/account/accounts.reducer.spec.ts
new file mode 100644
index 0000000..6260e85
--- /dev/null
+++ b/src/app/accounting/store/account/accounts.reducer.spec.ts
@@ -0,0 +1,77 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {reducer} from './accounts.reducer';
+import {ResourceState} from '../../../common/store/resource.reducer';
+import {ExecuteCommandPayload, ExecuteCommandSuccessAction} from './task/task.actions';
+import {AccountCommandAction} from '../../../services/accounting/domain/account-command-action.model';
+import {AccountState} from '../../../services/accounting/domain/account-state.model';
+
+describe('Accounts Reducer', () => {
+
+ function mockSuccessAction(action: AccountCommandAction): ExecuteCommandPayload {
+ return {
+ accountId: 'test',
+ command: {
+ action: action,
+ comment: 'test'
+ },
+ activatedRoute: null
+ };
+ }
+
+ function mockState(state: AccountState): ResourceState {
+ return {
+ ids: ['test'],
+ entities: {
+ 'test': {
+ identifier: 'test',
+ name: 'test',
+ state: state
+ }
+ },
+ selectedId: null,
+ loadedAt: null
+ };
+ }
+
+ it('should open the account when reopened', () => {
+ const result = reducer(mockState('CLOSED'), new ExecuteCommandSuccessAction(mockSuccessAction('REOPEN')));
+
+ expect(result).toEqual(mockState('OPEN'));
+ });
+
+ it('should open the account when unlocked', () => {
+ const result = reducer(mockState('LOCKED'), new ExecuteCommandSuccessAction(mockSuccessAction('UNLOCK')));
+
+ expect(result).toEqual(mockState('OPEN'));
+ });
+
+ it('should lock the account when locked', () => {
+ const result = reducer(mockState('OPEN'), new ExecuteCommandSuccessAction(mockSuccessAction('LOCK')));
+
+ expect(result).toEqual(mockState('LOCKED'));
+ });
+
+ it('should close the account when closed', () => {
+ const result = reducer(mockState('OPEN'), new ExecuteCommandSuccessAction(mockSuccessAction('CLOSE')));
+
+ expect(result).toEqual(mockState('CLOSED'));
+ });
+
+});
diff --git a/src/app/accounting/store/account/accounts.reducer.ts b/src/app/accounting/store/account/accounts.reducer.ts
new file mode 100644
index 0000000..d25e220
--- /dev/null
+++ b/src/app/accounting/store/account/accounts.reducer.ts
@@ -0,0 +1,71 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ResourceState} from '../../../common/store/resource.reducer';
+import * as accounts from './account.actions';
+import * as accountTasks from './task/task.actions';
+import {AccountCommand} from '../../../services/accounting/domain/account-command.model';
+import {AccountState} from '../../../services/accounting/domain/account-state.model';
+import {Account} from '../../../services/accounting/domain/account.model';
+
+export const initialState: ResourceState = {
+ ids: [],
+ entities: {},
+ loadedAt: {},
+ selectedId: null,
+};
+
+export function reducer(state = initialState, action: accounts.Actions | accountTasks.Actions): ResourceState {
+
+ switch (action.type) {
+
+ case accountTasks.EXECUTE_COMMAND_SUCCESS: {
+ const payload = action.payload;
+
+ const accountId = payload.accountId;
+ const command: AccountCommand = payload.command;
+
+ const account: Account = state.entities[accountId];
+
+ let accountState: AccountState = null;
+
+ if (command.action === 'LOCK') {
+ accountState = 'LOCKED';
+ } else if (command.action === 'UNLOCK' || command.action === 'REOPEN') {
+ accountState = 'OPEN';
+ } else if (command.action === 'CLOSE') {
+ accountState = 'CLOSED';
+ }
+
+ account.state = accountState;
+
+ return {
+ ids: [...state.ids],
+ entities: Object.assign({}, state.entities, {
+ [account.identifier]: account
+ }),
+ loadedAt: state.loadedAt,
+ selectedId: state.selectedId
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
diff --git a/src/app/accounting/store/account/effects/notification.effects.ts b/src/app/accounting/store/account/effects/notification.effects.ts
new file mode 100644
index 0000000..8d575b0
--- /dev/null
+++ b/src/app/accounting/store/account/effects/notification.effects.ts
@@ -0,0 +1,56 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as accountActions from '../account.actions';
+import {NotificationService, NotificationType} from '../../../../services/notification/notification.service';
+
+@Injectable()
+export class AccountNotificationEffects {
+
+ @Effect({ dispatch: false })
+ createAccountSuccess$: Observable<Action> = this.actions$
+ .ofType(accountActions.CREATE_SUCCESS, accountActions.UPDATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Account is going to be saved'
+ }));
+
+ @Effect({ dispatch: false })
+ deleteAccountSuccess$: Observable<Action> = this.actions$
+ .ofType(accountActions.DELETE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Account is going to be deleted'
+ }));
+
+ @Effect({ dispatch: false })
+ deleteAccountFail$: Observable<Action> = this.actions$
+ .ofType(accountActions.DELETE_FAIL)
+ .do(() => this.notificationService.send({
+ type: NotificationType.ALERT,
+ title: 'Account can\'t be deleted',
+ message: 'Account has account entries'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+}
+
diff --git a/src/app/accounting/store/account/effects/route.effects.ts b/src/app/accounting/store/account/effects/route.effects.ts
new file mode 100644
index 0000000..2bb544e
--- /dev/null
+++ b/src/app/accounting/store/account/effects/route.effects.ts
@@ -0,0 +1,48 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as accountActions from '../account.actions';
+import {Router} from '@angular/router';
+
+@Injectable()
+export class AccountRouteEffects {
+
+ @Effect({ dispatch: false })
+ createAccountSuccess$: Observable<Action> = this.actions$
+ .ofType(accountActions.CREATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../../'], { relativeTo: payload.activatedRoute }));
+
+ @Effect({ dispatch: false })
+ updateAccountSuccess$: Observable<Action> = this.actions$
+ .ofType(accountActions.UPDATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute }));
+
+ @Effect({ dispatch: false })
+ deleteAccountSuccess$: Observable<Action> = this.actions$
+ .ofType(accountActions.DELETE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../../../ledgers/detail', payload.resource.ledger], { relativeTo: payload.activatedRoute }));
+
+ constructor(private actions$: Actions, private router: Router) { }
+}
diff --git a/src/app/accounting/store/account/effects/service.effects.ts b/src/app/accounting/store/account/effects/service.effects.ts
new file mode 100644
index 0000000..a7ea458
--- /dev/null
+++ b/src/app/accounting/store/account/effects/service.effects.ts
@@ -0,0 +1,71 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+import * as accountActions from '../account.actions';
+import {AccountingService} from '../../../../services/accounting/accounting.service';
+import {Observable} from 'rxjs/Observable';
+
+@Injectable()
+export class AccountApiEffects {
+
+ @Effect()
+ createAccount$: Observable<Action> = this.actions$
+ .ofType(accountActions.CREATE)
+ .map((action: accountActions.CreateAccountAction) => action.payload)
+ .mergeMap(payload =>
+ this.accountingService.createAccount(payload.account)
+ .map(() => new accountActions.CreateAccountSuccessAction({
+ resource: payload.account,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch(error => of(new accountActions.CreateAccountFailAction(error)))
+ );
+
+ @Effect()
+ updateAccount$: Observable<Action> = this.actions$
+ .ofType(accountActions.UPDATE)
+ .map((action: accountActions.UpdateAccountAction) => action.payload)
+ .mergeMap(payload =>
+ this.accountingService.modifyAccount(payload.account)
+ .map(() => new accountActions.UpdateAccountSuccessAction({
+ resource: payload.account,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch(error => of(new accountActions.UpdateAccountFailAction(error)))
+ );
+
+ @Effect()
+ deleteAccount$: Observable<Action> = this.actions$
+ .ofType(accountActions.DELETE)
+ .map((action: accountActions.DeleteAccountAction) => action.payload)
+ .mergeMap(payload =>
+ this.accountingService.deleteAccount(payload.account)
+ .map(() => new accountActions.DeleteAccountSuccessAction({
+ resource: payload.account,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch(error => of(new accountActions.DeleteAccountFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private accountingService: AccountingService) { }
+
+}
diff --git a/src/app/accounting/store/account/entries/effects/service.effect.ts b/src/app/accounting/store/account/entries/effects/service.effect.ts
new file mode 100644
index 0000000..15dad3f
--- /dev/null
+++ b/src/app/accounting/store/account/entries/effects/service.effect.ts
@@ -0,0 +1,45 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Action} from '@ngrx/store';
+import {Observable} from 'rxjs/Observable';
+import {Actions, Effect} from '@ngrx/effects';
+import * as accountEntryActions from '../entries.actions';
+import {Injectable} from '@angular/core';
+import {of} from 'rxjs/observable/of';
+import {AccountingService} from '../../../../../services/accounting/accounting.service';
+
+@Injectable()
+export class AccountEntryApiEffects {
+
+ @Effect()
+ loadAccountEntries$: Observable<Action> = this.actions$
+ .ofType(accountEntryActions.SEARCH)
+ .map((action: accountEntryActions.SearchAction) => action.payload)
+ .mergeMap(payload =>
+ this.accountingService.fetchAccountEntries(payload.accountId, payload.startDate, payload.endDate, payload.fetchRequest)
+ .map(accountEntryPage => new accountEntryActions.SearchCompleteAction(accountEntryPage))
+ .catch(() => of(new accountEntryActions.SearchCompleteAction({
+ accountEntries: [],
+ totalPages: 0,
+ totalElements: 0
+ })))
+ );
+
+ constructor(private actions$: Actions, private accountingService: AccountingService) { }
+}
diff --git a/src/app/accounting/store/account/entries/entries.actions.ts b/src/app/accounting/store/account/entries/entries.actions.ts
new file mode 100644
index 0000000..dd35376
--- /dev/null
+++ b/src/app/accounting/store/account/entries/entries.actions.ts
@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Action} from '@ngrx/store';
+import {type} from '../../../../store/util';
+import {FetchRequest} from '../../../../services/domain/paging/fetch-request.model';
+import {AccountEntryPage} from '../../../../services/accounting/domain/account-entry-page.model';
+
+export const SEARCH = type('[Account Entry] Search');
+export const SEARCH_COMPLETE = type('[Account Entry] Search Complete');
+
+export interface SearchActionPayload {
+ accountId: string;
+ startDate: string;
+ endDate: string;
+ fetchRequest: FetchRequest;
+}
+
+export class SearchAction implements Action {
+ readonly type = SEARCH;
+
+ constructor(public payload: SearchActionPayload) { }
+}
+
+export class SearchCompleteAction implements Action {
+ readonly type = SEARCH_COMPLETE;
+
+ constructor(public payload: AccountEntryPage) { }
+}
+
+export type Actions = SearchAction
+ | SearchCompleteAction;
diff --git a/src/app/accounting/store/account/entries/search.reducer.ts b/src/app/accounting/store/account/entries/search.reducer.ts
new file mode 100644
index 0000000..9032bb6
--- /dev/null
+++ b/src/app/accounting/store/account/entries/search.reducer.ts
@@ -0,0 +1,87 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as entries from './entries.actions';
+import {AccountEntry} from '../../../../services/accounting/domain/account-entry.model';
+import {FetchRequest} from '../../../../services/domain/paging/fetch-request.model';
+
+export interface State {
+ entries: AccountEntry[];
+ startDate: string;
+ endDate: string;
+ totalPages: number;
+ totalElements: number;
+ loading: boolean;
+ fetchRequest: FetchRequest;
+}
+
+const initialState: State = {
+ entries: [],
+ startDate: null,
+ endDate: null,
+ totalPages: 0,
+ totalElements: 0,
+ loading: false,
+ fetchRequest: null
+};
+
+export function reducer(state = initialState, action: entries.Actions): State {
+
+ switch (action.type) {
+
+ case entries.SEARCH: {
+ const payload = action.payload;
+
+ return Object.assign({}, state, {
+ startDate: payload.startDate,
+ endDate: payload.endDate,
+ fetchRequest: payload.fetchRequest,
+ loading: true
+ });
+ }
+
+ case entries.SEARCH_COMPLETE: {
+ const entryPage = action.payload;
+
+ return {
+ entries: entryPage.accountEntries,
+ loading: false,
+ fetchRequest: state.fetchRequest,
+ totalElements: entryPage.totalElements,
+ totalPages: entryPage.totalPages,
+ startDate: state.startDate,
+ endDate: state.endDate
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+
+export const getEntries = (state: State) => state.entries;
+
+export const getFetchRequest = (state: State) => state.fetchRequest;
+
+export const getLoading = (state: State) => state.loading;
+
+export const getTotalPages = (state: State) => state.totalPages;
+
+export const getTotalElements = (state: State) => state.totalElements;
diff --git a/src/app/accounting/store/account/task/effects/notification.effects.ts b/src/app/accounting/store/account/task/effects/notification.effects.ts
new file mode 100644
index 0000000..0946ba1
--- /dev/null
+++ b/src/app/accounting/store/account/task/effects/notification.effects.ts
@@ -0,0 +1,40 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as taskActions from '../task.actions';
+import {NotificationService, NotificationType} from '../../../../../services/notification/notification.service';
+
+@Injectable()
+export class AccountCommandNotificationEffects {
+
+ @Effect({ dispatch: false })
+ executeAccountCommandSuccess$: Observable<Action> = this.actions$
+ .ofType(taskActions.EXECUTE_COMMAND_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Command is going to be executed'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+
+}
+
diff --git a/src/app/accounting/store/account/task/effects/route.effects.ts b/src/app/accounting/store/account/task/effects/route.effects.ts
new file mode 100644
index 0000000..9078955
--- /dev/null
+++ b/src/app/accounting/store/account/task/effects/route.effects.ts
@@ -0,0 +1,36 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Router} from '@angular/router';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as taskActions from '../task.actions';
+
+@Injectable()
+export class AccountCommandRouteEffects {
+
+ @Effect({ dispatch: false })
+ executeCommandSuccess$: Observable<Action> = this.actions$
+ .ofType(taskActions.EXECUTE_COMMAND_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute }));
+
+ constructor(private actions$: Actions, private router: Router) { }
+}
diff --git a/src/app/accounting/store/account/task/effects/service.effects.ts b/src/app/accounting/store/account/task/effects/service.effects.ts
new file mode 100644
index 0000000..8acba09
--- /dev/null
+++ b/src/app/accounting/store/account/task/effects/service.effects.ts
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+import * as taskActions from '../task.actions';
+import {AccountingService} from '../../../../../services/accounting/accounting.service';
+
+@Injectable()
+export class AccountCommandApiEffects {
+
+ @Effect()
+ executeCommand: Observable<Action> = this.actions$
+ .ofType(taskActions.EXECUTE_COMMAND)
+ .map((action: taskActions.ExecuteCommandAction) => action.payload)
+ .mergeMap(payload =>
+ this.accountingService.accountCommand(payload.accountId, payload.command)
+ .map(() => new taskActions.ExecuteCommandSuccessAction(payload))
+ .catch((error) => of(new taskActions.ExecuteCommandFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private accountingService: AccountingService) { }
+
+}
diff --git a/src/app/accounting/store/account/task/task.actions.ts b/src/app/accounting/store/account/task/task.actions.ts
new file mode 100644
index 0000000..df4ccbd
--- /dev/null
+++ b/src/app/accounting/store/account/task/task.actions.ts
@@ -0,0 +1,53 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Action} from '@ngrx/store';
+import {type} from '../../../../store/util';
+import {AccountCommand} from '../../../../services/accounting/domain/account-command.model';
+import {RoutePayload} from '../../../../common/store/route-payload';
+
+export const EXECUTE_COMMAND = type('[Account Command] Execute');
+export const EXECUTE_COMMAND_SUCCESS = type('[Account Command] Success');
+export const EXECUTE_COMMAND_FAIL = type('[Account Command] Fail');
+
+export interface ExecuteCommandPayload extends RoutePayload {
+ accountId: string;
+ command: AccountCommand;
+}
+
+export class ExecuteCommandAction implements Action {
+ readonly type = EXECUTE_COMMAND;
+
+ constructor(public payload: ExecuteCommandPayload) { }
+}
+
+export class ExecuteCommandSuccessAction implements Action {
+ readonly type = EXECUTE_COMMAND_SUCCESS;
+
+ constructor(public payload: ExecuteCommandPayload) { }
+}
+
+export class ExecuteCommandFailAction implements Action {
+ readonly type = EXECUTE_COMMAND_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export type Actions = ExecuteCommandAction
+ | ExecuteCommandSuccessAction
+ | ExecuteCommandFailAction;
diff --git a/src/app/accounting/store/cheques/cheque.actions.ts b/src/app/accounting/store/cheques/cheque.actions.ts
new file mode 100644
index 0000000..961525f
--- /dev/null
+++ b/src/app/accounting/store/cheques/cheque.actions.ts
@@ -0,0 +1,59 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {createResourceActions} from '../../../common/store/action-creator/action-creator';
+import {type} from '../../../store/util';
+import {Action} from '@ngrx/store';
+import {ChequeProcessingCommand} from '../../../services/cheque/domain/cheque-processing-command';
+import {Error} from '../../../services/domain/error.model';
+import {FimsCheque} from '../../../services/cheque/domain/fims-cheque.model';
+
+export const ChequeCRUDActions = createResourceActions<FimsCheque>('Cheque');
+
+export const PROCESS = type('[Cheque] Process');
+export const PROCESS_SUCCESS = type('[Cheque] Process Success');
+export const PROCESS_FAIL = type('[Cheque] Process Fail');
+
+export interface ProcessPayload {
+ chequeIdentifier: string;
+ command: ChequeProcessingCommand;
+}
+
+export class ProcessAction implements Action {
+ readonly type = PROCESS;
+
+ constructor(public payload: ProcessPayload) { }
+}
+
+export class ProcessSuccessAction implements Action {
+ readonly type = PROCESS_SUCCESS;
+
+ constructor(public payload: ProcessPayload) { }
+}
+
+export class ProcessFailAction implements Action {
+ readonly type = PROCESS_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export type Actions
+ = ProcessAction
+ | ProcessSuccessAction
+ | ProcessFailAction;
+
diff --git a/src/app/accounting/store/cheques/cheques.reducer.ts b/src/app/accounting/store/cheques/cheques.reducer.ts
new file mode 100644
index 0000000..472c29b
--- /dev/null
+++ b/src/app/accounting/store/cheques/cheques.reducer.ts
@@ -0,0 +1,87 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {FimsCheque} from '../../../services/cheque/domain/fims-cheque.model';
+import {ResourceState} from '../../../common/store/resource.reducer';
+import {Actions} from '../../../common/store/action-creator/action-creator';
+import * as cheque from './cheque.actions';
+import {idsToHashWithCurrentTimestamp, resourcesToHash} from '../../../common/store/reducer.helper';
+import {ChequeProcessingCommand} from '../../../services/cheque/domain/cheque-processing-command';
+
+export interface State extends ResourceState {
+ ids: string[];
+ entities: { [id: string]: FimsCheque };
+ selectedId: string | null;
+}
+
+export const initialState: State = {
+ ids: [],
+ entities: {},
+ loadedAt: {},
+ selectedId: null,
+};
+
+export function reducer(state = initialState, action: Actions<FimsCheque> | cheque.Actions): ResourceState {
+
+ switch (action.type) {
+
+ case cheque.ChequeCRUDActions.LOAD_ALL: {
+ return initialState;
+ }
+
+ case cheque.ChequeCRUDActions.LOAD_ALL_COMPLETE: {
+ const ranges: FimsCheque[] = action.payload.resources;
+
+ const ids = ranges.map(chargeDefinition => chargeDefinition.identifier);
+
+ const entities = resourcesToHash(ranges);
+
+ const loadedAt = idsToHashWithCurrentTimestamp(ids);
+
+ return {
+ ids: [ ...ids ],
+ entities: entities,
+ loadedAt: loadedAt,
+ selectedId: state.selectedId
+ };
+ }
+
+ case cheque.PROCESS_SUCCESS: {
+ const chequeIdentifier = action.payload.chequeIdentifier;
+ const command: ChequeProcessingCommand = action.payload.command;
+
+ const cheque = state.entities[chequeIdentifier];
+
+ if (command.action === 'APPROVE') {
+ cheque.state = 'PROCESSED';
+ } else {
+ cheque.state = 'CANCELED';
+ }
+
+ return Object.assign({}, state, {
+ entities: Object.assign({}, state.entities, {
+ [cheque.identifier]: cheque
+ }),
+ });
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
diff --git a/src/app/accounting/store/cheques/effects/service.effects.ts b/src/app/accounting/store/cheques/effects/service.effects.ts
new file mode 100644
index 0000000..a38c9f7
--- /dev/null
+++ b/src/app/accounting/store/cheques/effects/service.effects.ts
@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {of} from 'rxjs/observable/of';
+import {Action} from '@ngrx/store';
+import {Observable} from 'rxjs/Observable';
+import {Actions, Effect} from '@ngrx/effects';
+import {ChequeService} from '../../../../services/cheque/cheque.service';
+import {Injectable} from '@angular/core';
+import * as chequeActions from '../cheque.actions';
+import {ChequeCRUDActions} from '../cheque.actions';
+import {LoadAllAction} from '../../../../common/store/action-creator/actions';
+
+@Injectable()
+export class ChequeApiEffects {
+
+ @Effect()
+ loadAllChequesByState$: Observable<Action> = this.actions$
+ .ofType(ChequeCRUDActions.LOAD_ALL)
+ .map((action: LoadAllAction) => action.payload)
+ .mergeMap(payload =>
+ this.chequeService.fetch(payload.state)
+ .map(cheques => ChequeCRUDActions.loadAllCompleteAction({
+ resources: cheques,
+ data: payload.data
+ }))
+ .catch(() => of(ChequeCRUDActions.loadAllCompleteAction({
+ resources: [],
+ data: payload.data
+ })))
+ );
+
+ @Effect()
+ processCheque$: Observable<Action> = this.actions$
+ .ofType(chequeActions.PROCESS)
+ .map((action: chequeActions.ProcessAction) => action.payload)
+ .mergeMap(payload =>
+ this.chequeService.process(payload.chequeIdentifier, payload.command)
+ .map(() => new chequeActions.ProcessSuccessAction(payload))
+ .catch(error => of(new chequeActions.ProcessFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private chequeService: ChequeService) {}
+
+}
diff --git a/src/app/accounting/store/index.ts b/src/app/accounting/store/index.ts
new file mode 100644
index 0000000..e198807
--- /dev/null
+++ b/src/app/accounting/store/index.ts
@@ -0,0 +1,235 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as fromRoot from '../../store';
+import * as fromLedgers from './ledger/ledgers.reducer';
+import * as fromLedgerForm from './ledger/form.reducer';
+import * as fromTrialBalance from './ledger/trial-balance.reducer';
+import * as fromChartOfAccounts from './ledger/chart-of-account.reducer';
+import * as fromJournalEntrySearch from './ledger/journal-entry/search.reducer';
+import * as fromAccounts from './account/accounts.reducer';
+import * as fromAccountEntrySearch from './account/entries/search.reducer';
+import * as fromCheques from './cheques/cheques.reducer';
+import * as fromPayrolls from './payroll/payrolls.reducer';
+import {ActionReducer, Store} from '@ngrx/store';
+import {createReducer} from '../../store/index';
+import {createSelector} from 'reselect';
+import {
+ createResourceReducer,
+ getResourceAll,
+ getResourceLoadedAt,
+ getResourceSelected,
+ ResourceState
+} from '../../common/store/resource.reducer';
+import {createFormReducer, FormState, getFormError} from '../../common/store/form.reducer';
+import {
+ createSearchReducer,
+ getSearchEntities,
+ getSearchLoading,
+ getSearchTotalElements,
+ getSearchTotalPages,
+ SearchState
+} from '../../common/store/search.reducer';
+
+export interface State extends fromRoot.State {
+ accounts: ResourceState;
+ accountForm: FormState;
+ accountEntrySearch: fromAccountEntrySearch.State;
+
+ ledgers: fromLedgers.State;
+ ledgerForm: FormState;
+ trialBalance: fromTrialBalance.State;
+ chartOfAccounts: fromChartOfAccounts.State;
+ journalEntrySearch: fromJournalEntrySearch.State;
+ journalEntryForm: FormState;
+
+ transactionTypes: ResourceState;
+ transactionTypeSearch: SearchState;
+ transactionForm: FormState;
+
+ cheques: ResourceState;
+
+ payrollCollections: ResourceState;
+ payrollPayments: SearchState;
+}
+
+const reducers = {
+ ledgers: fromLedgers.reducer,
+ ledgerForm: createFormReducer('Ledger', fromLedgerForm.reducer),
+ trialBalance: fromTrialBalance.reducer,
+ chartOfAccounts: fromChartOfAccounts.reducer,
+
+ journalEntrySearch: fromJournalEntrySearch.reducer,
+ journalEntryForm: createFormReducer('Journal Entry'),
+
+ transactionTypes: createResourceReducer('Transaction Type', undefined, 'code'),
+ transactionTypeSearch: createSearchReducer('Transaction Type'),
+ transactionForm: createFormReducer('Transaction Type'),
+
+ accounts: createResourceReducer('Account', fromAccounts.reducer),
+ accountForm: createFormReducer('Account'),
+ accountEntrySearch: fromAccountEntrySearch.reducer,
+
+ cheques: createResourceReducer('Cheque', fromCheques.reducer),
+
+ payrollCollections: createResourceReducer('Payroll Collection', fromPayrolls.reducer),
+ payrollPayments: createSearchReducer('Payroll Payment')
+};
+
+export const accountingModuleReducer: ActionReducer<State> = createReducer(reducers);
+
+export class AccountingStore extends Store<State> {}
+
+export function accountingStoreFactory(appStore: Store<fromRoot.State>) {
+ appStore.replaceReducer(accountingModuleReducer);
+ return appStore;
+}
+
+/**
+ * Ledger Selectors
+ */
+export const getLedgerState = (state: State) => state.ledgers;
+
+export const getLedgerFormState = (state: State) => state.ledgerForm;
+export const getLedgerFormError = createSelector(getLedgerFormState, getFormError);
+
+export const getTrialBalanceState = (state: State) => state.trialBalance;
+
+export const getChartOfAccountsState = (state: State) => state.chartOfAccounts;
+
+export const getLedgerEntities = createSelector(getLedgerState, fromLedgers.getEntities);
+export const getLedgersLoadedAt = createSelector(getLedgerState, fromLedgers.getLoadedAt);
+export const getLedgerTopLevelIds = createSelector(getLedgerState, fromLedgers.getTopLevelIds);
+export const getSelectedLedger = createSelector(getLedgerState, fromLedgers.getSelected);
+
+export const getAllTopLevelLedgerEntities = createSelector(getLedgerTopLevelIds, getLedgerEntities, (topLevelIds, entities) => {
+ return topLevelIds.map(id => entities[id]);
+});
+
+export const getTrialBalance = createSelector(getTrialBalanceState, fromTrialBalance.getTrialBalance);
+
+export const getChartOfAccountEntries = createSelector(getChartOfAccountsState, fromChartOfAccounts.getChartOfAccountEntries);
+
+export const getChartOfAccountLoading = createSelector(getChartOfAccountsState, fromChartOfAccounts.getLoading);
+
+/**
+ * Journal Entries Selectors
+ */
+export const getJournalEntrySearchState = (state: State) => state.journalEntrySearch;
+
+export const getJournalEntryFormState = (state: State) => state.journalEntryForm;
+export const getJournalEntryFormError = createSelector(getJournalEntryFormState, getFormError);
+
+export const getJournalEntryEntities = createSelector(getJournalEntrySearchState, fromJournalEntrySearch.getEntities);
+
+export const getSearchJournalEntryIds = createSelector(getJournalEntrySearchState, fromJournalEntrySearch.getIds);
+
+export const getJournalEntriesSearchResult = createSelector(getJournalEntryEntities, getSearchJournalEntryIds, (entities, ids) => {
+ return ids.map(id => entities[id]);
+});
+
+/**
+ * Accounts
+ */
+export const getAccountsState = (state: State) => state.accounts;
+
+export const getAccountFormState = (state: State) => state.accountForm;
+export const getAccountFormError = createSelector(getAccountFormState, getFormError);
+
+export const getAccountsLoadedAt = createSelector(getAccountsState, getResourceLoadedAt);
+export const getSelectedAccount = createSelector(getAccountsState, getResourceSelected);
+
+export const getAccountEntrySearchState = (state: State) => state.accountEntrySearch;
+export const getAccountEntrySearchEntities = createSelector(getAccountEntrySearchState, fromAccountEntrySearch.getEntries);
+export const getAccountEntrySearchTotalElements = createSelector(getAccountEntrySearchState, fromAccountEntrySearch.getTotalElements);
+export const getAccountEntrySearchTotalPages = createSelector(getAccountEntrySearchState, fromAccountEntrySearch.getTotalPages);
+
+export const getAccountEntrySearchResults = createSelector(
+ getAccountEntrySearchEntities, getAccountEntrySearchTotalElements, getAccountEntrySearchTotalPages,
+ (entities, totalElements, totalPages) => {
+ return {
+ entries: entities,
+ totalPages: totalPages,
+ totalElements: totalElements
+ };
+ });
+
+/**
+ * Transaction Types
+ */
+export const getTransactionTypesState = (state: State) => state.transactionTypes;
+
+export const getTransactionTypeLoadedAt = createSelector(getTransactionTypesState, getResourceLoadedAt);
+export const getSelectedTransactionType = createSelector(getTransactionTypesState, getResourceSelected);
+
+export const getTransactionTypeSearchState = (state: State) => state.transactionTypeSearch;
+
+export const getTransactionTypeFormState = (state: State) => state.transactionForm;
+export const getTransactionTypeFormError = createSelector(getTransactionTypeFormState, getFormError);
+
+
+export const getSearchTransactionTypes = createSelector(getTransactionTypeSearchState, getSearchEntities);
+export const getTransactionTypeSearchTotalElements = createSelector(getTransactionTypeSearchState, getSearchTotalElements);
+export const getTransactionTypeSearchTotalPages = createSelector(getTransactionTypeSearchState, getSearchTotalPages);
+export const getTransactionTypeSearchLoading = createSelector(getTransactionTypeSearchState, getSearchLoading);
+
+export const getTransactionTypeSearchResults = createSelector(
+ getSearchTransactionTypes, getTransactionTypeSearchTotalPages, getTransactionTypeSearchTotalElements,
+ (transactionTypes, totalPages, totalElements) => {
+ return {
+ transactionTypes,
+ totalPages,
+ totalElements
+ };
+ });
+
+/**
+ * Cheques
+ */
+export const getChequesState = (state: State) => state.cheques;
+
+export const getChequeLoadedAt = createSelector(getChequesState, getResourceLoadedAt);
+export const getSelectedCheque = createSelector(getChequesState, getResourceSelected);
+
+export const getAllChequeEntities = createSelector(getChequesState, getResourceAll);
+
+/**
+ * Payroll collections
+ */
+export const getPayrollCollectionsState = (state: State) => state.payrollCollections;
+
+export const getPayrollCollectionLoadedAt = createSelector(getPayrollCollectionsState, getResourceLoadedAt);
+export const getSelectedPayrollCollection = createSelector(getPayrollCollectionsState, getResourceSelected);
+
+export const getAllPayrollCollectionEntities = createSelector(getPayrollCollectionsState, getResourceAll);
+
+export const getPayrollPaymentSearchState = (state: State) => state.payrollPayments;
+
+export const getSearchPayrollPayments = createSelector(getPayrollPaymentSearchState, getSearchEntities);
+export const getPayrollPaymentsSearchTotalElements = createSelector(getPayrollPaymentSearchState, getSearchTotalElements);
+export const getPayrollPaymentsSearchTotalPages = createSelector(getPayrollPaymentSearchState, getSearchTotalPages);
+
+export const getPayrollPaymentSearchResults = createSelector(
+ getSearchPayrollPayments, getPayrollPaymentsSearchTotalElements, getPayrollPaymentsSearchTotalPages,
+ (data, totalPages, totalElements) => {
+ return {
+ data,
+ totalPages,
+ totalElements
+ };
+ });
diff --git a/src/app/accounting/store/ledger/chart-of-account.reducer.ts b/src/app/accounting/store/ledger/chart-of-account.reducer.ts
new file mode 100644
index 0000000..e72afb7
--- /dev/null
+++ b/src/app/accounting/store/ledger/chart-of-account.reducer.ts
@@ -0,0 +1,62 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ChartOfAccountEntry} from '../../../services/accounting/domain/chart-of-account-entry.model';
+import * as ledger from './ledger.actions';
+
+export interface State {
+ chartOfAccountEntries: ChartOfAccountEntry[];
+ loading: boolean;
+}
+
+const initialState: State = {
+ chartOfAccountEntries: [],
+ loading: false
+};
+
+export function reducer(state = initialState, action: ledger.Actions): State {
+
+ switch (action.type) {
+
+ case ledger.LOAD_CHART_OF_ACCOUNTS: {
+
+ return Object.assign({}, state, {
+ chartOfAccountEntries: [],
+ loading: true
+ });
+ }
+
+ case ledger.LOAD_CHART_OF_ACCOUNTS_COMPLETE: {
+ const chartOfAccountEntries: ChartOfAccountEntry[] = action.payload;
+
+ return {
+ chartOfAccountEntries: chartOfAccountEntries,
+ loading: false
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+
+export const getChartOfAccountEntries = (state: State) => state.chartOfAccountEntries;
+
+export const getLoading = (state: State) => state.loading;
diff --git a/src/app/accounting/store/ledger/effects/notification.effects.ts b/src/app/accounting/store/ledger/effects/notification.effects.ts
new file mode 100644
index 0000000..35bc719
--- /dev/null
+++ b/src/app/accounting/store/ledger/effects/notification.effects.ts
@@ -0,0 +1,56 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as ledgerActions from '../ledger.actions';
+import {NotificationService, NotificationType} from '../../../../services/notification/notification.service';
+
+@Injectable()
+export class LedgerNotificationEffects {
+
+ @Effect({ dispatch: false })
+ createLedgerSuccess$: Observable<Action> = this.actions$
+ .ofType(ledgerActions.CREATE_SUCCESS, ledgerActions.CREATE_SUB_LEDGER_SUCCESS, ledgerActions.UPDATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Ledger is going to be saved'
+ }));
+
+ @Effect({ dispatch: false })
+ deleteLedgerSuccess$: Observable<Action> = this.actions$
+ .ofType(ledgerActions.DELETE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Ledger is going to be deleted'
+ }));
+
+ @Effect({ dispatch: false })
+ deleteLedgerFail$: Observable<Action> = this.actions$
+ .ofType(ledgerActions.DELETE_FAIL)
+ .do(() => this.notificationService.send({
+ type: NotificationType.ALERT,
+ title: 'Ledger can\'t be deleted',
+ message: 'Ledger has accounts or sub ledgers'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+}
+
diff --git a/src/app/accounting/store/ledger/effects/route.effects.ts b/src/app/accounting/store/ledger/effects/route.effects.ts
new file mode 100644
index 0000000..3e04e0f
--- /dev/null
+++ b/src/app/accounting/store/ledger/effects/route.effects.ts
@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as ledgerActions from '../ledger.actions';
+import {Router} from '@angular/router';
+
+@Injectable()
+export class LedgerRouteEffects {
+ @Effect({ dispatch: false })
+ createLedgerSuccess$: Observable<Action> = this.actions$
+ .ofType(ledgerActions.CREATE_SUCCESS, ledgerActions.CREATE_SUB_LEDGER_SUCCESS, ledgerActions.UPDATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute }));
+
+ @Effect({ dispatch: false })
+ deleteLedgerSuccess$: Observable<Action> = this.actions$
+ .ofType(ledgerActions.DELETE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => {
+ if (payload.ledger.parentLedgerIdentifier) {
+ this.router.navigate(['../../', payload.ledger.parentLedgerIdentifier, 'ledgers'], { relativeTo: payload.activatedRoute });
+ } else {
+ this.router.navigate(['../../../../'], { relativeTo: payload.activatedRoute });
+ }
+ });
+
+ constructor(private actions$: Actions, private router: Router) { }
+}
diff --git a/src/app/accounting/store/ledger/effects/service.effects.ts b/src/app/accounting/store/ledger/effects/service.effects.ts
new file mode 100644
index 0000000..e107277
--- /dev/null
+++ b/src/app/accounting/store/ledger/effects/service.effects.ts
@@ -0,0 +1,105 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+import * as ledgerActions from '../ledger.actions';
+import {AccountingService} from '../../../../services/accounting/accounting.service';
+
+@Injectable()
+export class LedgerApiEffects {
+
+ @Effect()
+ search$: Observable<Action> = this.actions$
+ .ofType(ledgerActions.LOAD_ALL_TOP_LEVEL)
+ .debounceTime(300)
+ .switchMap(() => {
+ const nextSearch$ = this.actions$.ofType(ledgerActions.LOAD_ALL_TOP_LEVEL).skip(1);
+
+ return this.accountingService.fetchLedgers()
+ .takeUntil(nextSearch$)
+ .map(ledgerPage => ledgerPage.ledgers)
+ .map(ledgers => new ledgerActions.LoadAllTopLevelComplete(ledgers))
+ .catch(() => of(new ledgerActions.LoadAllTopLevelComplete([])));
+ });
+
+ @Effect()
+ createLedger$: Observable<Action> = this.actions$
+ .ofType(ledgerActions.CREATE)
+ .map((action: ledgerActions.CreateLedgerAction) => action.payload)
+ .mergeMap(payload =>
+ this.accountingService.createLedger(payload.ledger)
+ .map(() => new ledgerActions.CreateLedgerSuccessAction(payload))
+ .catch((error) => of(new ledgerActions.CreateLedgerFailAction(error)))
+ );
+
+ @Effect()
+ createSubLedger$: Observable<Action> = this.actions$
+ .ofType(ledgerActions.CREATE_SUB_LEDGER)
+ .map((action: ledgerActions.CreateSubLedgerAction) => action.payload)
+ .mergeMap(payload =>
+ this.accountingService.addSubLedger(payload.parentLedgerId, payload.ledger)
+ .map(() => new ledgerActions.CreateSubLedgerSuccessAction(payload))
+ .catch((error) => of(new ledgerActions.CreateSubLedgerFailAction(error)))
+ );
+
+ @Effect()
+ updateLedger$: Observable<Action> = this.actions$
+ .ofType(ledgerActions.UPDATE)
+ .map((action: ledgerActions.UpdateLedgerAction) => action.payload)
+ .mergeMap(payload =>
+ this.accountingService.modifyLedger(payload.ledger)
+ .map(() => new ledgerActions.UpdateLedgerSuccessAction(payload))
+ .catch((error) => of(new ledgerActions.UpdateLedgerFailAction(error)))
+ );
+
+ @Effect()
+ deleteLedger$: Observable<Action> = this.actions$
+ .ofType(ledgerActions.DELETE)
+ .map((action: ledgerActions.DeleteLedgerAction) => action.payload)
+ .mergeMap(payload =>
+ this.accountingService.deleteLedger(payload.ledger.identifier)
+ .map(() => new ledgerActions.DeleteLedgerSuccessAction(payload))
+ .catch((error) => of(new ledgerActions.DeleteLedgerFailAction(error)))
+ );
+
+ @Effect()
+ loadTrialBalance$: Observable<Action> = this.actions$
+ .ofType(ledgerActions.LOAD_TRIAL_BALANCE)
+ .map((action: ledgerActions.LoadTrialBalanceAction) => action.payload)
+ .mergeMap(includeEmpty =>
+ this.accountingService.getTrialBalance(includeEmpty)
+ .map(trialBalance => new ledgerActions.LoadTrialBalanceActionComplete(trialBalance))
+ .catch(() => of(new ledgerActions.LoadTrialBalanceActionComplete(null)))
+ );
+
+ @Effect()
+ loadChartOfAccounts$: Observable<Action> = this.actions$
+ .ofType(ledgerActions.LOAD_CHART_OF_ACCOUNTS)
+ .mergeMap(() =>
+ this.accountingService.getChartOfAccounts()
+ .map(chartOfAccountEntries => new ledgerActions.LoadChartOfAccountsActionComplete(chartOfAccountEntries))
+ .catch(() => of(new ledgerActions.LoadChartOfAccountsActionComplete([])))
+ );
+
+ constructor(private actions$: Actions, private accountingService: AccountingService) { }
+
+}
diff --git a/src/app/accounting/store/ledger/form.reducer.ts b/src/app/accounting/store/ledger/form.reducer.ts
new file mode 100644
index 0000000..a11ef00
--- /dev/null
+++ b/src/app/accounting/store/ledger/form.reducer.ts
@@ -0,0 +1,39 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as ledger from './ledger.actions';
+import {FormState} from '../../../common/store/form.reducer';
+
+export const initialState: FormState = {};
+
+export function reducer(state = initialState, action: ledger.Actions): FormState {
+ switch (action.type) {
+
+ case ledger.CREATE_SUB_LEDGER_FAIL:
+ return {
+ error: action.payload
+ };
+
+ case ledger.CREATE_SUB_LEDGER_SUCCESS:
+ return initialState;
+
+ default:
+ return state;
+
+ }
+}
diff --git a/src/app/accounting/store/ledger/journal-entry/effects/notification.effects.ts b/src/app/accounting/store/ledger/journal-entry/effects/notification.effects.ts
new file mode 100644
index 0000000..bf70591
--- /dev/null
+++ b/src/app/accounting/store/ledger/journal-entry/effects/notification.effects.ts
@@ -0,0 +1,40 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as journalEntryActions from '../journal-entry.actions';
+import {NotificationService, NotificationType} from '../../../../../services/notification/notification.service';
+
+@Injectable()
+export class JournalEntryNotificationEffects {
+
+ @Effect({ dispatch: false })
+ createJournalEntrySuccess$: Observable<Action> = this.actions$
+ .ofType(journalEntryActions.CREATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Journal entry is going to be processed'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+
+}
+
diff --git a/src/app/accounting/store/ledger/journal-entry/effects/route.effects.ts b/src/app/accounting/store/ledger/journal-entry/effects/route.effects.ts
new file mode 100644
index 0000000..fdcf486
--- /dev/null
+++ b/src/app/accounting/store/ledger/journal-entry/effects/route.effects.ts
@@ -0,0 +1,37 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as journalEntryActions from '../journal-entry.actions';
+import {Router} from '@angular/router';
+
+@Injectable()
+export class JournalEntryRouteEffects {
+
+ @Effect({ dispatch: false })
+ createJournalEntrySuccess$: Observable<Action> = this.actions$
+ .ofType(journalEntryActions.CREATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute }));
+
+ constructor(private actions$: Actions, private router: Router) { }
+
+}
diff --git a/src/app/accounting/store/ledger/journal-entry/effects/service.effects.ts b/src/app/accounting/store/ledger/journal-entry/effects/service.effects.ts
new file mode 100644
index 0000000..995ccaa
--- /dev/null
+++ b/src/app/accounting/store/ledger/journal-entry/effects/service.effects.ts
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+import * as journalEntryActions from '../journal-entry.actions';
+import {AccountingService} from '../../../../../services/accounting/accounting.service';
+
+@Injectable()
+export class JournalEntryApiEffects {
+
+ @Effect()
+ loadJournalEntries$: Observable<Action> = this.actions$
+ .ofType(journalEntryActions.SEARCH)
+ .map((action: journalEntryActions.SearchAction) => action.payload)
+ .mergeMap(payload =>
+ this.accountingService.fetchJournalEntries(payload.startDate, payload.endDate, payload.account, payload.amount)
+ .map(journalEntries => new journalEntryActions.SearchCompleteAction(journalEntries))
+ .catch(() => of(new journalEntryActions.SearchCompleteAction([])))
+ );
+
+ @Effect()
+ createJournalEntry$: Observable<Action> = this.actions$
+ .ofType(journalEntryActions.CREATE)
+ .map((action: journalEntryActions.CreateJournalEntryAction) => action.payload)
+ .mergeMap(payload =>
+ this.accountingService.createJournalEntry(payload.journalEntry)
+ .map(() => new journalEntryActions.CreateJournalEntrySuccessAction(payload))
+ .catch((error) => of(new journalEntryActions.CreateJournalEntryFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private accountingService: AccountingService) { }
+
+}
diff --git a/src/app/accounting/store/ledger/journal-entry/journal-entry.actions.ts b/src/app/accounting/store/ledger/journal-entry/journal-entry.actions.ts
new file mode 100644
index 0000000..0e3b92c
--- /dev/null
+++ b/src/app/accounting/store/ledger/journal-entry/journal-entry.actions.ts
@@ -0,0 +1,86 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Action} from '@ngrx/store';
+import {Error} from '../../../../services/domain/error.model';
+import {type} from '../../../../store/util';
+import {JournalEntry} from '../../../../services/accounting/domain/journal-entry.model';
+import {RoutePayload} from '../../../../common/store/route-payload';
+
+export const SEARCH = type('[Journal Entry] Search');
+export const SEARCH_COMPLETE = type('[Journal Entry] Search Complete');
+
+export const CREATE = type('[Journal Entry] Create');
+export const CREATE_SUCCESS = type('[Journal Entry] Create Success');
+export const CREATE_FAIL = type('[Journal Entry] Create Fail');
+
+export const RESET_FORM = type('[Journal Entry] Reset Form');
+
+export interface SearchPayload {
+ startDate: string;
+ endDate: string;
+ account?: string;
+ amount?: string;
+}
+
+export interface JournalEntryRoutePayload extends RoutePayload {
+ journalEntry: JournalEntry;
+}
+
+export class SearchAction implements Action {
+ readonly type = SEARCH;
+
+ constructor(public payload: SearchPayload) { }
+}
+
+export class SearchCompleteAction implements Action {
+ readonly type = SEARCH_COMPLETE;
+
+ constructor(public payload: JournalEntry[]) { }
+}
+
+export class CreateJournalEntryAction implements Action {
+ readonly type = CREATE;
+
+ constructor(public payload: JournalEntryRoutePayload) { }
+}
+
+export class CreateJournalEntrySuccessAction implements Action {
+ readonly type = CREATE_SUCCESS;
+
+ constructor(public payload: JournalEntryRoutePayload) { }
+}
+
+export class CreateJournalEntryFailAction implements Action {
+ readonly type = CREATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class ResetJournalEntryFormAction implements Action {
+ readonly type = RESET_FORM;
+
+ constructor() {}
+}
+
+export type Actions = SearchAction
+ | SearchCompleteAction
+ | CreateJournalEntryAction
+ | CreateJournalEntrySuccessAction
+ | CreateJournalEntryFailAction
+ | ResetJournalEntryFormAction;
diff --git a/src/app/accounting/store/ledger/journal-entry/search.reducer.ts b/src/app/accounting/store/ledger/journal-entry/search.reducer.ts
new file mode 100644
index 0000000..83e8b85
--- /dev/null
+++ b/src/app/accounting/store/ledger/journal-entry/search.reducer.ts
@@ -0,0 +1,88 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as journalEntry from './journal-entry.actions';
+import {JournalEntry} from '../../../../services/accounting/domain/journal-entry.model';
+
+
+export interface State {
+ ids: string[];
+ entities: { [id: string]: JournalEntry };
+ loading: boolean;
+ startDate: string;
+ endDate: string;
+}
+
+const initialState: State = {
+ ids: [],
+ entities: {},
+ loading: false,
+ startDate: null,
+ endDate: null
+};
+
+export function reducer(state = initialState, action: journalEntry.Actions): State {
+
+ switch (action.type) {
+
+ case journalEntry.SEARCH: {
+ const payload = action.payload;
+
+ return Object.assign({}, state, {
+ startDate: payload.startDate,
+ endDate: payload.endDate,
+ loading: true
+ });
+ }
+
+ case journalEntry.SEARCH_COMPLETE: {
+ const journalEntries = action.payload;
+
+ const journalEntryIds = journalEntries.map(journalEntry => journalEntry.transactionIdentifier);
+
+ const newJournalEntryEntities = journalEntries.reduce((entities: { [id: string]: JournalEntry }, journalEntry: JournalEntry) => {
+ return Object.assign(entities, {
+ [journalEntry.transactionIdentifier]: journalEntry
+ });
+ }, {});
+
+ return {
+ ids: [ ...journalEntryIds ],
+ entities: newJournalEntryEntities,
+ startDate: state.startDate,
+ endDate: state.endDate,
+ loading: false
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+
+export const getIds = (state: State) => state.ids;
+
+export const getEntities = (state: State) => state.entities;
+
+export const getStartDate = (state: State) => state.startDate;
+
+export const getEndDate = (state: State) => state.endDate;
+
+export const getLoading = (state: State) => state.loading;
diff --git a/src/app/accounting/store/ledger/ledger.actions.ts b/src/app/accounting/store/ledger/ledger.actions.ts
new file mode 100644
index 0000000..6af9cc0
--- /dev/null
+++ b/src/app/accounting/store/ledger/ledger.actions.ts
@@ -0,0 +1,214 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Action} from '@ngrx/store';
+import {type} from '../../../store/util';
+import {Ledger} from '../../../services/accounting/domain/ledger.model';
+import {Error} from '../../../services/domain/error.model';
+import {TrialBalance} from '../../../services/accounting/domain/trial-balance.model';
+import {RoutePayload} from '../../../common/store/route-payload';
+import {ChartOfAccountEntry} from '../../../services/accounting/domain/chart-of-account-entry.model';
+
+export const LOAD_ALL_TOP_LEVEL = type('[Ledger] Load All Top Level');
+export const LOAD_ALL_TOP_LEVEL_COMPLETE = type('[Ledger] Load All Top Level Complete');
+
+export const LOAD = type('[Ledger] Load');
+export const SELECT = type('[Ledger] Select');
+
+export const CREATE = type('[Ledger] Create');
+export const CREATE_SUCCESS = type('[Ledger] Create Success');
+export const CREATE_FAIL = type('[Ledger] Create Fail');
+
+export const CREATE_SUB_LEDGER = type('[Ledger] Create Subledger');
+export const CREATE_SUB_LEDGER_SUCCESS = type('[Ledger] Create Subledger Success');
+export const CREATE_SUB_LEDGER_FAIL = type('[Ledger] Create Subledger Fail');
+
+
+export const UPDATE = type('[Ledger] Update');
+export const UPDATE_SUCCESS = type('[Ledger] Update Success');
+export const UPDATE_FAIL = type('[Ledger] Update Fail');
+
+export const DELETE = type('[Ledger] Delete');
+export const DELETE_SUCCESS = type('[Ledger] Delete Success');
+export const DELETE_FAIL = type('[Ledger] Delete Fail');
+
+export const LOAD_TRIAL_BALANCE = type('[Ledger] Load Trial Balance');
+export const LOAD_TRIAL_BALANCE_COMPLETE = type('[Ledger] Load Trial Balance Complete');
+
+export const LOAD_CHART_OF_ACCOUNTS = type('[Ledger] Load Chart Of Accounts');
+export const LOAD_CHART_OF_ACCOUNTS_COMPLETE = type('[Ledger] Load Chart Of Accounts Complete');
+
+export const RESET_FORM = type('[Ledger] Reset Form');
+
+export interface CreateSubLedgerPayload extends RoutePayload {
+ parentLedgerId: string;
+ ledger: Ledger;
+}
+
+export interface LedgerRoutePayload extends RoutePayload {
+ ledger: Ledger;
+}
+
+export class LoadAllTopLevel implements Action {
+ readonly type = LOAD_ALL_TOP_LEVEL;
+
+ constructor() { }
+}
+
+export class LoadAllTopLevelComplete implements Action {
+ readonly type = LOAD_ALL_TOP_LEVEL_COMPLETE;
+
+ constructor(public payload: Ledger[]) { }
+}
+
+export class LoadAction implements Action {
+ readonly type = LOAD;
+
+ constructor(public payload: Ledger) { }
+}
+
+export class SelectAction implements Action {
+ readonly type = SELECT;
+
+ constructor(public payload: string) { }
+}
+
+export class CreateLedgerAction implements Action {
+ readonly type = CREATE;
+
+ constructor(public payload: LedgerRoutePayload) { }
+}
+
+export class CreateLedgerSuccessAction implements Action {
+ readonly type = CREATE_SUCCESS;
+
+ constructor(public payload: LedgerRoutePayload) { }
+}
+
+export class CreateLedgerFailAction implements Action {
+ readonly type = CREATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class CreateSubLedgerAction implements Action {
+ readonly type = CREATE_SUB_LEDGER;
+
+ constructor(public payload: CreateSubLedgerPayload) { }
+}
+
+export class CreateSubLedgerSuccessAction implements Action {
+ readonly type = CREATE_SUB_LEDGER_SUCCESS;
+
+ constructor(public payload: CreateSubLedgerPayload) { }
+}
+
+export class CreateSubLedgerFailAction implements Action {
+ readonly type = CREATE_SUB_LEDGER_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class UpdateLedgerAction implements Action {
+ readonly type = UPDATE;
+
+ constructor(public payload: LedgerRoutePayload) { }
+}
+
+export class UpdateLedgerSuccessAction implements Action {
+ readonly type = UPDATE_SUCCESS;
+
+ constructor(public payload: LedgerRoutePayload) { }
+}
+
+export class UpdateLedgerFailAction implements Action {
+ readonly type = UPDATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class DeleteLedgerAction implements Action {
+ readonly type = DELETE;
+
+ constructor(public payload: LedgerRoutePayload) { }
+}
+
+export class DeleteLedgerSuccessAction implements Action {
+ readonly type = DELETE_SUCCESS;
+
+ constructor(public payload: LedgerRoutePayload) { }
+}
+
+export class DeleteLedgerFailAction implements Action {
+ readonly type = DELETE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class LoadTrialBalanceAction implements Action {
+ readonly type = LOAD_TRIAL_BALANCE;
+
+ constructor(public payload: boolean) { }
+}
+
+export class LoadTrialBalanceActionComplete implements Action {
+ readonly type = LOAD_TRIAL_BALANCE_COMPLETE;
+
+ constructor(public payload: TrialBalance) { }
+}
+
+export class LoadChartOfAccountsAction implements Action {
+ readonly type = LOAD_CHART_OF_ACCOUNTS;
+
+ constructor() { }
+}
+
+export class LoadChartOfAccountsActionComplete implements Action {
+ readonly type = LOAD_CHART_OF_ACCOUNTS_COMPLETE;
+
+ constructor(public payload: ChartOfAccountEntry[]) { }
+}
+
+export class ResetLedgerFormAction implements Action {
+ readonly type = RESET_FORM;
+
+ constructor() {}
+}
+
+export type Actions
+ = LoadAllTopLevel
+ | LoadAllTopLevelComplete
+ | LoadAction
+ | SelectAction
+ | CreateLedgerAction
+ | CreateLedgerSuccessAction
+ | CreateLedgerFailAction
+ | CreateSubLedgerAction
+ | CreateSubLedgerSuccessAction
+ | CreateSubLedgerFailAction
+ | UpdateLedgerAction
+ | UpdateLedgerSuccessAction
+ | UpdateLedgerFailAction
+ | DeleteLedgerAction
+ | DeleteLedgerSuccessAction
+ | DeleteLedgerFailAction
+ | LoadTrialBalanceAction
+ | LoadTrialBalanceActionComplete
+ | LoadChartOfAccountsAction
+ | LoadChartOfAccountsActionComplete
+ | ResetLedgerFormAction;
diff --git a/src/app/accounting/store/ledger/ledgers.reducer.spec.ts b/src/app/accounting/store/ledger/ledgers.reducer.spec.ts
new file mode 100644
index 0000000..03860b9
--- /dev/null
+++ b/src/app/accounting/store/ledger/ledgers.reducer.spec.ts
@@ -0,0 +1,226 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {reducer, State} from './ledgers.reducer';
+import {
+ CreateLedgerSuccessAction,
+ CreateSubLedgerPayload,
+ CreateSubLedgerSuccessAction,
+ DeleteLedgerSuccessAction,
+ LedgerRoutePayload,
+ LoadAllTopLevelComplete,
+ UpdateLedgerSuccessAction
+} from './ledger.actions';
+import {Ledger} from '../../../services/accounting/domain/ledger.model';
+
+describe('Ledgers Reducer', () => {
+
+ function createLedger(value: string): Ledger {
+ return { identifier: value, name: value, type: 'ASSET', showAccountsInChart: true, subLedgers: []};
+ }
+
+ describe('LOAD_ALL_TOP_LEVEL_COMPLETE', () => {
+
+ it('should add all ledgers if not in store', () => {
+ spyOn(Date, 'now').and.returnValue(1000);
+
+ const ledgerOne = createLedger('test1');
+ const ledgerTwo = createLedger('test2');
+
+ const payload: Ledger[] = [
+ ledgerOne,
+ ledgerTwo
+ ];
+
+ const expectedResult: State = {
+ ids: [ledgerOne.identifier, ledgerTwo.identifier],
+ topLevelIds: [ledgerOne.identifier, ledgerTwo.identifier],
+ entities: {
+ 'test1': ledgerOne,
+ 'test2': ledgerTwo
+ },
+ loadedAt: {
+ 'test1': 1000,
+ 'test2': 1000
+ },
+ selectedLedgerId: null,
+ };
+
+ const result = reducer(undefined, new LoadAllTopLevelComplete(payload));
+
+ expect(result).toEqual(expectedResult);
+ });
+ });
+
+ describe('CREATE_SUCCESS', () => {
+ it('should add ledger to top level ids if not in store', () => {
+ const ledgerOne = createLedger('test1');
+
+ const payload: LedgerRoutePayload = {
+ ledger: ledgerOne,
+ activatedRoute: null
+ };
+
+ const expectedResult: State = {
+ ids: [ledgerOne.identifier],
+ topLevelIds: [ledgerOne.identifier],
+ entities: {
+ 'test1': ledgerOne
+ },
+ loadedAt: {},
+ selectedLedgerId: null,
+ };
+
+ const result = reducer(undefined, new CreateLedgerSuccessAction(payload));
+
+ expect(result).toEqual(expectedResult);
+ });
+ });
+
+ describe('CREATE_SUB_LEDGER_SUCCESS', () => {
+ it('should add ledger not to top level ids', () => {
+ const parentLedger = createLedger('parent');
+ const ledgerOne = createLedger('test1');
+
+ const initialState: State = {
+ ids: [parentLedger.identifier],
+ topLevelIds: [parentLedger.identifier],
+ entities: {
+ 'parent': parentLedger
+ },
+ loadedAt: {},
+ selectedLedgerId: null,
+ };
+
+ const payload: CreateSubLedgerPayload = {
+ parentLedgerId: parentLedger.identifier,
+ ledger: ledgerOne,
+ activatedRoute: null
+ };
+
+ const expectedResult: State = {
+ ids: [parentLedger.identifier, ledgerOne.identifier],
+ topLevelIds: [parentLedger.identifier],
+ entities: {
+ 'test1': ledgerOne,
+ 'parent': Object.assign({}, parentLedger, {
+ subLedgers: [ledgerOne]
+ })
+ },
+ loadedAt: {},
+ selectedLedgerId: null,
+ };
+
+ const result = reducer(initialState, new CreateSubLedgerSuccessAction(payload));
+
+ expect(result).toEqual(expectedResult);
+ expect(result.entities['test1'].parentLedgerIdentifier).toEqual(parentLedger.identifier);
+ });
+ });
+
+ describe('UPDATE_SUCCESS', () => {
+ it('should update the new ledger in entities', () => {
+ const ledgerOne = createLedger('test1');
+
+ const updatedLedger = Object.assign({}, ledgerOne, {
+ name: 'newName'
+ });
+
+ const payload: LedgerRoutePayload = {
+ ledger: updatedLedger,
+ activatedRoute: null
+ };
+
+ const initialState: State = {
+ ids: [ledgerOne.identifier],
+ topLevelIds: [],
+ entities: {
+ 'test1': ledgerOne
+ },
+ loadedAt: {
+ 'test1': 1000
+ },
+ selectedLedgerId: null,
+ };
+
+ const expectedResult: State = {
+ ids: [ledgerOne.identifier],
+ topLevelIds: [],
+ entities: {
+ 'test1': updatedLedger
+ },
+ loadedAt: {
+ 'test1': 1000
+ },
+ selectedLedgerId: null,
+ };
+
+ const result = reducer(initialState, new UpdateLedgerSuccessAction(payload));
+
+ expect(result).toEqual(expectedResult);
+ });
+ });
+
+ describe('DELETE_SUCCESS', () => {
+
+ it('should delete sub ledger from parent ledger', () => {
+ const parentLedgerWithoutSub = createLedger('test1');
+ const subLedger = createLedger('test2');
+ subLedger.parentLedgerIdentifier = parentLedgerWithoutSub.identifier;
+
+ const parentLedgerWithSub = Object.assign({}, parentLedgerWithoutSub, {
+ subLedgers: [subLedger]
+ });
+
+ const initialState: State = {
+ ids: [parentLedgerWithSub.identifier, subLedger.identifier],
+ topLevelIds: ['test1'],
+ entities: {
+ 'test1': parentLedgerWithSub,
+ 'test2': subLedger
+ },
+ loadedAt: {
+ 'test1': 1000,
+ 'test2': 2000
+ },
+ selectedLedgerId: null,
+ };
+
+ const result: State = reducer(initialState, new DeleteLedgerSuccessAction({
+ ledger: subLedger,
+ activatedRoute: null
+ }));
+
+ const expectedResult: State = {
+ ids: [parentLedgerWithoutSub.identifier],
+ topLevelIds: ['test1'],
+ entities: {
+ 'test1': parentLedgerWithoutSub
+ },
+ loadedAt: {
+ 'test1': 1000
+ },
+ selectedLedgerId: null
+ };
+
+ expect(result).toEqual(expectedResult);
+ expect(result.entities['test1'].subLedgers.length === 0).toBeTruthy();
+ });
+ });
+
+});
diff --git a/src/app/accounting/store/ledger/ledgers.reducer.ts b/src/app/accounting/store/ledger/ledgers.reducer.ts
new file mode 100644
index 0000000..e4a5795
--- /dev/null
+++ b/src/app/accounting/store/ledger/ledgers.reducer.ts
@@ -0,0 +1,197 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as ledger from './ledger.actions';
+import {createSelector} from 'reselect';
+import {Ledger} from '../../../services/accounting/domain/ledger.model';
+import {resourcesToHash} from '../../../common/store/reducer.helper';
+
+export interface State {
+ ids: string[];
+ topLevelIds: string[];
+ entities: { [id: string]: Ledger };
+ loadedAt: { [id: string]: number };
+ selectedLedgerId: string | null;
+}
+
+export const initialState: State = {
+ ids: [],
+ topLevelIds: [],
+ entities: {},
+ loadedAt: {},
+ selectedLedgerId: null,
+};
+
+export function reducer(state = initialState, action: ledger.Actions): State {
+ switch (action.type) {
+
+ case ledger.LOAD_ALL_TOP_LEVEL_COMPLETE: {
+ const ledgers: Ledger[] = action.payload;
+
+ const newLedgers: Ledger[] = ledgers.filter(ledger => !state.entities[ledger.identifier]);
+
+ const newLedgerIds: string[] = newLedgers.map(ledger => ledger.identifier);
+
+ const newLedgerEntities = resourcesToHash(newLedgers);
+
+ const newLoadedAt = newLedgers.reduce((entities: { [id: string]: any }, ledger: Ledger) => {
+ return Object.assign(entities, {
+ [ledger.identifier]: Date.now()
+ });
+ }, {});
+
+ return {
+ ids: [ ...state.ids, ...newLedgerIds ],
+ topLevelIds: [ ...state.topLevelIds, ...newLedgerIds],
+ entities: Object.assign({}, state.entities, newLedgerEntities),
+ loadedAt: Object.assign({}, state.loadedAt, newLoadedAt),
+ selectedLedgerId: state.selectedLedgerId
+ };
+ }
+
+ case ledger.LOAD: {
+ const ledger: Ledger = action.payload;
+
+ const newIds = state.ids.filter(id => id !== ledger.identifier);
+
+ return {
+ ids: [ ...newIds, ledger.identifier ],
+ topLevelIds: state.topLevelIds,
+ entities: Object.assign({}, state.entities, {
+ [ledger.identifier]: ledger
+ }),
+ loadedAt: Object.assign({}, state.entities, {
+ [ledger.identifier]: Date.now()
+ }),
+ selectedLedgerId: state.selectedLedgerId
+ };
+ }
+
+ case ledger.SELECT: {
+ return Object.assign({}, state, {
+ selectedLedgerId: action.payload
+ });
+ }
+
+ case ledger.CREATE_SUCCESS: {
+ const ledger: Ledger = action.payload.ledger;
+
+ return {
+ ids: [ ...state.ids, ledger.identifier ],
+ topLevelIds: [ ...state.topLevelIds, ledger.identifier ],
+ entities: Object.assign({}, state.entities, {
+ [ledger.identifier]: ledger
+ }),
+ selectedLedgerId: state.selectedLedgerId,
+ loadedAt: state.loadedAt
+ };
+ }
+
+ case ledger.UPDATE_SUCCESS: {
+ const ledger: Ledger = action.payload.ledger;
+
+ return {
+ ids: [ ...state.ids ],
+ topLevelIds: [ ...state.topLevelIds ],
+ entities: Object.assign({}, state.entities, {
+ [ledger.identifier]: ledger
+ }),
+ selectedLedgerId: state.selectedLedgerId,
+ loadedAt: state.loadedAt
+ };
+ }
+
+ case ledger.CREATE_SUB_LEDGER_SUCCESS: {
+ const subLedger: Ledger = action.payload.ledger;
+ const parentLedgerId = action.payload.parentLedgerId;
+ const parentLedger: Ledger = Object.assign({}, state.entities[parentLedgerId]);
+ subLedger.parentLedgerIdentifier = parentLedgerId;
+ parentLedger.subLedgers.push(subLedger);
+
+ return {
+ ids: [ ...state.ids, subLedger.identifier ],
+ topLevelIds: [ ...state.topLevelIds ],
+ entities: Object.assign({}, state.entities, {
+ [subLedger.identifier]: subLedger,
+ [parentLedger.identifier]: parentLedger
+ }),
+ selectedLedgerId: state.selectedLedgerId,
+ loadedAt: state.loadedAt
+ };
+ }
+
+ case ledger.DELETE_SUCCESS: {
+ const deletedLedger: Ledger = action.payload.ledger;
+
+ const newIds: string[] = state.ids.filter(id => id !== deletedLedger.identifier);
+ const newTopLevelIds: string[] = state.topLevelIds.filter(id => id !== deletedLedger.identifier);
+
+ const newEntities = newIds.reduce((entities: { [id: string]: Ledger }, id: string) => {
+ let ledger = state.entities[id];
+
+ // Remove sub ledger from parent ledger
+ if (ledger.identifier === deletedLedger.parentLedgerIdentifier) {
+ ledger = Object.assign({}, ledger, {
+ subLedgers: ledger.subLedgers.filter(subLedger => subLedger.identifier !== deletedLedger.identifier)
+ });
+ }
+
+ return Object.assign(entities, {
+ [ledger.identifier]: ledger
+ });
+ }, {});
+
+ const newLoadedAt = newIds.reduce((entities: { [id: string]: any }, id: string) => {
+ const loadedAt = state.loadedAt[id];
+ return Object.assign(entities, {
+ [id]: loadedAt
+ });
+ }, {});
+
+ return {
+ ids: [...newIds],
+ topLevelIds: [...newTopLevelIds],
+ entities: newEntities,
+ loadedAt: newLoadedAt,
+ selectedLedgerId: state.selectedLedgerId
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+export const getEntities = (state: State) => state.entities;
+
+export const getLoadedAt = (state: State) => state.loadedAt;
+
+export const getIds = (state: State) => state.ids;
+
+export const getTopLevelIds = (state: State) => state.topLevelIds;
+
+export const getSelectedId = (state: State) => state.selectedLedgerId;
+
+export const getSelected = createSelector(getEntities, getSelectedId, (entities, selectedId) => {
+ return entities[selectedId];
+});
+
+export const getAll = createSelector(getEntities, getIds, (entities, ids) => {
+ return ids.map(id => entities[id]);
+});
diff --git a/src/app/accounting/store/ledger/transaction-type/effects/notification.effects.ts b/src/app/accounting/store/ledger/transaction-type/effects/notification.effects.ts
new file mode 100644
index 0000000..2f915c5
--- /dev/null
+++ b/src/app/accounting/store/ledger/transaction-type/effects/notification.effects.ts
@@ -0,0 +1,46 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as transactionTypeActions from '../transaction-type.actions';
+import {NotificationService, NotificationType} from '../../../../../services/notification/notification.service';
+
+@Injectable()
+export class TransactionTypeNotificationEffects {
+
+ @Effect({ dispatch: false })
+ createTransactionTypeSuccess$: Observable<Action> = this.actions$
+ .ofType(transactionTypeActions.CREATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Transaction type is going to be created'
+ }));
+
+ @Effect({ dispatch: false })
+ updateTransactionTypeSuccess$: Observable<Action> = this.actions$
+ .ofType(transactionTypeActions.UPDATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Transaction type is going to be updated'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+}
diff --git a/src/app/accounting/store/ledger/transaction-type/effects/route.effects.ts b/src/app/accounting/store/ledger/transaction-type/effects/route.effects.ts
new file mode 100644
index 0000000..08c7dcf
--- /dev/null
+++ b/src/app/accounting/store/ledger/transaction-type/effects/route.effects.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as transactionTypeActions from '../transaction-type.actions';
+import {Router} from '@angular/router';
+
+@Injectable()
+export class TransactionTypeRouteEffects {
+
+ @Effect({ dispatch: false })
+ createTransactionTypeSuccess$: Observable<Action> = this.actions$
+ .ofType(transactionTypeActions.CREATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute }));
+
+ @Effect({ dispatch: false })
+ updateTransactionTypeSuccess$: Observable<Action> = this.actions$
+ .ofType(transactionTypeActions.UPDATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../../'], { relativeTo: payload.activatedRoute }));
+
+ constructor(private actions$: Actions, private router: Router) { }
+
+}
diff --git a/src/app/accounting/store/ledger/transaction-type/effects/service.effects.ts b/src/app/accounting/store/ledger/transaction-type/effects/service.effects.ts
new file mode 100644
index 0000000..96ccb67
--- /dev/null
+++ b/src/app/accounting/store/ledger/transaction-type/effects/service.effects.ts
@@ -0,0 +1,72 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+import * as transactionTypeActions from '../transaction-type.actions';
+import {AccountingService} from '../../../../../services/accounting/accounting.service';
+import {emptySearchResult} from '../../../../../common/store/search.reducer';
+
+@Injectable()
+export class TransactionTypeApiEffects {
+
+ @Effect()
+ searchTransactionTypes$: Observable<Action> = this.actions$
+ .ofType(transactionTypeActions.SEARCH)
+ .map((action: transactionTypeActions.SearchAction) => action.payload)
+ .mergeMap(fetchRequest =>
+ this.accountingService.fetchTransactionTypes(fetchRequest)
+ .map(transactionTypePage => new transactionTypeActions.SearchCompleteAction({
+ elements: transactionTypePage.transactionTypes,
+ totalElements: transactionTypePage.totalElements,
+ totalPages: transactionTypePage.totalPages
+ }))
+ .catch(() => of(new transactionTypeActions.SearchCompleteAction(emptySearchResult())))
+ );
+
+ @Effect()
+ createTransactionType$: Observable<Action> = this.actions$
+ .ofType(transactionTypeActions.CREATE)
+ .map((action: transactionTypeActions.CreateTransactionTypeAction) => action.payload)
+ .mergeMap(payload =>
+ this.accountingService.createTransactionType(payload.transactionType)
+ .map(() => new transactionTypeActions.CreateTransactionTypeSuccessAction({
+ resource: payload.transactionType,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new transactionTypeActions.CreateTransactionTypeFailAction(error)))
+ );
+
+ @Effect()
+ updateTransactionType$: Observable<Action> = this.actions$
+ .ofType(transactionTypeActions.UPDATE)
+ .map((action: transactionTypeActions.UpdateTransactionTypeAction) => action.payload)
+ .mergeMap(payload =>
+ this.accountingService.changeTransactionType(payload.transactionType)
+ .map(() => new transactionTypeActions.UpdateTransactionTypeSuccessAction({
+ resource: payload.transactionType,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new transactionTypeActions.UpdateTransactionTypeFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private accountingService: AccountingService) { }
+}
diff --git a/src/app/accounting/store/ledger/transaction-type/transaction-type.actions.ts b/src/app/accounting/store/ledger/transaction-type/transaction-type.actions.ts
new file mode 100644
index 0000000..080a1fe
--- /dev/null
+++ b/src/app/accounting/store/ledger/transaction-type/transaction-type.actions.ts
@@ -0,0 +1,128 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {type} from '../../../../store/util';
+import {Action} from '@ngrx/store';
+import {TransactionType} from '../../../../services/accounting/domain/transaction-type.model';
+import {RoutePayload} from '../../../../common/store/route-payload';
+import {SearchPayload, SearchResult} from '../../../../common/store/search.reducer';
+import {
+ CreateResourceSuccessPayload,
+ LoadResourcePayload,
+ SelectResourcePayload,
+ UpdateResourceSuccessPayload
+} from '../../../../common/store/resource.reducer';
+
+export const SEARCH = type('[Transaction Type] Search');
+export const SEARCH_COMPLETE = type('[Transaction Type] Search Complete');
+
+export const LOAD = type('[Transaction Type] Load');
+export const SELECT = type('[Transaction Type] Select');
+
+export const CREATE = type('[Transaction Type] Create');
+export const CREATE_SUCCESS = type('[Transaction Type] Create Success');
+export const CREATE_FAIL = type('[Transaction Type] Create Fail');
+
+export const UPDATE = type('[Transaction Type] Update');
+export const UPDATE_SUCCESS = type('[Transaction Type] Update Success');
+export const UPDATE_FAIL = type('[Transaction Type] Update Fail');
+
+export const RESET_FORM = type('[Transaction Type] Reset Form');
+
+export interface TransactionTypePayload extends RoutePayload {
+ transactionType: TransactionType;
+}
+
+export class SearchAction implements Action {
+ readonly type = SEARCH;
+
+ constructor(public payload: SearchPayload) { }
+}
+
+export class SearchCompleteAction implements Action {
+ readonly type = SEARCH_COMPLETE;
+
+ constructor(public payload: SearchResult) { }
+}
+
+export class LoadAction implements Action {
+ readonly type = LOAD;
+
+ constructor(public payload: LoadResourcePayload) { }
+}
+
+export class SelectAction implements Action {
+ readonly type = SELECT;
+
+ constructor(public payload: SelectResourcePayload) { }
+}
+
+export class CreateTransactionTypeAction implements Action {
+ readonly type = CREATE;
+
+ constructor(public payload: TransactionTypePayload) { }
+}
+
+export class CreateTransactionTypeSuccessAction implements Action {
+ readonly type = CREATE_SUCCESS;
+
+ constructor(public payload: CreateResourceSuccessPayload) { }
+}
+
+export class CreateTransactionTypeFailAction implements Action {
+ readonly type = CREATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class UpdateTransactionTypeAction implements Action {
+ readonly type = UPDATE;
+
+ constructor(public payload: TransactionTypePayload) { }
+}
+
+export class UpdateTransactionTypeSuccessAction implements Action {
+ readonly type = UPDATE_SUCCESS;
+
+ constructor(public payload: UpdateResourceSuccessPayload) { }
+}
+
+export class UpdateTransactionTypeFailAction implements Action {
+ readonly type = UPDATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class ResetTransactionTypeFormAction implements Action {
+ readonly type = RESET_FORM;
+
+ constructor() {}
+}
+
+export type Actions
+ = SearchAction
+ | SearchCompleteAction
+ | LoadAction
+ | SelectAction
+ | CreateTransactionTypeAction
+ | CreateTransactionTypeSuccessAction
+ | CreateTransactionTypeFailAction
+ | UpdateTransactionTypeAction
+ | UpdateTransactionTypeSuccessAction
+ | UpdateTransactionTypeFailAction
+ | ResetTransactionTypeFormAction;
diff --git a/src/app/accounting/store/ledger/trial-balance.reducer.ts b/src/app/accounting/store/ledger/trial-balance.reducer.ts
new file mode 100644
index 0000000..69f642a
--- /dev/null
+++ b/src/app/accounting/store/ledger/trial-balance.reducer.ts
@@ -0,0 +1,69 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as ledger from './ledger.actions';
+import {TrialBalance} from '../../../services/accounting/domain/trial-balance.model';
+
+
+export interface State {
+ trialBalance: TrialBalance;
+ includeEmpty: boolean;
+ loading: boolean;
+}
+
+const initialState: State = {
+ trialBalance: null,
+ includeEmpty: false,
+ loading: false
+};
+
+export function reducer(state = initialState, action: ledger.Actions): State {
+
+ switch (action.type) {
+
+ case ledger.LOAD_TRIAL_BALANCE: {
+ const includeEmpty = action.payload;
+
+ return Object.assign({}, state, {
+ includeEmpty,
+ loading: true
+ });
+ }
+
+ case ledger.LOAD_TRIAL_BALANCE_COMPLETE: {
+ const trialBalance = action.payload;
+
+ return {
+ trialBalance: trialBalance,
+ loading: false,
+ includeEmpty: state.includeEmpty
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+
+export const getTrialBalance = (state: State) => state.trialBalance;
+
+export const getLoading = (state: State) => state.loading;
+
+export const getIncludeEmpty = (state: State) => state.includeEmpty;
diff --git a/src/app/accounting/store/payroll/effects/notification.effects.ts b/src/app/accounting/store/payroll/effects/notification.effects.ts
new file mode 100644
index 0000000..a0a274b
--- /dev/null
+++ b/src/app/accounting/store/payroll/effects/notification.effects.ts
@@ -0,0 +1,39 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {NotificationService, NotificationType} from '../../../../services/notification/notification.service';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as payrollActions from '../../payroll/payroll-collection.actions';
+
+@Injectable()
+export class PayrollCollectionNotificationEffects {
+
+ @Effect({ dispatch: false })
+ createPayrollSuccess$: Observable<Action> = this.actions$
+ .ofType(payrollActions.CREATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Payroll is going to be created'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+
+}
diff --git a/src/app/accounting/store/payroll/effects/route.effects.ts b/src/app/accounting/store/payroll/effects/route.effects.ts
new file mode 100644
index 0000000..254f62a
--- /dev/null
+++ b/src/app/accounting/store/payroll/effects/route.effects.ts
@@ -0,0 +1,37 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Router} from '@angular/router';
+import {Observable} from 'rxjs/Observable';
+import * as payrollActions from '../../payroll/payroll-collection.actions';
+import {Action} from '@ngrx/store';
+
+@Injectable()
+export class PayrollCollectionRouteEffects {
+
+ @Effect({ dispatch: false })
+ createPayrollSuccess$: Observable<Action> = this.actions$
+ .ofType(payrollActions.CREATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute }));
+
+ constructor(private actions$: Actions, private router: Router) { }
+
+}
diff --git a/src/app/accounting/store/payroll/effects/service.effects.ts b/src/app/accounting/store/payroll/effects/service.effects.ts
new file mode 100644
index 0000000..71330cb
--- /dev/null
+++ b/src/app/accounting/store/payroll/effects/service.effects.ts
@@ -0,0 +1,78 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import * as payrollActions from '../payroll-collection.actions';
+import {CreateSheetPayload} from '../payroll-collection.actions';
+import * as paymentActions from '../payment.actions';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+import {CreateResourceSuccessPayload} from '../../../../common/store/resource.reducer';
+import {emptySearchResult} from '../../../../common/store/search.reducer';
+import {PayrollService} from '../../../../services/payroll/payroll.service';
+
+@Injectable()
+export class PayrollCollectionApiEffects {
+
+ @Effect()
+ loadAllCollections$: Observable<Action> = this.actions$
+ .ofType(payrollActions.LOAD_ALL_COLLECTIONS)
+ .switchMap(() => this.payrollService.fetchDistributionHistory()
+ .map(payrolls => new payrollActions.LoadAllCompleteAction(payrolls))
+ .catch(() => of(new payrollActions.LoadAllCompleteAction([])))
+ );
+
+ @Effect()
+ createSheet$: Observable<Action> = this.actions$
+ .ofType(payrollActions.CREATE)
+ .map((action: payrollActions.CreateAction) => action.payload)
+ .switchMap(payload => this.payrollService.distribute(payload.sheet)
+ .map(() => new payrollActions.CreateSuccessAction(this.map(payload)))
+ .catch(error => of(new payrollActions.CreateFailAction(error)))
+ );
+
+ @Effect()
+ searchPayments$: Observable<Action> = this.actions$
+ .ofType(paymentActions.SEARCH)
+ .map((action: paymentActions.SearchAction) => action.payload)
+ .mergeMap(payload =>
+ this.payrollService.fetchPayments(payload.payrollIdentifier, payload.fetchRequest)
+ .map(payrollPaymentPage => new paymentActions.SearchCompleteAction({
+ elements: payrollPaymentPage.payrollPayments,
+ totalElements: payrollPaymentPage.totalElements,
+ totalPages: payrollPaymentPage.totalPages
+ }))
+ .catch(() => of(new paymentActions.SearchCompleteAction(emptySearchResult())))
+ );
+
+ private map(payload: CreateSheetPayload): CreateResourceSuccessPayload {
+ return {
+ resource: {
+ identifier: Date.now().toString(),
+ sourceAccountNumber: payload.sheet.sourceAccountNumber,
+ createdBy: '',
+ createdOn: new Date().toISOString()
+ },
+ activatedRoute: payload.activatedRoute
+ };
+ }
+
+ constructor(private actions$: Actions, private payrollService: PayrollService) {}
+}
diff --git a/src/app/accounting/store/payroll/payment.actions.ts b/src/app/accounting/store/payroll/payment.actions.ts
new file mode 100644
index 0000000..be459ce
--- /dev/null
+++ b/src/app/accounting/store/payroll/payment.actions.ts
@@ -0,0 +1,44 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {type} from '../../../store/util';
+import {SearchPayload, SearchResult} from '../../../common/store/search.reducer';
+import {Action} from '@ngrx/store';
+
+export const SEARCH = type('[Payroll Payment] Search');
+export const SEARCH_COMPLETE = type('[Payroll Payment] Search Complete');
+
+export interface PaymentSearchPayload extends SearchPayload {
+ payrollIdentifier: string;
+}
+
+export class SearchAction implements Action {
+ readonly type = SEARCH;
+
+ constructor(public payload: PaymentSearchPayload) { }
+}
+
+export class SearchCompleteAction implements Action {
+ readonly type = SEARCH_COMPLETE;
+
+ constructor(public payload: SearchResult) { }
+}
+
+export type Actions
+ = SearchAction
+ | SearchCompleteAction;
diff --git a/src/app/accounting/store/payroll/payroll-collection.actions.ts b/src/app/accounting/store/payroll/payroll-collection.actions.ts
new file mode 100644
index 0000000..346f5fc
--- /dev/null
+++ b/src/app/accounting/store/payroll/payroll-collection.actions.ts
@@ -0,0 +1,98 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {type} from '../../../store/util';
+import {Action} from '@ngrx/store';
+import {RoutePayload} from '../../../common/store/route-payload';
+import {PayrollCollectionSheet} from '../../../services/payroll/domain/payroll-collection-sheet.model';
+import {PayrollCollectionHistory} from '../../../services/payroll/domain/payroll-collection-history.model';
+import {CreateResourceSuccessPayload} from '../../../common/store/resource.reducer';
+
+export const LOAD_ALL_COLLECTIONS = type('[Payroll Collection] Load All');
+export const LOAD_ALL_COLLECTIONS_COMPLETE = type('[Payroll Collection] Load All Complete');
+
+export const LOAD = type('[Payroll Collection] Load');
+export const SELECT = type('[Payroll Collection] Select');
+
+export const CREATE = type('[Payroll Collection] Create');
+export const CREATE_SUCCESS = type('[Payroll Collection] Create Success');
+export const CREATE_FAIL = type('[Payroll Collection] Create Fail');
+
+export const RESET_FORM = type('[Payroll Collection] Reset Form');
+
+export interface CreateSheetPayload extends RoutePayload {
+ sheet: PayrollCollectionSheet;
+}
+
+export class LoadAllAction implements Action {
+ readonly type = LOAD_ALL_COLLECTIONS;
+
+ constructor() { }
+}
+
+export class LoadAllCompleteAction implements Action {
+ readonly type = LOAD_ALL_COLLECTIONS_COMPLETE;
+
+ constructor(public payload: PayrollCollectionHistory[]) { }
+}
+
+export class LoadAction implements Action {
+ readonly type = LOAD;
+
+ constructor(public payload: PayrollCollectionHistory) { }
+}
+
+export class SelectAction implements Action {
+ readonly type = SELECT;
+
+ constructor(public payload: string) { }
+}
+
+export class CreateAction implements Action {
+ readonly type = CREATE;
+
+ constructor(public payload: CreateSheetPayload) { }
+}
+
+export class CreateSuccessAction implements Action {
+ readonly type = CREATE_SUCCESS;
+
+ constructor(public payload: CreateResourceSuccessPayload) { }
+}
+
+export class CreateFailAction implements Action {
+ readonly type = CREATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class ResetFormAction implements Action {
+ readonly type = RESET_FORM;
+
+ constructor() { }
+}
+
+export type Actions
+ = LoadAllAction
+ | LoadAllCompleteAction
+ | LoadAction
+ | SelectAction
+ | CreateAction
+ | CreateSuccessAction
+ | CreateFailAction
+ | ResetFormAction;
diff --git a/src/app/accounting/store/payroll/payrolls.reducer.ts b/src/app/accounting/store/payroll/payrolls.reducer.ts
new file mode 100644
index 0000000..37b96cc
--- /dev/null
+++ b/src/app/accounting/store/payroll/payrolls.reducer.ts
@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ResourceState} from '../../../common/store/resource.reducer';
+import {idsToHashWithCurrentTimestamp, resourcesToHash} from '../../../common/store/reducer.helper';
+import * as payroll from './payroll-collection.actions';
+import {PayrollCollectionHistory} from '../../../services/payroll/domain/payroll-collection-history.model';
+
+export const initialState: ResourceState = {
+ ids: [],
+ entities: {},
+ loadedAt: {},
+ selectedId: null,
+};
+
+export function reducer(state = initialState, action: payroll.Actions): ResourceState {
+
+ switch (action.type) {
+
+ case payroll.LOAD_ALL_COLLECTIONS: {
+ return initialState;
+ }
+
+ case payroll.LOAD_ALL_COLLECTIONS_COMPLETE: {
+ const payrolls: PayrollCollectionHistory[] = action.payload;
+
+ const ids = payrolls.map(payroll => payroll.identifier);
+
+ const entities = resourcesToHash(payrolls);
+
+ const loadedAt = idsToHashWithCurrentTimestamp(ids);
+
+ return {
+ ids: [ ...ids ],
+ entities: entities,
+ loadedAt: loadedAt,
+ selectedId: state.selectedId
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
diff --git a/src/app/accounting/subLedger/sub-ledger.component.html b/src/app/accounting/subLedger/sub-ledger.component.html
new file mode 100644
index 0000000..ca721b3
--- /dev/null
+++ b/src/app/accounting/subLedger/sub-ledger.component.html
@@ -0,0 +1,18 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<router-outlet></router-outlet>
diff --git a/src/app/accounting/subLedger/sub-ledger.component.ts b/src/app/accounting/subLedger/sub-ledger.component.ts
new file mode 100644
index 0000000..299f988
--- /dev/null
+++ b/src/app/accounting/subLedger/sub-ledger.component.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {SelectAction} from '../store/ledger/ledger.actions';
+import {AccountingStore} from '../store/index';
+import {Subscription} from 'rxjs/Subscription';
+import {ActivatedRoute} from '@angular/router';
+
+@Component({
+ templateUrl: './sub-ledger.component.html'
+})
+export class SubLedgerComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ constructor(private route: ActivatedRoute, private store: AccountingStore) {}
+
+ ngOnInit(): void {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectAction(params['id']))
+ .subscribe(this.store);
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ }
+}
diff --git a/src/app/accounting/subLedger/sub-ledger.detail.component.html b/src/app/accounting/subLedger/sub-ledger.detail.component.html
new file mode 100644
index 0000000..ec0d1bf
--- /dev/null
+++ b/src/app/accounting/subLedger/sub-ledger.detail.component.html
@@ -0,0 +1,63 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{ ledger.identifier + ' - ' + ledger.name }}" [subTitle]="ledger.description" [navigateBackTo]="ledger.parentLedgerIdentifier ? ['../../', ledger.parentLedgerIdentifier, 'ledgers'] : ['../../../../']">
+ <fims-layout-card-over-header-menu layout="row" layout-align="end center">
+ <a mat-icon-button [routerLink]="['edit']" *hasPermission="{ id: 'accounting_ledgers', accessLevel: 'CHANGE' }"><mat-icon>mode_edit</mat-icon></a>
+ <button mat-icon-button (click)="deleteLedger()" title="{{'Delete this ledger' | translate}}" *hasPermission="{ id: 'accounting_ledgers', accessLevel: 'DELETE' }"><mat-icon>delete</mat-icon></button>
+ </fims-layout-card-over-header-menu>
+ <fims-two-column-layout>
+ <mat-nav-list left>
+ <h3 mat-subheader translate>Details</h3>
+ <a mat-list-item [routerLink]="['./ledgers']">
+ <mat-icon matListAvatar>library_books</mat-icon>
+ <h3 matLine translate>Subledgers</h3>
+ <p matLine translate>View subledgers</p>
+ </a>
+ </mat-nav-list>
+ <ng-container right>
+ <div layout="row">
+ <mat-list>
+ <mat-list-item>
+ <mat-icon matListAvatar>account_balance</mat-icon>
+ <h3 matLine translate>Type</h3>
+ <p matLine>{{ledger.type}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Balance</h3>
+ <p matLine>{{ledger.totalValue | displayFimsNumber}}</p>
+ </mat-list-item>
+ </mat-list>
+ </div>
+ <div layout="row">
+ <div layout="column" flex-gt-md="100">
+ <h3 translate>Accounts</h3>
+ <fims-data-table flex
+ (onFetch)="fetchAccounts($event)"
+ (onActionCellClick)="rowSelect($event)"
+ [columns]="columns"
+ [data]="accountData$ | async"
+ [loading]="loading$ | async"
+ [sortable]="true"
+ [pageable]="true">
+ </fims-data-table>
+ </div>
+ </div>
+ </ng-container>
+ </fims-two-column-layout>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Create account' | translate}}" icon="add" [link]="['accounts/create']" [permission]="{ id: 'accounting_accounts', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/accounting/subLedger/sub-ledger.detail.component.ts b/src/app/accounting/subLedger/sub-ledger.detail.component.ts
new file mode 100644
index 0000000..dc9db69
--- /dev/null
+++ b/src/app/accounting/subLedger/sub-ledger.detail.component.ts
@@ -0,0 +1,124 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Ledger} from '../../services/accounting/domain/ledger.model';
+import {TableData, TableFetchRequest} from '../../common/data-table/data-table.component';
+import {Observable} from 'rxjs/Observable';
+import {Subscription} from 'rxjs/Subscription';
+import {TdDialogService} from '@covalent/core';
+import {TranslateService} from '@ngx-translate/core';
+import * as fromAccounting from '../store';
+import * as fromRoot from '../../store';
+import {DELETE} from '../store/ledger/ledger.actions';
+import {AccountingStore} from '../store/index';
+import {SEARCH_BY_LEDGER} from '../../store/account/account.actions';
+import {FetchRequest} from '../../services/domain/paging/fetch-request.model';
+import {Account} from '../../services/accounting/domain/account.model';
+
+@Component({
+ templateUrl: './sub-ledger.detail.component.html'
+})
+export class SubLedgerDetailComponent implements OnInit, OnDestroy {
+
+ private ledgerSubscription: Subscription;
+
+ private lastFetchRequest: FetchRequest = {};
+
+ ledger: Ledger;
+
+ accountData$: Observable<TableData>;
+
+ loading$: Observable<boolean>;
+
+ columns: any[] = [
+ { name: 'identifier', label: 'Id' },
+ { name: 'name', label: 'Name' },
+ { name: 'state', label: 'State' },
+ { name: 'balance', label: 'Balance' }
+ ];
+
+ constructor(private router: Router, private route: ActivatedRoute, private dialogService: TdDialogService,
+ private translate: TranslateService, private store: AccountingStore) {}
+
+ ngOnInit(): void {
+ this.ledgerSubscription = this.store.select(fromAccounting.getSelectedLedger)
+ .filter(ledger => !!ledger)
+ .subscribe(ledger => {
+ this.ledger = ledger;
+ this.fetchAccounts();
+ });
+
+ this.accountData$ = this.store.select(fromRoot.getAccountSearchResults)
+ .map(accountPage => ({
+ data: accountPage.accounts,
+ totalElements: accountPage.totalElements,
+ totalPages: accountPage.totalPages
+ }));
+
+ this.loading$ = this.store.select(fromRoot.getAccountSearchLoading);
+ }
+
+ ngOnDestroy(): void {
+ this.ledgerSubscription.unsubscribe();
+ }
+
+ rowSelect(account: Account): void {
+ this.router.navigate(['../../../../accounts/detail', account.identifier], { relativeTo: this.route });
+ }
+
+ confirmDeletion(): Observable<boolean> {
+ const message = 'Do you want to delete this ledger?';
+ const title = 'Confirm deletion';
+ const button = 'DELETE LEDGER';
+
+ return this.translate.get([title, message, button])
+ .flatMap(result =>
+ this.dialogService.openConfirm({
+ message: result[message],
+ title: result[title],
+ acceptButton: result[button]
+ }).afterClosed()
+ );
+ }
+
+ deleteLedger(): void {
+ this.confirmDeletion()
+ .filter(accept => accept)
+ .subscribe(() => {
+ this.store.dispatch({ type: DELETE, payload: {
+ ledger: this.ledger,
+ activatedRoute: this.route
+ }});
+ });
+ }
+
+ fetchAccounts(fetchRequest?: TableFetchRequest): void {
+ if (fetchRequest) {
+ this.lastFetchRequest = fetchRequest;
+ }
+
+ this.store.dispatch({ type: SEARCH_BY_LEDGER, payload: {
+ ledgerId: this.ledger.identifier,
+ fetchRequest: this.lastFetchRequest
+ } });
+
+ }
+
+}
diff --git a/src/app/accounting/subLedger/sub-ledger.list.component.html b/src/app/accounting/subLedger/sub-ledger.list.component.html
new file mode 100644
index 0000000..768a0ef
--- /dev/null
+++ b/src/app/accounting/subLedger/sub-ledger.list.component.html
@@ -0,0 +1,49 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{ ledger.identifier + ' - ' + ledger.name }}" [subTitle]="ledger.description" [navigateBackTo]="ledger.parentLedgerIdentifier ? ['../'] : ['../../../../']">
+ <fims-layout-card-over-header-menu layout="row" layout-align="end center">
+ <a mat-icon-button [routerLink]="['edit']" *hasPermission="{ id: 'accounting_ledgers', accessLevel: 'CHANGE' }"><mat-icon>mode_edit</mat-icon></a>
+ <button mat-icon-button (click)="deleteLedger()" title="{{'Delete this ledger' | translate}}" *hasPermission="{ id: 'accounting_ledgers', accessLevel: 'DELETE' }"><mat-icon>delete</mat-icon></button>
+ </fims-layout-card-over-header-menu>
+ <div class="mat-content inset" flex>
+ <div layout="row">
+ <mat-list>
+ <mat-list-item>
+ <mat-icon matListAvatar>account_balance</mat-icon>
+ <h3 matLine translate>Type</h3>
+ <p matLine>{{ledger.type}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Balance</h3>
+ <p matLine>{{ledger.totalValue | displayFimsNumber}}</p>
+ </mat-list-item>
+ </mat-list>
+ </div>
+ <div layout="row">
+ <div layout="column" flex-gt-md="100">
+ <h3 translate>Subledgers</h3>
+ <fims-data-table flex
+ (onActionCellClick)="rowSelect($event)"
+ [columns]="columns"
+ [data]="ledgerData">
+ </fims-data-table>
+ </div>
+ </div>
+ </div>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Add subledger' | translate}}" icon="add" [link]="['create']" [permission]="{ id: 'accounting_ledgers', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/accounting/subLedger/sub-ledger.list.component.ts b/src/app/accounting/subLedger/sub-ledger.list.component.ts
new file mode 100644
index 0000000..4540d2e
--- /dev/null
+++ b/src/app/accounting/subLedger/sub-ledger.list.component.ts
@@ -0,0 +1,109 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {Subscription} from 'rxjs/Subscription';
+import {Ledger} from '../../services/accounting/domain/ledger.model';
+import {TableData} from '../../common/data-table/data-table.component';
+import {AccountingStore} from '../store/index';
+import {ActivatedRoute, Router} from '@angular/router';
+import * as fromAccounting from '../store';
+import {Observable} from 'rxjs/Observable';
+import {DELETE} from '../store/ledger/ledger.actions';
+import {TranslateService} from '@ngx-translate/core';
+import {TdDialogService} from '@covalent/core';
+
+@Component({
+ templateUrl: './sub-ledger.list.component.html'
+})
+export class SubLedgerListComponent implements OnInit, OnDestroy {
+
+ private selectionSubscription: Subscription;
+
+ private _ledger: Ledger;
+
+ ledgerData: TableData = {
+ totalElements: 0,
+ totalPages: 1,
+ data: []
+ };
+
+ columns: any[] = [
+ { name: 'identifier', label: 'Id' },
+ { name: 'name', label: 'Name' },
+ { name: 'description', label: 'Description' },
+ { name: 'totalValue', label: 'Balance', format: value => value ? value.toFixed(2) : '-' }
+ ];
+
+ constructor(private route: ActivatedRoute, private router: Router, private store: AccountingStore, private translate: TranslateService,
+ private dialogService: TdDialogService) {}
+
+ ngOnInit(): void {
+ this.selectionSubscription = this.store.select(fromAccounting.getSelectedLedger)
+ .filter(ledger => !!ledger)
+ .subscribe(ledger => this.ledger = ledger);
+ }
+
+ ngOnDestroy(): void {
+ this.selectionSubscription.unsubscribe();
+ }
+
+ rowSelect(ledger: Ledger): void {
+ this.router.navigate(['/accounting/ledgers/detail', ledger.identifier]);
+ }
+
+ set ledger(ledger: Ledger) {
+ this._ledger = ledger;
+
+ if (ledger.subLedgers) {
+ this.ledgerData.data = ledger.subLedgers;
+ this.ledgerData.totalElements = ledger.subLedgers.length;
+ }
+ }
+
+ get ledger(): Ledger {
+ return this._ledger;
+ }
+
+ confirmDeletion(): Observable<boolean> {
+ const message = 'Do you want to delete this ledger?';
+ const title = 'Confirm deletion';
+ const button = 'DELETE LEDGER';
+
+ return this.translate.get([title, message, button])
+ .flatMap(result =>
+ this.dialogService.openConfirm({
+ message: result[message],
+ title: result[title],
+ acceptButton: result[button]
+ }).afterClosed()
+ );
+ }
+
+ deleteLedger(): void {
+ this.confirmDeletion()
+ .filter(accept => accept)
+ .subscribe(() => {
+ this.store.dispatch({ type: DELETE, payload: {
+ ledger: this.ledger,
+ activatedRoute: this.route
+ }});
+ });
+ }
+
+}
diff --git a/src/app/accounting/trailBalance/trail-balance.component.html b/src/app/accounting/trailBalance/trail-balance.component.html
new file mode 100644
index 0000000..9d7120b
--- /dev/null
+++ b/src/app/accounting/trailBalance/trail-balance.component.html
@@ -0,0 +1,61 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Trial balance' | translate}}" [navigateBackTo]="['../']">
+ <fims-layout-card-over-header-menu>
+ <mat-checkbox [(ngModel)]="includeEmptyEntries" (change)="fetchTrialBalance($event)" translate>Include empty entries?</mat-checkbox>
+ </fims-layout-card-over-header-menu>
+ <div layout="row">
+ <table td-data-table>
+ <thead>
+ <tr td-data-table-column-row>
+ <th td-data-table-column>
+ <span translate>Ledger</span>
+ </th>
+ <th td-data-table-column>
+ <span translate>Debit</span>
+ </th>
+ <th td-data-table-column>
+ <span translate>Credit</span>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr td-data-table-row *ngFor="let row of (trialBalance$ | async)?.trialBalanceEntries">
+ <td td-data-table-cell>
+ <a [routerLink]="['../ledgers/detail', row['ledger'].identifier]" *hasPermission="{ id: 'accounting_ledgers', accessLevel: 'READ' }">{{row['ledger'].identifier }} - {{row['ledger'].name }}</a>
+ </td>
+ <td td-data-table-cell>
+ {{row['type'] == 'DEBIT' ? (row['amount'] | number): '-'}}
+ </td>
+ <td td-data-table-cell>
+ {{row['type'] == 'CREDIT' ? (row['amount'] | number) : '-'}}
+ </td>
+ </tr>
+ <tr td-data-table-row>
+ <td td-data-table-cell translate>Total</td>
+ <td td-data-table-cell>
+ {{(trialBalance$ | async)?.debitTotal | number}}
+ </td>
+ <td td-data-table-cell>
+ {{(trialBalance$ | async)?.creditTotal | number}}
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+</fims-layout-card-over>
diff --git a/src/app/accounting/trailBalance/trial-balance.component.ts b/src/app/accounting/trailBalance/trial-balance.component.ts
new file mode 100644
index 0000000..63e1b9d
--- /dev/null
+++ b/src/app/accounting/trailBalance/trial-balance.component.ts
@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import {TrialBalance} from '../../services/accounting/domain/trial-balance.model';
+import * as fromAccounting from '../store';
+import {LOAD_TRIAL_BALANCE} from '../store/ledger/ledger.actions';
+import {Observable} from 'rxjs/Observable';
+import {AccountingStore} from '../store/index';
+import {MatCheckboxChange} from '@angular/material';
+
+@Component({
+ templateUrl: './trail-balance.component.html'
+})
+export class TrailBalanceComponent implements OnInit {
+
+ includeEmptyEntries = false;
+
+ trialBalance$: Observable<TrialBalance>;
+
+ constructor(private store: AccountingStore) {}
+
+ ngOnInit(): void {
+ this.trialBalance$ = this.store.select(fromAccounting.getTrialBalance);
+ this.fetchTrialBalance();
+ }
+
+ fetchTrialBalance(event?: MatCheckboxChange): void {
+ this.store.dispatch({ type: LOAD_TRIAL_BALANCE, payload: this.includeEmptyEntries });
+ }
+
+}
diff --git a/src/app/accounting/transactionTypes/form/create/create.form.component.html b/src/app/accounting/transactionTypes/form/create/create.form.component.html
new file mode 100644
index 0000000..9e0c416
--- /dev/null
+++ b/src/app/accounting/transactionTypes/form/create/create.form.component.html
@@ -0,0 +1,24 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Create new transaction type' | translate}}">
+ <fims-transaction-type-form #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [transactionType]="transactionType">
+ </fims-transaction-type-form>
+</fims-layout-card-over>
diff --git a/src/app/accounting/transactionTypes/form/create/create.form.component.ts b/src/app/accounting/transactionTypes/form/create/create.form.component.ts
new file mode 100644
index 0000000..3f5bb10
--- /dev/null
+++ b/src/app/accounting/transactionTypes/form/create/create.form.component.ts
@@ -0,0 +1,73 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {Subscription} from 'rxjs/Subscription';
+import {ActivatedRoute, Router} from '@angular/router';
+import * as fromAccounting from '../../../store/index';
+import {AccountingStore} from '../../../store/index';
+import {Error} from '../../../../services/domain/error.model';
+import {TransactionType} from '../../../../services/accounting/domain/transaction-type.model';
+import {CREATE, RESET_FORM} from '../../../store/ledger/transaction-type/transaction-type.actions';
+import {TransactionTypeFormComponent} from '../transaction-type-form.component';
+
+@Component({
+ templateUrl: './create.form.component.html'
+})
+export class CreateTransactionTypeFormComponent implements OnInit, OnDestroy {
+
+ private formStateSubscription: Subscription;
+
+ @ViewChild('form') formComponent: TransactionTypeFormComponent;
+
+ transactionType: TransactionType = {
+ code: '',
+ name: ''
+ };
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: AccountingStore) {}
+
+ ngOnInit() {
+ this.formStateSubscription = this.store.select(fromAccounting.getTransactionTypeFormError)
+ .filter((error: Error) => !!error)
+ .subscribe((error: Error) => {
+ this.formComponent.showNumberValidationError();
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.formStateSubscription.unsubscribe();
+
+ this.store.dispatch({ type: RESET_FORM });
+ }
+
+ onSave(transactionType: TransactionType) {
+ this.store.dispatch({ type: CREATE, payload: {
+ transactionType,
+ activatedRoute: this.route
+ } });
+ }
+
+ onCancel() {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/accounting/transactionTypes/form/edit/edit.form.component.html b/src/app/accounting/transactionTypes/form/edit/edit.form.component.html
new file mode 100644
index 0000000..c295aca
--- /dev/null
+++ b/src/app/accounting/transactionTypes/form/edit/edit.form.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Edit transaction type' | translate}}">
+ <fims-transaction-type-form #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [editMode]="true"
+ [transactionType]="transactionType$ | async">
+ </fims-transaction-type-form>
+</fims-layout-card-over>
diff --git a/src/app/accounting/transactionTypes/form/edit/edit.form.component.ts b/src/app/accounting/transactionTypes/form/edit/edit.form.component.ts
new file mode 100644
index 0000000..7fb5b3c
--- /dev/null
+++ b/src/app/accounting/transactionTypes/form/edit/edit.form.component.ts
@@ -0,0 +1,65 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import * as fromAccounting from '../../../store/index';
+import {AccountingStore} from '../../../store/index';
+import {TransactionType} from '../../../../services/accounting/domain/transaction-type.model';
+import {SelectAction, UPDATE} from '../../../store/ledger/transaction-type/transaction-type.actions';
+import {Observable} from 'rxjs/Observable';
+import {Subscription} from 'rxjs/Subscription';
+
+@Component({
+ templateUrl: './edit.form.component.html'
+})
+export class EditTransactionTypeFormComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ transactionType$: Observable<TransactionType>;
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: AccountingStore) {}
+
+ ngOnInit() {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectAction(params['code']))
+ .subscribe(this.store);
+
+ this.transactionType$ = this.store.select(fromAccounting.getSelectedTransactionType);
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ }
+
+ onSave(transactionType: TransactionType) {
+ this.store.dispatch({ type: UPDATE, payload: {
+ transactionType,
+ activatedRoute: this.route
+ } });
+ }
+
+ onCancel() {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/accounting/transactionTypes/form/transaction-type-form.component.html b/src/app/accounting/transactionTypes/form/transaction-type-form.component.html
new file mode 100644
index 0000000..5faa964
--- /dev/null
+++ b/src/app/accounting/transactionTypes/form/transaction-type-form.component.html
@@ -0,0 +1,35 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Transaction type' | translate}}" [state]="form.valid ? 'complete' : form.pristine ? 'none' : 'required'">
+ <form [formGroup]="form" layout="column">
+ <fims-id-input [form]="form" controlName="code" [placeholder]="'Code'" [readonly]="editMode"></fims-id-input>
+ <fims-text-input [form]="form" controlName="name" placeholder="{{'Name' | translate}}"></fims-text-input>
+ <fims-text-input [form]="form" controlName="description" placeholder="{{'Description(optional)' | translate}}"></fims-text-input>
+ </form>
+ <ng-template td-step-actions>
+ <fims-form-final-action
+ [resourceName]="'TRANSACTION TYPE'"
+ [editMode]="editMode"
+ [disabled]="!form.valid"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/accounting/transactionTypes/form/transaction-type-form.component.spec.ts b/src/app/accounting/transactionTypes/form/transaction-type-form.component.spec.ts
new file mode 100644
index 0000000..ca63cef
--- /dev/null
+++ b/src/app/accounting/transactionTypes/form/transaction-type-form.component.spec.ts
@@ -0,0 +1,103 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, DebugElement} from '@angular/core';
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {By} from '@angular/platform-browser';
+import {TranslateModule} from '@ngx-translate/core';
+import {ReactiveFormsModule} from '@angular/forms';
+import {FimsSharedModule} from '../../../common/common.module';
+import {CovalentStepsModule} from '@covalent/core';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {TransactionTypeFormComponent} from './transaction-type-form.component';
+import {TransactionType} from '../../../services/accounting/domain/transaction-type.model';
+import {MatInputModule} from '@angular/material';
+
+describe('Test transaction type form', () => {
+
+ let fixture: ComponentFixture<TestComponent>;
+
+ let testComponent: TestComponent;
+
+ beforeEach( async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot(),
+ MatInputModule,
+ CovalentStepsModule,
+ FimsSharedModule,
+ ReactiveFormsModule,
+ NoopAnimationsModule
+ ],
+ declarations: [
+ TransactionTypeFormComponent,
+ TestComponent
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TestComponent);
+ testComponent = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should trigger save event', () => {
+ const button: DebugElement = fixture.debugElement.query(By.css('button[mat-raised-button]'));
+
+ button.nativeElement.click();
+
+ expect(testComponent.savedType).toEqual(testComponent.type);
+ });
+
+ it('should trigger cancel event', () => {
+ const button: DebugElement = fixture.debugElement.query(By.css('button[mat-button]'));
+
+ button.nativeElement.click();
+
+ expect(testComponent.canceled).toBeTruthy();
+ });
+
+});
+
+@Component({
+ template: `
+ <fims-transaction-type-form (onSave)="onSave($event)" (onCancel)="onCancel($event)" [transactionType]="type" [editMode]="true">
+ </fims-transaction-type-form>`
+})
+class TestComponent {
+
+ type: TransactionType = {
+ code: 'test',
+ name: 'test',
+ description: 'test'
+ };
+
+ savedType: TransactionType;
+
+ canceled: boolean;
+
+ onSave(type: TransactionType): void {
+ this.savedType = type;
+ }
+
+ onCancel(): void {
+ this.canceled = true;
+ }
+
+}
diff --git a/src/app/accounting/transactionTypes/form/transaction-type-form.component.ts b/src/app/accounting/transactionTypes/form/transaction-type-form.component.ts
new file mode 100644
index 0000000..b6cc982
--- /dev/null
+++ b/src/app/accounting/transactionTypes/form/transaction-type-form.component.ts
@@ -0,0 +1,79 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
+import {FormComponent} from '../../../common/forms/form.component';
+import {TransactionType} from '../../../services/accounting/domain/transaction-type.model';
+import {TdStepComponent} from '@covalent/core';
+import {FormBuilder, Validators} from '@angular/forms';
+import {FimsValidators} from '../../../common/validator/validators';
+
+@Component({
+ selector: 'fims-transaction-type-form',
+ templateUrl: './transaction-type-form.component.html'
+})
+export class TransactionTypeFormComponent extends FormComponent<TransactionType> implements OnInit {
+
+ @ViewChild('detailsStep') step: TdStepComponent;
+
+ @Input() transactionType: TransactionType;
+
+ @Input() editMode = false;
+
+ @Output('onSave') onSave = new EventEmitter<TransactionType>();
+
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder) {
+ super();
+ }
+
+ ngOnInit(): void {
+ this.form = this.formBuilder.group({
+ code: [this.transactionType.code, [Validators.required, Validators.minLength(3), Validators.maxLength(32), FimsValidators.urlSafe]],
+ name: [this.transactionType.name, [Validators.required, Validators.maxLength(256)]],
+ description: [this.transactionType.description, [Validators.maxLength(2048)]]
+ });
+
+ this.step.open();
+ }
+
+ showNumberValidationError(): void {
+ this.setError('number', 'unique', true);
+ }
+
+ get formData(): TransactionType {
+ // Not needed
+ return;
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+ save(): void {
+ const transactionType: TransactionType = {
+ code: this.form.get('code').value,
+ name: this.form.get('name').value,
+ description: this.form.get('description').value
+ };
+
+ this.onSave.emit(transactionType);
+ }
+
+}
diff --git a/src/app/accounting/transactionTypes/transaction-type-exists.guard.ts b/src/app/accounting/transactionTypes/transaction-type-exists.guard.ts
new file mode 100644
index 0000000..af36452
--- /dev/null
+++ b/src/app/accounting/transactionTypes/transaction-type-exists.guard.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import * as fromAccounting from '../store/index';
+import {AccountingStore} from '../store/index';
+import {ExistsGuardService} from '../../common/guards/exists-guard';
+import {AccountingService} from '../../services/accounting/accounting.service';
+import {Observable} from 'rxjs/Observable';
+import {LoadAction} from '../store/ledger/transaction-type/transaction-type.actions';
+import {of} from 'rxjs/observable/of';
+
+@Injectable()
+export class TransactionTypeExistsGuard implements CanActivate {
+
+ constructor(private store: AccountingStore,
+ private accountingService: AccountingService,
+ private existsGuardService: ExistsGuardService) {
+ }
+
+ hasTransactionTypeInStore(code: string): Observable<boolean> {
+ const timestamp$: Observable<number> = this.store.select(fromAccounting.getTransactionTypeLoadedAt)
+ .map(loadedAt => loadedAt[code]);
+
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ hasTransactionTypeInApi(code: string): Observable<boolean> {
+ const getTransactionType: Observable<any> = this.accountingService.findTransactionType(code)
+ .map(transactionTypeEntity => new LoadAction({
+ resource: transactionTypeEntity
+ }))
+ .do((action: LoadAction) => this.store.dispatch(action))
+ .map(transactionType => !!transactionType);
+
+ return this.existsGuardService.routeTo404OnError(getTransactionType);
+ }
+
+ hasTransactionType(code: string): Observable<boolean> {
+ return this.hasTransactionTypeInStore(code)
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+ return this.hasTransactionTypeInApi(code);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasTransactionType(route.params['code']);
+ }
+}
diff --git a/src/app/accounting/transactionTypes/transaction-types.list.component.html b/src/app/accounting/transactionTypes/transaction-types.list.component.html
new file mode 100644
index 0000000..bcd2565
--- /dev/null
+++ b/src/app/accounting/transactionTypes/transaction-types.list.component.html
@@ -0,0 +1,30 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Manage transaction types' | translate}}" [navigateBackTo]="['../']">
+ <fims-data-table
+ (onFetch)="fetchTransactionTypes($event)"
+ (onActionCellClick)="rowSelect($event)"
+ [columns]="columns"
+ [data]="transactionTypesData$ | async"
+ [loading]="loading$ | async"
+ [sortable]="true"
+ [pageable]="true"
+ [actionColumnLabel]="'EDIT'">
+ </fims-data-table>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Create new transaction type' | translate}}" icon="add" [link]="['create']" [permission]="{ id: 'accounting_tx_types', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/accounting/transactionTypes/transaction-types.list.component.ts b/src/app/accounting/transactionTypes/transaction-types.list.component.ts
new file mode 100644
index 0000000..6b7f04c
--- /dev/null
+++ b/src/app/accounting/transactionTypes/transaction-types.list.component.ts
@@ -0,0 +1,76 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import * as fromAccounting from '../store/index';
+import {AccountingStore} from '../store/index';
+import {TableData, TableFetchRequest} from '../../common/data-table/data-table.component';
+import {FetchRequest} from '../../services/domain/paging/fetch-request.model';
+import {SEARCH} from '../store/ledger/transaction-type/transaction-type.actions';
+import {TransactionType} from '../../services/accounting/domain/transaction-type.model';
+import {ActivatedRoute, Router} from '@angular/router';
+
+@Component({
+ templateUrl: './transaction-types.list.component.html'
+})
+export class TransactionTypeListComponent implements OnInit {
+
+ transactionTypesData$: Observable<TableData>;
+
+ loading$: Observable<boolean>;
+
+ columns: any[] = [
+ { name: 'code', label: 'Code' },
+ { name: 'name', label: 'Name' },
+ { name: 'description', label: 'Description' }
+ ];
+
+ private searchTerm: string;
+
+ private lastFetchRequest: FetchRequest = {};
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: AccountingStore) {}
+
+ ngOnInit(): void {
+ this.transactionTypesData$ = this.store.select(fromAccounting.getTransactionTypeSearchResults)
+ .map(transactionTypePage => ({
+ data: transactionTypePage.transactionTypes,
+ totalElements: transactionTypePage.totalElements,
+ totalPages: transactionTypePage.totalPages
+ }));
+
+ this.loading$ = this.store.select(fromAccounting.getTransactionTypeSearchLoading);
+
+ this.fetchTransactionTypes();
+ }
+
+ rowSelect(transactionType: TransactionType): void {
+ this.router.navigate(['edit', transactionType.code], { relativeTo: this.route });
+ }
+
+ fetchTransactionTypes(fetchRequest?: TableFetchRequest): void {
+ if (fetchRequest) {
+ this.lastFetchRequest = fetchRequest;
+ }
+
+ this.lastFetchRequest.searchTerm = this.searchTerm;
+
+ this.store.dispatch({ type: SEARCH, payload: this.lastFetchRequest });
+ }
+}
diff --git a/src/app/app.component.html b/src/app/app.component.html
index 90c6b64..ca721b3 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -1 +1,18 @@
-<router-outlet></router-outlet>
\ No newline at end of file
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<router-outlet></router-outlet>
diff --git a/src/app/app.component.scss b/src/app/app.component.scss
index e69de29..fcd652e 100644
--- a/src/app/app.component.scss
+++ b/src/app/app.component.scss
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/* :host /deep/ lets shadowdom style child elements */
+:host /deep/ {
+ /**
+ * CSS Overrides for bug fixes
+ */
+
+ /**
+ * END CSS Overrides for bug fixes
+ */
+
+ /* Manage list custom styles */
+ .md-sort-item {
+ /deep/ {
+ .md-list-item {
+ padding: 0;
+ }
+ }
+ }
+ .md-sort-icon {
+ font-size: 15px;
+ margin-right: 10px;
+ }
+ .md-sort-header {
+ padding: 10px;
+ &:hover {
+ background-color: #EEEEEE;
+ cursor: pointer;
+ }
+ }
+
+}
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 57b1bce..8f9f642 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -1,10 +1,62 @@
-import { Component } from '@angular/core';
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {TranslateService} from '@ngx-translate/core';
+import * as fromRoot from './store';
+import {Store} from '@ngrx/store';
+import {LoginSuccessAction} from './store/security/security.actions';
+import {Subscription} from 'rxjs/Subscription';
+import {getSelectedLanguage} from './common/i18n/translate';
@Component({
- selector: 'app-root',
+ selector: 'fims-app',
templateUrl: './app.component.html',
- styleUrls: ['./app.component.scss']
+ styleUrls: ['./app.component.scss'],
})
-export class AppComponent {
-
+export class AppComponent implements OnInit, OnDestroy {
+
+ private authSubscription: Subscription;
+
+ constructor(private translate: TranslateService, private store: Store<fromRoot.State>) {}
+
+ ngOnInit(): void {
+ this.translate.addLangs(['en', 'es']);
+ this.translate.setDefaultLang('en');
+ this.translate.use(getSelectedLanguage(this.translate));
+ // this.relogin();
+ }
+
+ relogin(): void {
+ this.store.select(fromRoot.getAuthenticationState)
+ .filter(state => !!state.authentication)
+ .take(1)
+ .map(state => ({
+ username: state.username,
+ tenant: state.tenant,
+ authentication: state.authentication
+ }))
+ .map(payload => new LoginSuccessAction(payload))
+ .subscribe((action: LoginSuccessAction) => this.store.dispatch(action));
+ }
+
+ ngOnDestroy(): void {
+ this.authSubscription.unsubscribe();
+ }
}
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 70b772a..2607262 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -1,221 +1,100 @@
-import { BrowserModule } from '@angular/platform-browser';
-import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
-import { FormsModule, ReactiveFormsModule } from '@angular/forms'
-import { RouterModule, Routes } from '@angular/router';
-import { HttpModule, Http } from '@angular/http';
-import { CommonModule } from '@angular/common';
-import { TranslateModule, TranslateStore } from '@ngx-translate/core';
-import {CovalentLoadingModule} from '@covalent/core';
-
-import {
- MatAutocompleteModule,
- MatButtonModule,
- MatButtonToggleModule,
- MatCheckboxModule,
- MatToolbarModule,
- MatTooltipModule,
- MatCardModule,
- MatChipsModule,
- MatDatepickerModule,
- MatDialogModule,
- MatExpansionModule,
- MatFormFieldModule,
- MatGridListModule,
- MatIconModule,
- MatInputModule,
- MatListModule,
- MatMenuModule,
- MatNativeDateModule,
- MatPaginatorModule,
- MatProgressBarModule,
- MatProgressSpinnerModule,
- MatRadioModule,
- MatRippleModule,
- MatSelectModule,
- MatSidenavModule,
- MatSliderModule,
- MatSlideToggleModule,
- MatSnackBarModule,
- MatSortModule,
- MatTableModule,
- MatTabsModule,
- MatStepperModule
-
-} from '@angular/material';
-
-
-import { LOCALE_ID, NgModule } from '@angular/core';
-
-
-import { AppComponent } from './app.component';
-import { LoginComponent } from './login/login.component';
-import { NavbarComponent } from './navbar/navbar.component';
-import { DashboardComponent } from './dashboard/dashboard.component';
-import { AccountingComponent } from './accounting/accounting.component';
-import { GeneralLedgerComponent } from './accounting/general-ledger/general-ledger.component';
-import { AddJournalEntryComponent } from './accounting/add-journal-entry/add-journal-entry.component';
-
-import { PayrollsComponent } from './accounting/payrolls/payrolls.component';
-import { ChartOfAccountsComponent } from './accounting/chart-of-accounts/chart-of-accounts.component';
-import { AddTransactionTypeComponent } from './accounting/add-transaction-type/add-transaction-type.component';
-import { TrialBalanceComponent } from './accounting/trial-balance/trial-balance.component';
-import { ChequeClearingComponent } from './accounting/cheque-clearing/cheque-clearing.component';
-import { TransactionTypeComponent } from './accounting/transaction-type/transaction-type.component';
-import { AddMemberComponent } from './customer/add-member/add-member.component';
-import { ManageMembersComponent } from './customer/manage-members/manage-members.component';
-import { AddEmployeeComponent } from './employee/add-employee/add-employee.component';
-import { ManageEmployeeComponent } from './employee/manage-employee/manage-employee.component';
-import { AddOfficeComponent } from './office/add-office/add-office.component';
-import { ViewOfficesComponent } from './office/view-offices/view-offices.component';
-import { AddLedgerComponent } from './accounting/add-ledger/add-ledger.component';
-import { AccountPayableComponent } from './accounting/account-payable/account-payable.component';
-import { AddChequeComponent } from './accounting/add-cheque/add-cheque.component';
-import { AddPayrollComponent } from './accounting/add-payroll/add-payroll.component';
-
-import { HttpClients } from './sevices/http/http.service';
-import { IdentityService } from './sevices/identity/identity.service';
-import { OfficeService } from './sevices/office/office.service';
-import { CustomerService } from './sevices/customer/customer.service';
-import { AuthenticationService } from './sevices/security/authn/authentication.service';
-import { CatalogService } from './sevices/catalog/catalog.service';
-import { AccountingService } from './sevices/accounting/accounting.service';
-import { PortfolioService } from './sevices/portfolio/portfolio.service';
-import { TranslateLoader, TranslateService } from '@ngx-translate/core';
-import { TranslateHttpLoader } from '@ngx-translate/http-loader';
-import { PermittableGroupIdMapper } from './sevices/security/authz/permittable-group-id-mapper';
-import { reducer } from './store';
-import { StoreModule } from '@ngrx/store';
-import { EffectsModule } from '@ngrx/effects';
-import { NotificationService } from './sevices/notification/notification.service';
-import { OfficeSearchApiEffects } from './store/office/effects/service.effects';
-import { EmployeeSearchApiEffects } from './store/employee/effects/service.effects';
-import { RoleSearchApiEffects } from './store/role/effects/service.effects';
-import { CustomerSearchApiEffects } from './store/customer/effects/service.effects';
-import { AccountSearchApiEffects } from './store/account/effects/service.effects';
-import { SecurityRouteEffects } from './store/security/effects/route.effects';
-import { SecurityApiEffects } from './store/security/effects/service.effects';
-import { SecurityNotificationEffects } from './store/security/effects/notification.effects';
-import { LedgerSearchApiEffects } from './store/ledger/effects/service.effects';
-//import {ExistsGuardService} from './common/guards/exists-guard';
-import { ImageService } from './sevices/image/image.service';
-import { DepositAccountService } from './sevices/depositAccount/deposit-account.service';
-import { CurrencyService } from './sevices/currency/currency.service';
-import { TellerService } from './sevices/teller/teller-service';
-import { ReportingService } from './sevices/reporting/reporting.service';
-import { getSelectedLanguage } from './common/i18n/translate';
-import { ChequeService } from './sevices/cheque/cheque.service';
-import { PayrollService } from './sevices/payroll/payroll.service';
-import { CountryService } from './sevices/country/country.service';
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {LOCALE_ID, NgModule} from '@angular/core';
+import {BrowserModule} from '@angular/platform-browser';
+import {Http, HttpModule} from '@angular/http';
+import {AppComponent} from './app.component';
+import {appRoutes, appRoutingProviders} from './app.routes';
+import {HttpClient} from './services/http/http.service';
+import {IdentityService} from './services/identity/identity.service';
+import {OfficeService} from './services/office/office.service';
+import {CustomerService} from './services/customer/customer.service';
+import {AuthenticationService} from './services/security/authn/authentication.service';
+import {CatalogService} from './services/catalog/catalog.service';
+import {AccountingService} from './services/accounting/accounting.service';
+import {PortfolioService} from './services/portfolio/portfolio.service';
+import {TranslateLoader, TranslateModule, TranslateService} from '@ngx-translate/core';
+import {TranslateHttpLoader} from '@ngx-translate/http-loader';
+import {PermittableGroupIdMapper} from './services/security/authz/permittable-group-id-mapper';
+import {reducer} from './store';
+import {StoreModule} from '@ngrx/store';
+import {EffectsModule} from '@ngrx/effects';
+import {NotificationService} from './services/notification/notification.service';
+import {OfficeSearchApiEffects} from './store/office/effects/service.effects';
+import {EmployeeSearchApiEffects} from './store/employee/effects/service.effects';
+import {RoleSearchApiEffects} from './store/role/effects/service.effects';
+import {CustomerSearchApiEffects} from './store/customer/effects/service.effects';
+import {AccountSearchApiEffects} from './store/account/effects/service.effects';
+import {SecurityRouteEffects} from './store/security/effects/route.effects';
+import {SecurityApiEffects} from './store/security/effects/service.effects';
+import {SecurityNotificationEffects} from './store/security/effects/notification.effects';
+import {LedgerSearchApiEffects} from './store/ledger/effects/service.effects';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {ExistsGuardService} from './common/guards/exists-guard';
+import {CountryService} from './services/country/country.service';
+import {ImageService} from './services/image/image.service';
+import {DepositAccountService} from './services/depositAccount/deposit-account.service';
+import {CurrencyService} from './services/currency/currency.service';
+import {TellerService} from './services/teller/teller-service';
+import {ReportingService} from './services/reporting/reporting.service';
+import {getSelectedLanguage} from './common/i18n/translate';
+import {ChequeService} from './services/cheque/cheque.service';
+import {PayrollService} from './services/payroll/payroll.service';
export function HttpLoaderFactory(http: Http) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
-
-const appRoutes: Routes = [
- { path: 'login', component: LoginComponent },
- { path: '', redirectTo: 'login', pathMatch: 'full' },
-
- {
- path: 'navbar', component: NavbarComponent, children: [
- { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
- { path: 'dashboard', component: DashboardComponent },
- { path: 'accounting', component: AccountingComponent },
- { path: 'GL', component: GeneralLedgerComponent },
- { path: 'add_journal_entry', component: AddJournalEntryComponent },
- { path: 'payroll', component: PayrollsComponent },
- { path: 'chart_of_accounts', component: ChartOfAccountsComponent },
- { path: 'add_transaction_type', component: AddTransactionTypeComponent },
- { path: 'trial_balance', component: TrialBalanceComponent },
- { path: 'cheque_clearing', component: ChequeClearingComponent },
- { path: 'transaction_type', component: TransactionTypeComponent },
- { path: 'add_member', component: AddMemberComponent },
- { path: 'manage_members', component: ManageMembersComponent },
- { path: 'add_employee', component: AddEmployeeComponent },
- { path: 'manage_employees', component: ManageEmployeeComponent },
- { path: 'view_offices', component: ViewOfficesComponent },
- { path: 'add_office', component: AddOfficeComponent },
- { path: 'add_ledger', component: AddLedgerComponent },
- { path: 'account_payable', component: AccountPayableComponent },
- { path: 'add_cheque', component: AddChequeComponent },
- { path: 'add_payroll', component: AddPayrollComponent },
- ]
- }
-];
-
-
@NgModule({
declarations: [
- AppComponent, LoginComponent, NavbarComponent, DashboardComponent,
- AccountingComponent, GeneralLedgerComponent, AddJournalEntryComponent,
- PayrollsComponent, ChartOfAccountsComponent, AddTransactionTypeComponent,
- TrialBalanceComponent, ChequeClearingComponent, TransactionTypeComponent,
- AddMemberComponent, ManageMembersComponent, AddEmployeeComponent, ManageEmployeeComponent, AddOfficeComponent,
- ViewOfficesComponent, AddLedgerComponent, AccountPayableComponent, AddChequeComponent, AddPayrollComponent,
-
+ AppComponent
],
- imports: [RouterModule.forRoot(appRoutes),
- BrowserModule, BrowserAnimationsModule,
- CommonModule, TranslateModule, CovalentLoadingModule,
- FormsModule, ReactiveFormsModule, HttpModule,
- TranslateModule.forRoot({
- loader: {
- provide: TranslateLoader,
- useFactory: HttpLoaderFactory,
- deps: [Http]
- }
- }),
- StoreModule.forRoot(reducer),
+ imports: [
+ BrowserModule,
+ BrowserAnimationsModule,
+ HttpModule,
+ TranslateModule.forRoot({
+ loader: {
+ provide: TranslateLoader,
+ useFactory: HttpLoaderFactory,
+ deps: [Http]
+ }
+ }),
+ appRoutes,
+ StoreModule.provideStore(reducer),
- /**
- * Root effects
- */
- EffectsModule.forRoot([SecurityApiEffects,
- SecurityRouteEffects,
- SecurityNotificationEffects,
+ /**
+ * Root effects
+ */
+ EffectsModule.run(SecurityApiEffects),
+ EffectsModule.run(SecurityRouteEffects),
+ EffectsModule.run(SecurityNotificationEffects),
- OfficeSearchApiEffects,
- EmployeeSearchApiEffects,
- CustomerSearchApiEffects,
- AccountSearchApiEffects,
- RoleSearchApiEffects,
- LedgerSearchApiEffects
- ]),
-
- MatAutocompleteModule,
- MatButtonModule,
- MatButtonToggleModule,
- MatCheckboxModule,
- MatToolbarModule,
- MatTooltipModule,
- MatCardModule,
- MatChipsModule,
- MatDatepickerModule,
- MatDialogModule,
- MatExpansionModule,
- MatFormFieldModule,
- MatGridListModule,
- MatIconModule,
- MatInputModule,
- MatListModule,
- MatMenuModule,
- MatNativeDateModule,
- MatPaginatorModule,
- MatProgressBarModule,
- MatProgressSpinnerModule,
- MatRadioModule,
- MatRippleModule,
- MatSelectModule,
- MatSidenavModule,
- MatSliderModule,
- MatSlideToggleModule,
- MatSnackBarModule,
- MatSortModule,
- MatTableModule,
- MatTabsModule,
- MatStepperModule
+ EffectsModule.run(OfficeSearchApiEffects),
+ EffectsModule.run(EmployeeSearchApiEffects),
+ EffectsModule.run(CustomerSearchApiEffects),
+ EffectsModule.run(AccountSearchApiEffects),
+ EffectsModule.run(RoleSearchApiEffects),
+ EffectsModule.run(LedgerSearchApiEffects)
],
- providers: [HttpClients,
+ providers: [
+ HttpClient,
AuthenticationService,
PermittableGroupIdMapper,
IdentityService,
@@ -232,14 +111,13 @@
CountryService,
CurrencyService,
NotificationService,
- TranslateService,
-
- // ExistsGuardService,
- // ...appRoutingProviders,
+ ExistsGuardService,
+ ...appRoutingProviders,
ImageService,
{
provide: LOCALE_ID, useFactory: getSelectedLanguage, deps: [TranslateService],
- }],
- bootstrap: [AppComponent]
+ }
+ ],
+ bootstrap: [ AppComponent ]
})
-export class AppModule { }
+export class AppModule {}
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
new file mode 100644
index 0000000..8803f78
--- /dev/null
+++ b/src/app/app.routes.ts
@@ -0,0 +1,31 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {NoPreloading, RouterModule, Routes} from '@angular/router';
+import {AuthGuard} from './services/security/authn/auth-guard.service';
+
+const routes: Routes = [
+ {path: 'login', loadChildren: './login/login.module#LoginModule'},
+ {path: '', loadChildren: './main/main.module#MainModule', canActivate: [AuthGuard] }
+];
+
+export const appRoutingProviders: any[] = [
+ AuthGuard
+];
+
+export const appRoutes: any = RouterModule.forRoot(routes, { preloadingStrategy: NoPreloading });
diff --git a/src/app/common/account-select/account-select.component.html b/src/app/common/account-select/account-select.component.html
new file mode 100644
index 0000000..8368bd2
--- /dev/null
+++ b/src/app/common/account-select/account-select.component.html
@@ -0,0 +1,31 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<div layout="row">
+ <mat-form-field layout-margin flex>
+ <input matInput [placeholder]="title" [matAutocomplete]="auto" [formControl]="formControl">
+ <mat-hint class="tc-red-600">
+ <ng-content></ng-content>
+ </mat-hint>
+ </mat-form-field>
+</div>
+
+<mat-autocomplete #auto="matAutocomplete">
+ <mat-option *ngFor="let account of accounts | async" [value]="account.identifier">
+ {{ account.identifier }}({{account.name}})
+ </mat-option>
+</mat-autocomplete>
diff --git a/src/app/common/account-select/account-select.component.ts b/src/app/common/account-select/account-select.component.ts
new file mode 100644
index 0000000..40fb645
--- /dev/null
+++ b/src/app/common/account-select/account-select.component.ts
@@ -0,0 +1,105 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, forwardRef, Input, OnInit} from '@angular/core';
+import {FetchRequest} from '../../services/domain/paging/fetch-request.model';
+import {Observable} from 'rxjs/Observable';
+import {AccountingService} from '../../services/accounting/accounting.service';
+import {Account} from '../../services/accounting/domain/account.model';
+import {AccountPage} from '../../services/accounting/domain/account-page.model';
+import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';
+import {AccountType} from '../../services/accounting/domain/account-type.model';
+
+const noop: () => void = () => {
+ // empty method
+};
+
+@Component({
+ providers: [
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AccountSelectComponent), multi: true }
+ ],
+ selector: 'fims-account-select',
+ templateUrl: './account-select.component.html'
+})
+export class AccountSelectComponent implements ControlValueAccessor, OnInit {
+
+ formControl: FormControl;
+
+ @Input() title: string;
+
+ @Input() required: boolean;
+
+ @Input() type: AccountType;
+
+ accounts: Observable<Account[]>;
+
+ private _onTouchedCallback: () => void = noop;
+
+ private _onChangeCallback: (_: any) => void = noop;
+
+ constructor(private accountingService: AccountingService) {}
+
+ ngOnInit(): void {
+ this.formControl = new FormControl('');
+
+ this.accounts = this.formControl.valueChanges
+ .distinctUntilChanged()
+ .debounceTime(500)
+ .do(name => this.changeValue(name))
+ .filter(name => name)
+ .switchMap(name => this.onSearch(name));
+ }
+
+ changeValue(value: string): void {
+ this._onChangeCallback(value);
+ }
+
+ writeValue(value: any): void {
+ this.formControl.setValue(value);
+ }
+
+ registerOnChange(fn: any): void {
+ this._onChangeCallback = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ this._onTouchedCallback = fn;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ if (isDisabled) {
+ this.formControl.disable();
+ } else {
+ this.formControl.enable();
+ }
+ }
+
+ onSearch(searchTerm?: string): Observable<Account[]> {
+ const fetchRequest: FetchRequest = {
+ page: {
+ pageIndex: 0,
+ size: 5
+ },
+ searchTerm: searchTerm
+ };
+
+ return this.accountingService.fetchAccounts(fetchRequest, this.type)
+ .map((accountPage: AccountPage) => accountPage.accounts);
+ }
+
+}
diff --git a/src/app/common/address/address.component.html b/src/app/common/address/address.component.html
new file mode 100644
index 0000000..3199b61
--- /dev/null
+++ b/src/app/common/address/address.component.html
@@ -0,0 +1,33 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<form [formGroup]="form" layout="column">
+ <fims-text-input [form]="form" controlName="street" placeholder="{{'Street' | translate}}"></fims-text-input>
+ <fims-text-input [form]="form" controlName="city" placeholder="{{'City' | translate}}"></fims-text-input>
+ <fims-text-input [form]="form" controlName="postalCode" placeholder="{{'Postal code(optional)' | translate}}"></fims-text-input>
+ <mat-form-field layout-margin flex>
+ <input matInput placeholder="{{'Country' | translate}}" formControlName="country" [matAutocomplete]="auto">
+ <mat-error *ngIf="form.get('country').hasError('required')" translate>Required</mat-error>
+ <mat-error *ngIf="form.get('country').hasError('invalidCountry')" translate>Invalid country</mat-error>
+ </mat-form-field>
+ <mat-autocomplete #auto="matAutocomplete" [displayWith]="countryDisplay">
+ <mat-option *ngFor="let country of filteredCountries | async" [value]="country">
+ {{country.displayName}}
+ </mat-option>
+ </mat-autocomplete>
+ <fims-text-input [form]="form" controlName="region" placeholder="{{'Region(optional)' | translate}}"></fims-text-input>
+</form>
diff --git a/src/app/common/address/address.component.spec.ts b/src/app/common/address/address.component.spec.ts
new file mode 100644
index 0000000..e65aeb6
--- /dev/null
+++ b/src/app/common/address/address.component.spec.ts
@@ -0,0 +1,98 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, ViewChild} from '@angular/core';
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {ReactiveFormsModule} from '@angular/forms';
+import {CovalentStepsModule} from '@covalent/core';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {AddressFormComponent} from './address.component';
+import {Address} from '../../services/domain/address/address.model';
+import {CountryService} from '../../services/country/country.service';
+import {Country} from '../../services/country/model/country.model';
+import {FimsSharedModule} from '../common.module';
+import {MatAutocompleteModule, MatInputModule} from '@angular/material';
+
+const country: Country = {
+ displayName: 'country',
+ name: 'country',
+ alpha2Code: 'countryCode',
+ translations: {}
+};
+
+describe('Test address form', () => {
+
+ let fixture: ComponentFixture<TestComponent>;
+
+ let testComponent: TestComponent;
+
+ beforeEach( async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot(),
+ FimsSharedModule,
+ MatInputModule,
+ MatAutocompleteModule,
+ CovalentStepsModule,
+ ReactiveFormsModule,
+ NoopAnimationsModule
+ ],
+ declarations: [
+ TestComponent
+ ],
+ providers: [
+ {
+ provide: CountryService, useClass: class {
+ fetchByCountryCode = jasmine.createSpy('fetchByCountryCode').and.returnValue(country);
+ fetchCountries = jasmine.createSpy('fetchCountries').and.returnValue([country]);
+ }
+ }
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TestComponent);
+ testComponent = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should return same address', () => {
+ expect(testComponent.form.formData).toEqual(testComponent.address);
+ });
+
+});
+
+@Component({
+ template: '<fims-address-form #form [formData]="address"></fims-address-form>'
+})
+class TestComponent {
+
+ @ViewChild('form') form: AddressFormComponent;
+
+ address: Address = {
+ street: 'street',
+ city: 'city',
+ region: 'region',
+ postalCode: 'postalCode',
+ countryCode: 'countryCode',
+ country: 'country'
+ };
+
+}
diff --git a/src/app/common/address/address.component.ts b/src/app/common/address/address.component.ts
new file mode 100644
index 0000000..d27dd73
--- /dev/null
+++ b/src/app/common/address/address.component.ts
@@ -0,0 +1,79 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input, OnInit} from '@angular/core';
+import {FormComponent} from '../forms/form.component';
+import {FormBuilder, Validators} from '@angular/forms';
+import {Address} from '../../services/domain/address/address.model';
+import {Country} from '../../services/country/model/country.model';
+import {CountryService} from '../../services/country/country.service';
+import {countryExists} from '../validator/country-exists.validator';
+import {Observable} from 'rxjs/Observable';
+
+@Component({
+ selector: 'fims-address-form',
+ templateUrl: './address.component.html'
+})
+export class AddressFormComponent extends FormComponent<Address> implements OnInit {
+
+ filteredCountries: Observable<Country[]>;
+
+ @Input() set formData(address: Address) {
+ let country: Country;
+
+ if (address) {
+ country = this.countryService.fetchByCountryCode(address.countryCode);
+ }
+
+ this.form = this.formBuilder.group({
+ street: [address ? address.street : undefined, [Validators.required, Validators.maxLength(256)]],
+ city: [address ? address.city : undefined, [Validators.required, Validators.maxLength(256)]],
+ postalCode: [address ? address.postalCode : undefined, Validators.maxLength(32)],
+ region: [address ? address.region : undefined, Validators.maxLength(256)],
+ country: [country, [Validators.required], countryExists(this.countryService)]
+ });
+ };
+
+ constructor(private formBuilder: FormBuilder, private countryService: CountryService) {
+ super();
+ }
+
+ ngOnInit(): void {
+ this.filteredCountries = this.form.get('country').valueChanges
+ .startWith(null)
+ .map(country => country && typeof country === 'object' ? country.displayName : country)
+ .map(searchTerm => this.countryService.fetchCountries(searchTerm));
+ }
+
+ get formData(): Address {
+ const country: Country = this.form.get('country').value;
+
+ return {
+ street: this.form.get('street').value,
+ city: this.form.get('city').value,
+ postalCode: this.form.get('postalCode').value,
+ region: this.form.get('region').value,
+ country: country.name,
+ countryCode: country.alpha2Code
+ };
+ }
+
+ countryDisplay(country: Country): string {
+ return country ? country.displayName : undefined;
+ }
+}
diff --git a/src/app/common/command-display/command-display.component.html b/src/app/common/command-display/command-display.component.html
new file mode 100644
index 0000000..52f2345
--- /dev/null
+++ b/src/app/common/command-display/command-display.component.html
@@ -0,0 +1,29 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<mat-list-item [ngSwitch]="command.action">
+ <mat-icon matListAvatar color="primary" *ngSwitchCase="'ACTIVATE'">check_circle</mat-icon>
+ <mat-icon matListAvatar color="primary" *ngSwitchCase="'LOCK'">lock</mat-icon>
+ <mat-icon matListAvatar color="primary" *ngSwitchCase="'CLOSE'">close</mat-icon>
+ <mat-icon matListAvatar color="primary" *ngSwitchCase="'UNLOCK'">lock_open</mat-icon>
+ <h3 matLine *ngSwitchCase="'ACTIVATE'" translate>ACTIVATE</h3>
+ <h3 matLine *ngSwitchCase="'LOCK'" translate>LOCK</h3>
+ <h3 matLine *ngSwitchCase="'CLOSE'" translate>CLOSE</h3>
+ <h3 matLine *ngSwitchCase="'UNLOCK'" translate>UNLOCK</h3>
+ <h4 matLine>{{command.createdBy}}, {{command.createdOn | date:'medium'}}</h4>
+ <p matLine>{{command.comment}}</p>
+</mat-list-item>
diff --git a/src/app/common/command-display/command-display.component.ts b/src/app/common/command-display/command-display.component.ts
new file mode 100644
index 0000000..d641dc3
--- /dev/null
+++ b/src/app/common/command-display/command-display.component.ts
@@ -0,0 +1,27 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input} from '@angular/core';
+
+@Component({
+ selector: 'fims-command-display',
+ templateUrl: './command-display.component.html'
+})
+export class CommandDisplayComponent {
+ @Input() command: any;
+}
diff --git a/src/app/common/common.module.ts b/src/app/common/common.module.ts
new file mode 100644
index 0000000..88a67d6
--- /dev/null
+++ b/src/app/common/common.module.ts
@@ -0,0 +1,158 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {NgModule} from '@angular/core';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {CovalentCommonModule, CovalentDataTableModule, CovalentDialogsModule, CovalentPagingModule} from '@covalent/core';
+import {LayoutCardOverComponent, LayoutCardOverComponentTagsDirective} from './layout-card-over/layout-card-over.component';
+import {IdInputComponent} from './id-input/id-input.component';
+import {PermissionDirective} from '../services/security/authz/permission.directive';
+import {DataTableComponent} from './data-table/data-table.component';
+import {StateDisplayComponent} from './state-display/state-display.component';
+import {CommandDisplayComponent} from './command-display/command-display.component';
+import {CustomerSelectComponent} from './customer-select/customer-select.component';
+import {SelectListComponent} from './select-list/select-list.component';
+import {EmployeeSelectComponent} from './employee-select/employee-select.component';
+import {AccountSelectComponent} from './account-select/account-select.component';
+import {ProductSelectComponent} from './product-select/product-select.component';
+import {TranslateModule} from '@ngx-translate/core';
+import {MinMaxComponent} from './min-max/min-max.component';
+import {ValidateOnBlurDirective} from './validate-on-blur.directive';
+import {LedgerSelectComponent} from './ledger-select/ledger-select.component';
+import {FormContinueActionComponent} from './forms/form-continue-action.component';
+import {FormFinalActionComponent} from './forms/form-final-action.component';
+import {AddressFormComponent} from './address/address.component';
+import {PortraitComponent} from './portrait/portrait.component';
+import {CommonModule} from '@angular/common';
+import {
+ MatAutocompleteModule,
+ MatButtonModule,
+ MatCardModule,
+ MatIconModule,
+ MatInputModule,
+ MatListModule,
+ MatOptionModule,
+ MatSelectModule,
+ MatSnackBarModule,
+ MatToolbarModule,
+ MatTooltipModule
+} from '@angular/material';
+import {EmployeeAutoCompleteComponent} from './employee-autocomplete/employee-auto-complete.component';
+import {TextMaskModule} from 'angular2-text-mask';
+import {NumberInputComponent} from './number-input/number-input.component';
+import {ImageComponent} from './image/image.component';
+import {FimsTwoColumnLayoutComponent} from './layouts/two-column-layout.component';
+import {FimsFabButtonComponent} from './fab-button/fab-button.component';
+import {RouterModule} from '@angular/router';
+import {DisplayFimsDate} from './date/fims-date.pipe';
+import {DateInputComponent} from './date-input/date-input.component';
+import {TextInputComponent} from './text-input/text-input.component';
+import {DisplayFimsNumber} from './number/fims-number.pipe';
+import {DisplayFimsFinancialNumber} from './number/fims-financial-number.pipe';
+
+@NgModule({
+ imports: [
+ RouterModule,
+ CommonModule,
+ CovalentCommonModule,
+ CovalentDataTableModule,
+ CovalentDialogsModule,
+ CovalentPagingModule,
+ FormsModule,
+ MatAutocompleteModule,
+ MatButtonModule,
+ MatCardModule,
+ MatIconModule,
+ MatInputModule,
+ MatListModule,
+ MatSelectModule,
+ MatOptionModule,
+ MatSnackBarModule,
+ MatToolbarModule,
+ MatTooltipModule,
+ ReactiveFormsModule,
+ TextMaskModule,
+ TranslateModule
+ ],
+ declarations: [
+ LayoutCardOverComponent,
+ LayoutCardOverComponentTagsDirective,
+ FimsTwoColumnLayoutComponent,
+ SelectListComponent,
+ CustomerSelectComponent,
+ EmployeeSelectComponent,
+ AccountSelectComponent,
+ LedgerSelectComponent,
+ ProductSelectComponent,
+ EmployeeAutoCompleteComponent,
+ IdInputComponent,
+ PermissionDirective,
+ DataTableComponent,
+ StateDisplayComponent,
+ CommandDisplayComponent,
+ MinMaxComponent,
+ ValidateOnBlurDirective,
+ FormFinalActionComponent,
+ FormContinueActionComponent,
+ AddressFormComponent,
+ PortraitComponent,
+ NumberInputComponent,
+ DateInputComponent,
+ TextInputComponent,
+ ImageComponent,
+ FimsFabButtonComponent,
+ DisplayFimsDate,
+ DisplayFimsNumber,
+ DisplayFimsFinancialNumber
+ ],
+ exports: [
+ LayoutCardOverComponent,
+ LayoutCardOverComponentTagsDirective,
+ FimsTwoColumnLayoutComponent,
+ SelectListComponent,
+ CustomerSelectComponent,
+ EmployeeSelectComponent,
+ AccountSelectComponent,
+ LedgerSelectComponent,
+ ProductSelectComponent,
+ EmployeeAutoCompleteComponent,
+ IdInputComponent,
+ PermissionDirective,
+ DataTableComponent,
+ StateDisplayComponent,
+ CommandDisplayComponent,
+ MinMaxComponent,
+ ValidateOnBlurDirective,
+ FormFinalActionComponent,
+ FormContinueActionComponent,
+ AddressFormComponent,
+ PortraitComponent,
+ NumberInputComponent,
+ DateInputComponent,
+ TextInputComponent,
+ ImageComponent,
+ FimsFabButtonComponent,
+ DisplayFimsDate,
+ DisplayFimsNumber,
+ DisplayFimsFinancialNumber
+ ],
+ entryComponents: [
+ ImageComponent
+ ]
+})
+export class FimsSharedModule {}
diff --git a/src/app/common/customer-select/customer-select.component.html b/src/app/common/customer-select/customer-select.component.html
new file mode 100644
index 0000000..f037af8
--- /dev/null
+++ b/src/app/common/customer-select/customer-select.component.html
@@ -0,0 +1,31 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<div layout="row">
+ <mat-form-field layout-margin flex>
+ <input matInput [placeholder]="title" [matAutocomplete]="auto" [formControl]="formControl">
+ <mat-hint class="tc-red-600">
+ <ng-content></ng-content>
+ </mat-hint>
+ </mat-form-field>
+</div>
+
+<mat-autocomplete #auto="matAutocomplete">
+ <mat-option *ngFor="let customer of customers | async" [value]="customer.identifier">
+ {{ customer.identifier }}
+ </mat-option>
+</mat-autocomplete>
diff --git a/src/app/common/customer-select/customer-select.component.ts b/src/app/common/customer-select/customer-select.component.ts
new file mode 100644
index 0000000..f27fcad
--- /dev/null
+++ b/src/app/common/customer-select/customer-select.component.ts
@@ -0,0 +1,94 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, forwardRef, Input, OnInit} from '@angular/core';
+import {FetchRequest} from '../../services/domain/paging/fetch-request.model';
+import {Observable} from 'rxjs/Observable';
+import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';
+import {Customer} from '../../services/customer/domain/customer.model';
+import {CustomerService} from '../../services/customer/customer.service';
+import {CustomerPage} from '../../services/customer/domain/customer-page.model';
+
+const noop: () => void = () => {
+ // empty method
+};
+
+@Component({
+ providers: [
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CustomerSelectComponent), multi: true }
+ ],
+ selector: 'fims-customer-select',
+ templateUrl: './customer-select.component.html'
+})
+export class CustomerSelectComponent implements ControlValueAccessor, OnInit {
+
+ formControl: FormControl;
+
+ @Input() title: string;
+
+ @Input() required: boolean;
+
+ customers: Observable<Customer[]>;
+
+ private _onTouchedCallback: () => void = noop;
+
+ private _onChangeCallback: (_: any) => void = noop;
+
+ constructor(private customerService: CustomerService) {}
+
+ ngOnInit(): void {
+ this.formControl = new FormControl('');
+
+ this.customers = this.formControl.valueChanges
+ .distinctUntilChanged()
+ .debounceTime(500)
+ .do(name => this.changeValue(name))
+ .filter(name => name)
+ .switchMap(name => this.onSearch(name));
+ }
+
+ changeValue(value: string): void {
+ this._onChangeCallback(value);
+ }
+
+ writeValue(value: any): void {
+ this.formControl.setValue(value);
+ }
+
+ registerOnChange(fn: any): void {
+ this._onChangeCallback = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ this._onTouchedCallback = fn;
+ }
+
+ onSearch(searchTerm?: string): Observable<Customer[]> {
+ const fetchRequest: FetchRequest = {
+ page: {
+ pageIndex: 0,
+ size: 5
+ },
+ searchTerm
+ };
+
+ return this.customerService.fetchCustomers(fetchRequest)
+ .map((customerPage: CustomerPage) => customerPage.customers);
+ }
+
+}
diff --git a/src/app/common/data-table/data-table.component.ts b/src/app/common/data-table/data-table.component.ts
index ded0277..1d3f8f9 100644
--- a/src/app/common/data-table/data-table.component.ts
+++ b/src/app/common/data-table/data-table.component.ts
@@ -17,8 +17,8 @@
* under the License.
*/
import {Component, EventEmitter, Input, Output} from '@angular/core';
-import {Sort} from '../../sevices/domain/paging/sort.model';
-import {Page} from '../../sevices/domain/paging/page.model';
+import {Sort} from '../../services/domain/paging/sort.model';
+import {Page} from '../../services/domain/paging/page.model';
import {IPageChangeEvent, ITdDataTableColumn, ITdDataTableSortChangeEvent, TdDataTableSortingOrder} from '@covalent/core';
import {TranslateService} from '@ngx-translate/core';
diff --git a/src/app/common/date-input/date-input.component.html b/src/app/common/date-input/date-input.component.html
new file mode 100644
index 0000000..4aad02d
--- /dev/null
+++ b/src/app/common/date-input/date-input.component.html
@@ -0,0 +1,23 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<mat-form-field layout-margin [formGroup]="form">
+ <input matInput type="date" [id]="controlName" [placeholder]="placeholder" [formControlName]="controlName" [title]="title"/>
+ <mat-error *ngIf="hasRequiredError" translate>Required</mat-error>
+ <mat-error *ngIf="hasBeforeTodayError" translate>Date must be before today</mat-error>
+ <mat-error *ngIf="hasAfterTodayError" translate>Date must be after today</mat-error>
+</mat-form-field>
diff --git a/src/app/common/date-input/date-input.component.ts b/src/app/common/date-input/date-input.component.ts
new file mode 100644
index 0000000..41e3efa
--- /dev/null
+++ b/src/app/common/date-input/date-input.component.ts
@@ -0,0 +1,51 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input} from '@angular/core';
+import {FormGroup} from '@angular/forms';
+
+@Component({
+ selector: 'fims-date-input',
+ templateUrl: './date-input.component.html'
+})
+export class DateInputComponent {
+
+ @Input() placeholder;
+
+ @Input() controlName: string;
+
+ @Input() form: FormGroup;
+
+ @Input() title = '';
+
+ get hasRequiredError(): boolean {
+ return this.hasError('required');
+ }
+
+ get hasBeforeTodayError(): boolean {
+ return this.hasError('beforeToday');
+ }
+
+ get hasAfterTodayError(): boolean {
+ return this.hasError('afterToday');
+ }
+
+ hasError(key: string): boolean {
+ return this.form.get(this.controlName).hasError(key);
+ }
+}
diff --git a/src/app/common/date/fims-date.pipe.spec.ts b/src/app/common/date/fims-date.pipe.spec.ts
new file mode 100644
index 0000000..9d4cd69
--- /dev/null
+++ b/src/app/common/date/fims-date.pipe.spec.ts
@@ -0,0 +1,46 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {DisplayFimsDate} from './fims-date.pipe';
+import {FimsDate} from '../../services/domain/date.converter';
+
+describe('DisplayFimsDate', () => {
+ it('should show short date by default', () => {
+ const pipe = new DisplayFimsDate('en-US');
+
+ const fimsDate: FimsDate = {
+ day: 1,
+ month: 1,
+ year: 2017
+ };
+
+ expect(pipe.transform(fimsDate)).toBe('1/1/2017');
+ });
+
+ it('should not add tz offset', () => {
+ const pipe = new DisplayFimsDate('en-US');
+
+ const fimsDate: FimsDate = {
+ day: 1,
+ month: 1,
+ year: 2017
+ };
+
+ expect(pipe.transform(fimsDate, 'shortTime')).toBe('12:00 AM');
+ });
+});
diff --git a/src/app/common/date/fims-date.pipe.ts b/src/app/common/date/fims-date.pipe.ts
new file mode 100644
index 0000000..f688687
--- /dev/null
+++ b/src/app/common/date/fims-date.pipe.ts
@@ -0,0 +1,36 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Inject, LOCALE_ID, Pipe, PipeTransform} from '@angular/core';
+import {FimsDate, toISOString} from '../../services/domain/date.converter';
+import {DatePipe} from '@angular/common';
+
+@Pipe({
+ name: 'displayFimsDate',
+ pure: true
+})
+export class DisplayFimsDate extends DatePipe implements PipeTransform {
+
+ constructor(@Inject(LOCALE_ID) private locale: string) {
+ super(locale);
+ }
+
+ transform(fimsDate: FimsDate, format = 'shortDate'): string {
+ return super.transform(toISOString(fimsDate), format);
+ }
+}
diff --git a/src/app/common/domain/action-option.model.ts b/src/app/common/domain/action-option.model.ts
new file mode 100644
index 0000000..f3af6c6
--- /dev/null
+++ b/src/app/common/domain/action-option.model.ts
@@ -0,0 +1,37 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {WorkflowAction} from '../../services/portfolio/domain/individuallending/workflow-action.model';
+
+export interface ActionOption {
+ type: WorkflowAction;
+ label: string;
+}
+
+export const ActionOptions: ActionOption[] = [
+ { type: 'OPEN', label: 'loan is opened' },
+ { type: 'DENY', label: 'loan is denied' },
+ { type: 'APPROVE', label: 'loan is approved' },
+ { type: 'ACCEPT_PAYMENT', label: 'payment is accepted' },
+ { type: 'DISBURSE', label: 'loan is disbursed' },
+ { type: 'MARK_LATE', label: 'payment is late' },
+ { type: 'APPLY_INTEREST', label: 'interest is applied' },
+ { type: 'WRITE_OFF', label: 'loan is written off' },
+ { type: 'CLOSE', label: 'loan is closed' },
+ { type: 'RECOVER', label: 'loan is recovered' }
+];
diff --git a/src/app/common/domain/alignment.model.ts b/src/app/common/domain/alignment.model.ts
new file mode 100644
index 0000000..15e4cb6
--- /dev/null
+++ b/src/app/common/domain/alignment.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export const alignmentOptions: any[] = [
+ { type: 0, label: 'first'},
+ { type: 1, label: 'second'},
+ { type: 2, label: 'third'},
+ { type: 3, label: 'last'}
+];
diff --git a/src/app/common/domain/months.model.ts b/src/app/common/domain/months.model.ts
new file mode 100644
index 0000000..7a2fa03
--- /dev/null
+++ b/src/app/common/domain/months.model.ts
@@ -0,0 +1,33 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export const monthOptions: any[] = [
+ { type: 0, label: 'January' },
+ { type: 1, label: 'February' },
+ { type: 2, label: 'March' },
+ { type: 3, label: 'April' },
+ { type: 4, label: 'May' },
+ { type: 5, label: 'June' },
+ { type: 6, label: 'July' },
+ { type: 7, label: 'August' },
+ { type: 8, label: 'September' },
+ { type: 9, label: 'October' },
+ { type: 10, label: 'November' },
+ { type: 11, label: 'December' }
+];
diff --git a/src/app/common/domain/temporal.domain.ts b/src/app/common/domain/temporal.domain.ts
new file mode 100644
index 0000000..04f70e9
--- /dev/null
+++ b/src/app/common/domain/temporal.domain.ts
@@ -0,0 +1,30 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ChronoUnit} from '../../services/portfolio/domain/chrono-unit.model';
+
+export interface TemporalOption {
+ label: string;
+ type: ChronoUnit;
+}
+
+export const temporalOptionList: TemporalOption[] = [
+ { type: 'WEEKS', label: 'weeks'},
+ { type: 'MONTHS', label: 'months'},
+ { type: 'YEARS', label: 'years'}
+];
diff --git a/src/app/common/domain/week-days.model.ts b/src/app/common/domain/week-days.model.ts
new file mode 100644
index 0000000..ccf71b7
--- /dev/null
+++ b/src/app/common/domain/week-days.model.ts
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export const weekDayOptions: any[] = [
+ { type: 0, label: 'Monday' },
+ { type: 1, label: 'Tuesday' },
+ { type: 2, label: 'Wednesday' },
+ { type: 3, label: 'Thursday' },
+ { type: 4, label: 'Friday' },
+ { type: 5, label: 'Saturday' },
+ { type: 6, label: 'Sunday' }
+];
diff --git a/src/app/common/employee-autocomplete/employee-auto-complete.component.html b/src/app/common/employee-autocomplete/employee-auto-complete.component.html
new file mode 100644
index 0000000..f9dd0fd
--- /dev/null
+++ b/src/app/common/employee-autocomplete/employee-auto-complete.component.html
@@ -0,0 +1,31 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<div layout="row">
+ <mat-form-field layout-margin flex>
+ <input matInput [placeholder]="title" [matAutocomplete]="auto" [formControl]="formControl">
+ <mat-hint class="tc-red-600">
+ <ng-content></ng-content>
+ </mat-hint>
+ </mat-form-field>
+</div>
+
+<mat-autocomplete #auto="matAutocomplete">
+ <mat-option *ngFor="let employee of employees | async" [value]="employee.identifier">
+ {{ employee.surname }}, {{ employee.givenName }}
+ </mat-option>
+</mat-autocomplete>
diff --git a/src/app/common/employee-autocomplete/employee-auto-complete.component.ts b/src/app/common/employee-autocomplete/employee-auto-complete.component.ts
new file mode 100644
index 0000000..866ae0a
--- /dev/null
+++ b/src/app/common/employee-autocomplete/employee-auto-complete.component.ts
@@ -0,0 +1,94 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, forwardRef, Input, OnInit} from '@angular/core';
+import {FetchRequest} from '../../services/domain/paging/fetch-request.model';
+import {Observable} from 'rxjs/Observable';
+import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';
+import {Employee} from '../../services/office/domain/employee.model';
+import {OfficeService} from '../../services/office/office.service';
+import {EmployeePage} from '../../services/office/domain/employee-page.model';
+
+const noop: () => void = () => {
+ // empty method
+};
+
+@Component({
+ providers: [
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => EmployeeAutoCompleteComponent), multi: true }
+ ],
+ selector: 'fims-employee-auto-complete',
+ templateUrl: './employee-auto-complete.component.html'
+})
+export class EmployeeAutoCompleteComponent implements ControlValueAccessor, OnInit {
+
+ private _onTouchedCallback: () => void = noop;
+
+ private _onChangeCallback: (_: any) => void = noop;
+
+ formControl: FormControl;
+
+ @Input() title: string;
+
+ @Input() required: boolean;
+
+ employees: Observable<Employee[]>;
+
+ constructor(private officeService: OfficeService) {}
+
+ ngOnInit(): void {
+ this.formControl = new FormControl('');
+
+ this.employees = this.formControl.valueChanges
+ .distinctUntilChanged()
+ .debounceTime(500)
+ .do(name => this.changeValue(name))
+ .filter(name => name)
+ .switchMap(name => this.onSearch(name));
+ }
+
+ changeValue(value: string): void {
+ this._onChangeCallback(value);
+ }
+
+ writeValue(value: any): void {
+ this.formControl.setValue(value);
+ }
+
+ registerOnChange(fn: any): void {
+ this._onChangeCallback = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ this._onTouchedCallback = fn;
+ }
+
+ onSearch(searchTerm?: string): Observable<Employee[]> {
+ const fetchRequest: FetchRequest = {
+ page: {
+ pageIndex: 0,
+ size: 5
+ },
+ searchTerm
+ };
+
+ return this.officeService.listEmployees(fetchRequest)
+ .map((employeePage: EmployeePage) => employeePage.employees);
+ }
+
+}
diff --git a/src/app/common/employee-select/employee-select.component.html b/src/app/common/employee-select/employee-select.component.html
new file mode 100644
index 0000000..c887626
--- /dev/null
+++ b/src/app/common/employee-select/employee-select.component.html
@@ -0,0 +1,30 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-select-list flex
+ [data]="employees"
+ id="identifier"
+ displayName="surname"
+ listIcon="face"
+ [preSelection]="preSelection"
+ [multiple]="multiple"
+ (onSearch)="onSearch($event)"
+ (onSelectionChange)="selectionChange($event)"
+ [title]="title"
+ noResultsMessage="{{'No employee was found.' | translate}}"
+ noSelectionMessage="{{'No selection' | translate}}"
+></fims-select-list>
diff --git a/src/app/common/employee-select/employee-select.component.ts b/src/app/common/employee-select/employee-select.component.ts
new file mode 100644
index 0000000..21637de
--- /dev/null
+++ b/src/app/common/employee-select/employee-select.component.ts
@@ -0,0 +1,63 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
+import {FetchRequest} from '../../services/domain/paging/fetch-request.model';
+import {Observable} from 'rxjs/Observable';
+import {Employee} from '../../services/office/domain/employee.model';
+import {OfficeService} from '../../services/office/office.service';
+import {EmployeePage} from '../../services/office/domain/employee-page.model';
+
+@Component({
+ selector: 'fims-employee-select',
+ templateUrl: './employee-select.component.html'
+})
+export class EmployeeSelectComponent implements OnInit {
+
+ @Input() title: string;
+
+ @Input() preSelection: string[];
+
+ @Input() multiple = true;
+
+ @Output() onSelectionChange: EventEmitter<any> = new EventEmitter();
+
+ employees: Observable<Employee[]>;
+
+ constructor(private officeService: OfficeService) {}
+
+ ngOnInit(): void {
+ this.onSearch();
+ }
+
+ onSearch(searchTerm?: string): void {
+ const fetchRequest: FetchRequest = {
+ page: {
+ pageIndex: 0,
+ size: 5
+ },
+ searchTerm
+ };
+ this.employees = this.officeService.listEmployees(fetchRequest).map((employeePage: EmployeePage) => employeePage.employees);
+ }
+
+ selectionChange(selections: string[]): void {
+ this.onSelectionChange.emit(selections);
+ }
+
+}
diff --git a/src/app/common/fab-button/fab-button.component.html b/src/app/common/fab-button/fab-button.component.html
new file mode 100644
index 0000000..5b34b26
--- /dev/null
+++ b/src/app/common/fab-button/fab-button.component.html
@@ -0,0 +1,20 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<a mat-fab color="accent" class="fims-fab-button" title="{{title}}" [routerLink]="link" [disabled]="disabled" *hasPermission="permission">
+ <mat-icon>{{icon}}</mat-icon>
+</a>
diff --git a/src/app/common/fab-button/fab-button.component.scss b/src/app/common/fab-button/fab-button.component.scss
new file mode 100644
index 0000000..445ad52
--- /dev/null
+++ b/src/app/common/fab-button/fab-button.component.scss
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+.fims-fab-button {
+ top: auto;
+ right: 20px;
+ left: auto;
+ bottom: 20px;
+ position: fixed;
+}
diff --git a/src/app/common/fab-button/fab-button.component.ts b/src/app/common/fab-button/fab-button.component.ts
new file mode 100644
index 0000000..be1829b
--- /dev/null
+++ b/src/app/common/fab-button/fab-button.component.ts
@@ -0,0 +1,45 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input, OnInit} from '@angular/core';
+import {FimsPermission} from '../../services/security/authz/fims-permission.model';
+
+@Component({
+ selector: 'fims-fab-button',
+ templateUrl: './fab-button.component.html',
+ styleUrls: ['./fab-button.component.scss']
+})
+
+export class FimsFabButtonComponent implements OnInit {
+
+ @Input() title: string;
+
+ @Input() icon: string;
+
+ @Input() link: any[];
+
+ @Input() permission: FimsPermission;
+
+ @Input() disabled = false;
+
+ constructor() {
+ }
+
+ ngOnInit() {
+ }
+}
diff --git a/src/app/common/forms/form-continue-action.component.html b/src/app/common/forms/form-continue-action.component.html
new file mode 100644
index 0000000..53af30f
--- /dev/null
+++ b/src/app/common/forms/form-continue-action.component.html
@@ -0,0 +1,18 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<button mat-raised-button color="accent" (click)="continueClick()">{{'CONTINUE' | translate}}</button>
diff --git a/src/app/common/forms/form-continue-action.component.ts b/src/app/common/forms/form-continue-action.component.ts
new file mode 100644
index 0000000..8cdd0e7
--- /dev/null
+++ b/src/app/common/forms/form-continue-action.component.ts
@@ -0,0 +1,33 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Output} from '@angular/core';
+
+@Component({
+ selector: 'fims-form-continue-action',
+ templateUrl: './form-continue-action.component.html'
+})
+export class FormContinueActionComponent {
+
+ @Output() onContinue = new EventEmitter<any>();
+
+ continueClick() {
+ this.onContinue.emit();
+ }
+
+}
diff --git a/src/app/common/forms/form-final-action.component.html b/src/app/common/forms/form-final-action.component.html
new file mode 100644
index 0000000..7af6379
--- /dev/null
+++ b/src/app/common/forms/form-final-action.component.html
@@ -0,0 +1,21 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<button mat-raised-button color="primary" *ngIf="!editMode" (click)="save()" [disabled]="disabled">{{'CREATE ' + resourceName | translate}}</button>
+<button mat-raised-button color="primary" *ngIf="editMode" (click)="save()" [disabled]="disabled">{{'UPDATE ' + resourceName | translate}}</button>
+<span flex></span>
+<button mat-button (click)="cancel()">{{'CANCEL' | translate}}</button>
diff --git a/src/app/common/forms/form-final-action.component.ts b/src/app/common/forms/form-final-action.component.ts
new file mode 100644
index 0000000..db7f997
--- /dev/null
+++ b/src/app/common/forms/form-final-action.component.ts
@@ -0,0 +1,45 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, Output} from '@angular/core';
+
+@Component({
+ selector: 'fims-form-final-action',
+ templateUrl: './form-final-action.component.html'
+})
+export class FormFinalActionComponent {
+
+ @Input() resourceName: string;
+
+ @Input() editMode: boolean;
+
+ @Input() disabled: boolean;
+
+ @Output() onSave = new EventEmitter<any>();
+
+ @Output() onCancel = new EventEmitter<any>();
+
+ save() {
+ this.onSave.emit();
+ }
+
+ cancel() {
+ this.onCancel.emit();
+ }
+
+}
diff --git a/src/app/common/forms/form-helper.ts b/src/app/common/forms/form-helper.ts
new file mode 100644
index 0000000..05f2787
--- /dev/null
+++ b/src/app/common/forms/form-helper.ts
@@ -0,0 +1,32 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {AbstractControl} from '@angular/forms';
+
+export function setSelections(key: string, control: AbstractControl, selections: string[]): void {
+ const keyControl: AbstractControl = control.get(key);
+ keyControl.setValue(selections && selections.length > 0 ? selections[0] : undefined);
+ keyControl.markAsDirty();
+}
+
+export function setSelection(key: string, control: AbstractControl, selection: string): void {
+ const keyControl: AbstractControl = control.get(key);
+ keyControl.setValue(selection);
+ keyControl.markAsDirty();
+}
+
diff --git a/src/app/common/forms/form.component.ts b/src/app/common/forms/form.component.ts
new file mode 100644
index 0000000..70aa97e
--- /dev/null
+++ b/src/app/common/forms/form.component.ts
@@ -0,0 +1,73 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {AbstractControl, FormGroup} from '@angular/forms';
+
+export abstract class FormComponent<T> {
+
+ form: FormGroup;
+
+ constructor() {
+ this.form = new FormGroup({});
+ }
+
+ abstract get formData(): T
+
+ abstract set formData(data: T)
+
+ get pristine(): boolean {
+ if (!this.form) {
+ return true;
+ }
+ return this.form.pristine;
+ }
+
+ get valid(): boolean {
+ if (!this.form) {
+ return true;
+ }
+ return this.form.valid;
+ }
+
+ get dirty(): boolean {
+ if (!this.form) {
+ return true;
+ }
+ return this.form.dirty;
+ }
+
+ /**
+ * Checks if form is pristine before doing valid check
+ * @returns {boolean}
+ */
+ get validWhenOptional(): boolean {
+ if (!this.pristine && this.valid) {
+ return true;
+ }else if (!this.pristine && !this.valid) {
+ return false;
+ }
+ return true;
+ }
+
+ setError(field: string, error: string, value: any): void {
+ const control: AbstractControl = this.form.get(field);
+ const errors = control.errors || {};
+ errors[error] = value;
+ control.setErrors(errors);
+ }
+}
diff --git a/src/app/common/guards/exists-guard.spec.ts b/src/app/common/guards/exists-guard.spec.ts
new file mode 100644
index 0000000..a6a6a76
--- /dev/null
+++ b/src/app/common/guards/exists-guard.spec.ts
@@ -0,0 +1,83 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Router} from '@angular/router';
+import {ExistsGuardService} from './exists-guard';
+import {fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
+import {Observable} from 'rxjs/Observable';
+
+describe('Test Exists Guard Service', () => {
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ ExistsGuardService,
+ {
+ provide: Router,
+ useValue: jasmine.createSpyObj('router', ['navigate'])
+ },
+ {
+ provide: 'cacheExpiry',
+ useValue: 5000
+ }
+ ]
+ });
+ });
+
+ it('should return false when not within expiry', fakeAsync(() => {
+ inject([ExistsGuardService], (existsGuardService: ExistsGuardService) => {
+
+ let result = true;
+
+ existsGuardService.isWithinExpiry(Observable.of(Date.now() - 6000)).subscribe(isExpired => result = isExpired);
+
+ tick();
+
+ expect(result).toBeFalsy();
+ })();
+ }));
+
+ it('should return true when within expiry', fakeAsync(() => {
+ inject([ExistsGuardService], (existsGuardService: ExistsGuardService) => {
+
+ let result = false;
+
+ existsGuardService.isWithinExpiry(Observable.of(Date.now() - 1000)).subscribe(isExpired => result = isExpired);
+
+ tick();
+
+ expect(result).toBeTruthy();
+ })();
+ }));
+
+ it('should route to /404 on exception and return false', fakeAsync(() => {
+ inject([ExistsGuardService, Router], (existsGuardService: ExistsGuardService, router: Router) => {
+
+ let result = true;
+
+ existsGuardService.routeTo404OnError(Observable.throw({})).subscribe(canActivate => result = canActivate);
+
+ tick();
+
+ expect(router.navigate).toHaveBeenCalledWith(['/404']);
+
+ expect(result).toBeFalsy();
+ })();
+ }));
+
+});
diff --git a/src/app/common/guards/exists-guard.ts b/src/app/common/guards/exists-guard.ts
new file mode 100644
index 0000000..19c29ab
--- /dev/null
+++ b/src/app/common/guards/exists-guard.ts
@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Observable} from 'rxjs/Observable';
+import {of} from 'rxjs/observable/of';
+import {Router} from '@angular/router';
+import {Inject, Injectable} from '@angular/core';
+
+@Injectable()
+export class ExistsGuardService {
+
+ constructor(private router: Router, @Inject('cacheExpiry') private cacheExpiry: number) {}
+
+ isWithinExpiry(observable: Observable<number>): Observable<boolean> {
+ return observable
+ .map(loadedAtTimestamp => {
+ if (!loadedAtTimestamp) {
+ return false;
+ }
+ return loadedAtTimestamp + this.cacheExpiry > Date.now();
+ })
+ .take(1);
+ }
+
+ routeTo404OnError(observable: Observable<any>): Observable<any> {
+ return observable.catch(() => {
+ this.router.navigate(['/404']);
+ return of(false);
+ });
+ }
+
+}
diff --git a/src/app/common/id-input/id-input.component.html b/src/app/common/id-input/id-input.component.html
new file mode 100644
index 0000000..45c5b9c
--- /dev/null
+++ b/src/app/common/id-input/id-input.component.html
@@ -0,0 +1,37 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<mat-form-field [formGroup]="form" flex layout-margin>
+ <input matInput [id]="controlName" placeholder="{{placeholder | translate}}" [formControlName]="controlName"
+ [readonly]="readonly"
+ tdAutoTrim/>
+ <mat-error *ngIf="form.controls[controlName].hasError('required')" translate>
+ Unique, Required.
+ </mat-error>
+ <mat-error *ngIf="form.controls[controlName].hasError('minlength')">
+ {{ 'Must have at least characters.' | translate:{ value: form.controls[controlName].getError('minlength')['requiredLength']} }}
+ </mat-error>
+ <mat-error *ngIf="form.controls[controlName].hasError('maxlength')">
+ {{ 'Only characters allowed.' | translate:{ value: form.controls[controlName].getError('maxlength')['requiredLength']} }}
+ </mat-error>
+ <mat-error *ngIf="form.controls[controlName].hasError('unique')" translate>
+ Please enter a different identifier.
+ </mat-error>
+ <mat-error *ngIf="form.controls[controlName].hasError('urlSafe')" translate>
+ Only alphabetic, decimal digits, - _ . * characters allowed
+ </mat-error>
+</mat-form-field>
diff --git a/src/app/common/id-input/id-input.component.ts b/src/app/common/id-input/id-input.component.ts
new file mode 100644
index 0000000..9896a9c
--- /dev/null
+++ b/src/app/common/id-input/id-input.component.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, HostBinding, Input, OnInit} from '@angular/core';
+import {FormGroup} from '@angular/forms';
+
+@Component({
+ selector: 'fims-id-input',
+ templateUrl: './id-input.component.html'
+})
+export class IdInputComponent implements OnInit {
+
+ @HostBinding('attr.layout')
+ @Input() layout = 'row';
+
+ @Input() controlName: string;
+
+ @Input() form: FormGroup;
+
+ @Input() readonly: boolean;
+
+ @Input() placeholder = 'Identifier';
+
+ constructor() { }
+
+ ngOnInit() { }
+
+}
diff --git a/src/app/common/image/image.component.ts b/src/app/common/image/image.component.ts
new file mode 100644
index 0000000..08f77da
--- /dev/null
+++ b/src/app/common/image/image.component.ts
@@ -0,0 +1,41 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Inject, OnDestroy} from '@angular/core';
+import {MAT_DIALOG_DATA} from '@angular/material';
+import {DomSanitizer, SafeUrl} from '@angular/platform-browser';
+
+@Component({
+ template: '<img [src]="safeUrl" alt/>'
+})
+export class ImageComponent implements OnDestroy {
+
+ private objectUrl: string;
+
+ constructor(private domSanitizer: DomSanitizer, @Inject(MAT_DIALOG_DATA) public blob: Blob) {
+ this.objectUrl = URL.createObjectURL(blob);
+ }
+
+ ngOnDestroy(): void {
+ URL.revokeObjectURL(this.objectUrl);
+ }
+
+ get safeUrl(): SafeUrl {
+ return this.domSanitizer.bypassSecurityTrustUrl(this.objectUrl);
+ }
+}
diff --git a/src/app/common/layout-card-over/layout-card-over.component.html b/src/app/common/layout-card-over/layout-card-over.component.html
new file mode 100644
index 0000000..b141dc6
--- /dev/null
+++ b/src/app/common/layout-card-over/layout-card-over.component.html
@@ -0,0 +1,32 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<div layout="column" layout-fill>
+ <mat-toolbar>
+ <a mat-icon-button *ngIf="navigateBackTo" [routerLink]="navigateBackTo">
+ <mat-icon>keyboard_arrow_left</mat-icon>
+ </a>
+ <span class="truncate">{{fullTitle}}</span>
+ <span flex></span>
+ <ng-content select="fims-layout-card-over-header-menu"></ng-content>
+ </mat-toolbar>
+ <div class="mat-content">
+ <mat-card>
+ <ng-content></ng-content>
+ </mat-card>
+ </div>
+</div>
diff --git a/src/app/common/layout-card-over/layout-card-over.component.scss b/src/app/common/layout-card-over/layout-card-over.component.scss
new file mode 100644
index 0000000..3fdd197
--- /dev/null
+++ b/src/app/common/layout-card-over/layout-card-over.component.scss
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+:host {
+ position: relative;
+ display: block;
+ width: 100%;
+ min-height: 100%;
+ height: 100%;
+ div[after-card] {
+ display: block;
+ }
+}
+.margin {
+ margin-top: -64px;
+}
+
+.truncate {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
diff --git a/src/app/common/layout-card-over/layout-card-over.component.ts b/src/app/common/layout-card-over/layout-card-over.component.ts
new file mode 100644
index 0000000..5bf0b20
--- /dev/null
+++ b/src/app/common/layout-card-over/layout-card-over.component.ts
@@ -0,0 +1,54 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Directive, EventEmitter, Input, OnInit, Output} from '@angular/core';
+
+// tslint:disable-next-line:directive-selector
+@Directive({selector: 'fims-layout-card-over-header-menu'})
+export class LayoutCardOverComponentTagsDirective { }
+
+@Component({
+ selector: 'fims-layout-card-over',
+ templateUrl: './layout-card-over.component.html',
+ styleUrls: ['./layout-card-over.component.scss']
+})
+export class LayoutCardOverComponent implements OnInit {
+
+ @Input() title: string;
+
+ @Input() subTitle: string;
+
+ @Input() navigateBackTo: string[];
+
+ @Input() searchTerm: string;
+
+ @Input() searchable: boolean;
+
+ @Output() search: EventEmitter<string> = new EventEmitter<string>();
+
+ ngOnInit(): void {}
+
+ onSearch(searchTerm: string): void {
+ this.search.emit(searchTerm);
+ }
+
+ get fullTitle(): string {
+ const titles = [this.title, this.subTitle];
+ return titles.filter(title => !!title).join(' - ');
+ }
+}
diff --git a/src/app/common/layouts/two-column-layout.component.html b/src/app/common/layouts/two-column-layout.component.html
new file mode 100644
index 0000000..257247b
--- /dev/null
+++ b/src/app/common/layouts/two-column-layout.component.html
@@ -0,0 +1,31 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<div class="mat-content inset" flex>
+ <div layout="row">
+ <div layout="column" flex-gt-md="100">
+ <div layout-gt-md="row">
+ <div flex-gt-md="30">
+ <ng-content select="[left]"></ng-content>
+ </div>
+ <div flex-gt-md="70">
+ <ng-content select="[right]"></ng-content>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/src/app/common/layouts/two-column-layout.component.ts b/src/app/common/layouts/two-column-layout.component.ts
new file mode 100644
index 0000000..d034fe8
--- /dev/null
+++ b/src/app/common/layouts/two-column-layout.component.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component} from '@angular/core';
+
+@Component({
+ selector: 'fims-two-column-layout',
+ templateUrl: './two-column-layout.component.html'
+})
+
+export class FimsTwoColumnLayoutComponent {}
diff --git a/src/app/common/ledger-select/ledger-select.component.html b/src/app/common/ledger-select/ledger-select.component.html
new file mode 100644
index 0000000..4637230
--- /dev/null
+++ b/src/app/common/ledger-select/ledger-select.component.html
@@ -0,0 +1,31 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<div layout="row">
+ <mat-form-field layout-margin flex>
+ <input matInput [placeholder]="title" [matAutocomplete]="auto" [formControl]="formControl">
+ <mat-hint class="tc-red-600">
+ <ng-content></ng-content>
+ </mat-hint>
+ </mat-form-field>
+</div>
+
+<mat-autocomplete #auto="matAutocomplete">
+ <mat-option *ngFor="let ledger of ledgers | async" [value]="ledger.identifier" [matTooltip]="ledger.description">
+ {{ ledger.identifier }}({{ledger.name}})
+ </mat-option>
+</mat-autocomplete>
diff --git a/src/app/common/ledger-select/ledger-select.component.ts b/src/app/common/ledger-select/ledger-select.component.ts
new file mode 100644
index 0000000..022970a
--- /dev/null
+++ b/src/app/common/ledger-select/ledger-select.component.ts
@@ -0,0 +1,97 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, forwardRef, Input, OnInit} from '@angular/core';
+import {FetchRequest} from '../../services/domain/paging/fetch-request.model';
+import {Observable} from 'rxjs/Observable';
+import {AccountingService} from '../../services/accounting/accounting.service';
+import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';
+import {Ledger} from '../../services/accounting/domain/ledger.model';
+import {LedgerPage} from '../../services/accounting/domain/ledger-page.model';
+import {AccountType} from '../../services/accounting/domain/account-type.model';
+
+const noop: () => void = () => {
+ // empty method
+};
+
+@Component({
+ providers: [
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => LedgerSelectComponent), multi: true }
+ ],
+ selector: 'fims-ledger-select',
+ templateUrl: './ledger-select.component.html'
+})
+export class LedgerSelectComponent implements ControlValueAccessor, OnInit {
+
+ private _onTouchedCallback: () => void = noop;
+
+ private _onChangeCallback: (_: any) => void = noop;
+
+ formControl: FormControl;
+
+ @Input() title: string;
+
+ @Input() required: boolean;
+
+ @Input() type: AccountType;
+
+ ledgers: Observable<Ledger[]>;
+
+ constructor(private accountingService: AccountingService) {}
+
+ ngOnInit(): void {
+ this.formControl = new FormControl('');
+
+ this.ledgers = this.formControl.valueChanges
+ .distinctUntilChanged()
+ .debounceTime(500)
+ .do(name => this.changeValue(name))
+ .filter(name => name)
+ .switchMap(name => this.onSearch(name));
+ }
+
+ changeValue(value: string): void {
+ this._onChangeCallback(value);
+ }
+
+ writeValue(value: any): void {
+ this.formControl.setValue(value);
+ }
+
+ registerOnChange(fn: any): void {
+ this._onChangeCallback = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ this._onTouchedCallback = fn;
+ }
+
+ onSearch(searchTerm?: string): Observable<Ledger[]> {
+ const fetchRequest: FetchRequest = {
+ page: {
+ pageIndex: 0,
+ size: 5
+ },
+ searchTerm
+ };
+
+ return this.accountingService.fetchLedgers(true, fetchRequest, this.type)
+ .map((ledgerPage: LedgerPage) => ledgerPage.ledgers);
+ }
+
+}
diff --git a/src/app/common/min-max/min-max.component.html b/src/app/common/min-max/min-max.component.html
new file mode 100644
index 0000000..70db6cb
--- /dev/null
+++ b/src/app/common/min-max/min-max.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<div layout="row">
+ <fims-number-input [form]="form" [controlName]="minControlName" [placeholder]="minPlaceholder" [requireDecimal]="requireDecimal" [decimalLimit]="decimalLimit"></fims-number-input>
+</div>
+<div layout="column">
+ <fims-number-input [form]="form" [controlName]="maxControlName" [placeholder]="maxPlaceholder" [requireDecimal]="requireDecimal" [decimalLimit]="decimalLimit"></fims-number-input>
+ <mat-error *ngIf="form.touched && form.hasError('greaterThan')" layout-margin translate>Must be greater than minimum</mat-error>
+ <mat-error *ngIf="form.touched && form.hasError('greaterThanEquals')" layout-margin translate>Must be greater than or equal minimum</mat-error>
+</div>
diff --git a/src/app/common/min-max/min-max.component.ts b/src/app/common/min-max/min-max.component.ts
new file mode 100644
index 0000000..0ac3786
--- /dev/null
+++ b/src/app/common/min-max/min-max.component.ts
@@ -0,0 +1,41 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input} from '@angular/core';
+import {FormGroup} from '@angular/forms';
+
+@Component({
+ selector: 'fims-min-max',
+ templateUrl: './min-max.component.html'
+})
+export class MinMaxComponent {
+
+ @Input() minPlaceholder;
+
+ @Input() maxPlaceholder;
+
+ @Input() minControlName: string;
+
+ @Input() maxControlName: string;
+
+ @Input() form: FormGroup;
+
+ @Input() requireDecimal;
+
+ @Input() decimalLimit;
+}
diff --git a/src/app/common/number-input/number-input.component.html b/src/app/common/number-input/number-input.component.html
new file mode 100644
index 0000000..1b09dae
--- /dev/null
+++ b/src/app/common/number-input/number-input.component.html
@@ -0,0 +1,38 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<mat-form-field layout-margin flex [formGroup]="form">
+ <input matInput type="text" [textMask]="{mask: mask}" [placeholder]="placeholder" [formControlName]="controlName"/>
+ <mat-error *ngIf="hasRequiredError" translate>
+ Required
+ </mat-error>
+ <mat-error *ngIf="hasMinValueError" translate>
+ {{ 'Value must be greater than or equal to' | translate:{value: form.get(controlName).errors.minValue.value} }}
+ </mat-error>
+ <mat-error *ngIf="hasMaxValueError" translate>
+ {{ 'Value must be smaller than or equal to' | translate:{value: form.get(controlName).errors.maxValue.value} }}
+ </mat-error>
+ <mat-error *ngIf="hasGreaterThanValueError" translate>
+ {{ 'Value must be greater than' | translate:{value: form.get(controlName).errors.greaterThanValue.value} }}
+ </mat-error>
+ <mat-error *ngIf="hasScaleError">
+ {{ 'Must have decimal places' | translate:{value: form.get(controlName).errors.scale.value} }}
+ </mat-error>
+ <mat-hint align="end" *ngIf="form.get(controlName).enabled">
+ {{hint}}
+ </mat-hint>
+</mat-form-field>
diff --git a/src/app/common/number-input/number-input.component.ts b/src/app/common/number-input/number-input.component.ts
new file mode 100644
index 0000000..2f1af4c
--- /dev/null
+++ b/src/app/common/number-input/number-input.component.ts
@@ -0,0 +1,78 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input} from '@angular/core';
+import {FormGroup} from '@angular/forms';
+import {createNumberMask} from 'text-mask-addons/dist/textMaskAddons';
+
+@Component({
+ selector: 'fims-number-input',
+ templateUrl: './number-input.component.html'
+})
+export class NumberInputComponent {
+
+ @Input() placeholder;
+
+ @Input() controlName: string;
+
+ @Input() form: FormGroup;
+
+ @Input() requireDecimal = true;
+
+ @Input() decimalLimit = 2;
+
+ @Input() hint: string;
+
+ mask: any;
+
+ constructor() {
+ this.mask = createNumberMask({
+ prefix: '',
+ suffix: '',
+ includeThousandsSeparator: false,
+ requireDecimal: this.requireDecimal,
+ allowNegative: false,
+ allowLeadingZeroes: true,
+ decimalLimit: this.decimalLimit
+ });
+ }
+
+ get hasRequiredError(): boolean {
+ return this.hasError('required');
+ }
+
+ get hasMinValueError(): boolean {
+ return this.hasError('minValue');
+ }
+
+ get hasGreaterThanValueError(): boolean {
+ return this.hasError('greaterThanValue');
+ }
+
+ get hasMaxValueError(): boolean {
+ return this.hasError('maxValue');
+ }
+
+ get hasScaleError(): boolean {
+ return this.hasError('maxScale');
+ }
+
+ hasError(key: string): boolean {
+ return this.form.get(this.controlName).hasError(key);
+ }
+}
diff --git a/src/app/common/number/fims-financial-number.pipe.ts b/src/app/common/number/fims-financial-number.pipe.ts
new file mode 100644
index 0000000..f303bdf
--- /dev/null
+++ b/src/app/common/number/fims-financial-number.pipe.ts
@@ -0,0 +1,41 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Inject, LOCALE_ID, Pipe, PipeTransform} from '@angular/core';
+import {DecimalPipe} from '@angular/common';
+
+@Pipe({
+ name: 'displayFimsFinancialNumber',
+ pure: true
+})
+export class DisplayFimsFinancialNumber extends DecimalPipe implements PipeTransform {
+
+ constructor(@Inject(LOCALE_ID) private locale: string) {
+ super(locale);
+ }
+
+ transform(value: any, digits: string = '1.2-2'): string {
+ const transformed = super.transform(value, digits);
+
+ if (transformed.indexOf('-') === 0) {
+ return `(${transformed.substr(1, transformed.length)})`;
+ }
+
+ return transformed;
+ }
+}
diff --git a/src/app/common/number/fims-number.pipe.ts b/src/app/common/number/fims-number.pipe.ts
new file mode 100644
index 0000000..b8506a6
--- /dev/null
+++ b/src/app/common/number/fims-number.pipe.ts
@@ -0,0 +1,35 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Inject, LOCALE_ID, Pipe, PipeTransform} from '@angular/core';
+import {DecimalPipe} from '@angular/common';
+
+@Pipe({
+ name: 'displayFimsNumber',
+ pure: true
+})
+export class DisplayFimsNumber extends DecimalPipe implements PipeTransform {
+
+ constructor(@Inject(LOCALE_ID) private locale: string) {
+ super(locale);
+ }
+
+ transform(value: any, digits: string = '1.2-2'): string {
+ return super.transform(value, digits);
+ }
+}
diff --git a/src/app/common/portrait/portrait.component.html b/src/app/common/portrait/portrait.component.html
new file mode 100644
index 0000000..2f11d2d
--- /dev/null
+++ b/src/app/common/portrait/portrait.component.html
@@ -0,0 +1,20 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<a href="#" (click)="clickPortrait($event)" [matTooltip]="tooltip">
+ <img [ngClass]="{'small': size === 'small', 'medium': size === 'medium', 'large': size === 'large'}" [src]="safeUrl" alt>
+</a>
diff --git a/src/app/common/portrait/portrait.component.scss b/src/app/common/portrait/portrait.component.scss
new file mode 100644
index 0000000..d23c80a
--- /dev/null
+++ b/src/app/common/portrait/portrait.component.scss
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+.small {
+ font-size: 40px;
+ line-height: 40px;
+ height: 90px;
+ width: 90px;
+ margin: 16px 0 0 15px;
+ border-radius: 50%;
+}
+
+.medium {
+ font-size: 40px;
+ line-height: 40px;
+ height: 190px;
+ width: 190px;
+ margin: 16px 0 0 15px;
+ border-radius: 50%;
+}
+
+.large {
+ font-size: 40px;
+ line-height: 40px;
+ height: 290px;
+ width: 290px;
+ margin: 16px 0 0 15px;
+ border-radius: 50%;
+}
diff --git a/src/app/common/portrait/portrait.component.ts b/src/app/common/portrait/portrait.component.ts
new file mode 100644
index 0000000..419134c
--- /dev/null
+++ b/src/app/common/portrait/portrait.component.ts
@@ -0,0 +1,66 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnDestroy, Output} from '@angular/core';
+import {DomSanitizer, SafeUrl} from '@angular/platform-browser';
+
+export type PORTRAIT_SIZE = 'small' | 'medium' | 'large';
+
+@Component({
+ selector: 'fims-portrait',
+ templateUrl: './portrait.component.html',
+ styleUrls: ['./portrait.component.scss']
+})
+export class PortraitComponent implements OnDestroy {
+
+ private defaultUrl = './assets/images/ic_account_circle_black_48dp_2x.png';
+
+ private objectUrl: string;
+
+ @Input() set blob(blob: Blob) {
+ if (blob) {
+ this.objectUrl = URL.createObjectURL(blob);
+ } else {
+ this.objectUrl = this.defaultUrl;
+ }
+ };
+
+ @Input() size: PORTRAIT_SIZE = 'medium';
+
+ @Input() tooltip: string;
+
+ @Output() onClick = new EventEmitter();
+
+ constructor(private domSanitizer: DomSanitizer) {}
+
+ ngOnDestroy(): void {
+ if (this.objectUrl) {
+ URL.revokeObjectURL(this.objectUrl);
+ }
+ }
+
+ get safeUrl(): SafeUrl {
+ return this.objectUrl ? this.domSanitizer.bypassSecurityTrustUrl(this.objectUrl) : undefined;
+ }
+
+ clickPortrait(event: MouseEvent): void {
+ event.preventDefault();
+ this.onClick.emit();
+ }
+
+}
diff --git a/src/app/common/product-select/product-select.component.html b/src/app/common/product-select/product-select.component.html
new file mode 100644
index 0000000..d0ee48c
--- /dev/null
+++ b/src/app/common/product-select/product-select.component.html
@@ -0,0 +1,31 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<div layout="row">
+ <mat-form-field layout-margin flex>
+ <input matInput [placeholder]="title" [matAutocomplete]="auto" [formControl]="formControl">
+ <mat-hint class="tc-red-600">
+ <ng-content></ng-content>
+ </mat-hint>
+ </mat-form-field>
+</div>
+
+<mat-autocomplete #auto="matAutocomplete">
+ <mat-option *ngFor="let product of products | async" [value]="product.identifier">
+ {{ product.identifier }}
+ </mat-option>
+</mat-autocomplete>
diff --git a/src/app/common/product-select/product-select.component.ts b/src/app/common/product-select/product-select.component.ts
new file mode 100644
index 0000000..7723630
--- /dev/null
+++ b/src/app/common/product-select/product-select.component.ts
@@ -0,0 +1,105 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, forwardRef, Input, OnInit, Output} from '@angular/core';
+import {FetchRequest} from '../../services/domain/paging/fetch-request.model';
+import {Observable} from 'rxjs/Observable';
+import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';
+import {PortfolioService} from '../../services/portfolio/portfolio.service';
+import {ProductPage} from '../../services/portfolio/domain/product-page.model';
+import {Product} from '../../services/portfolio/domain/product.model';
+
+const noop: () => void = () => {
+ // empty method
+};
+
+@Component({
+ providers: [
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ProductSelectComponent), multi: true }
+ ],
+ selector: 'fims-product-select',
+ templateUrl: './product-select.component.html'
+})
+export class ProductSelectComponent implements ControlValueAccessor, OnInit {
+
+ private _onTouchedCallback: () => void = noop;
+
+ private _onChangeCallback: (_: any) => void = noop;
+
+ formControl: FormControl;
+
+ @Input() title: string;
+
+ @Input() required: boolean;
+
+ @Output() onProductSelected: EventEmitter<Product> = new EventEmitter<Product>();
+
+ products: Observable<Product[]>;
+
+ constructor(private portfolioService: PortfolioService) {}
+
+ ngOnInit(): void {
+ this.formControl = new FormControl('');
+
+ this.products = this.formControl.valueChanges
+ .distinctUntilChanged()
+ .debounceTime(500)
+ .do(product => {
+ if (this.isObject(product)) {
+ this.onProductSelected.emit(product);
+ }
+ })
+ .map(product => this.isObject(product) ? product.name : product)
+ .do(product => this.changeValue(product))
+ .switchMap(name => this.onSearch(name));
+ }
+
+ private isObject(product: Product): boolean {
+ return product && typeof product === 'object';
+ }
+
+ changeValue(value: string): void {
+ this._onChangeCallback(value);
+ }
+
+ writeValue(value: any): void {
+ this.formControl.setValue(value);
+ }
+
+ registerOnChange(fn: any): void {
+ this._onChangeCallback = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ this._onTouchedCallback = fn;
+ }
+
+ onSearch(searchTerm?: string): Observable<Product[]> {
+ const fetchRequest: FetchRequest = {
+ page: {
+ pageIndex: 0,
+ size: 5
+ },
+ searchTerm
+ };
+
+ return this.portfolioService.findAllProducts(false, fetchRequest)
+ .map((productPage: ProductPage) => productPage.elements);
+ }
+
+}
diff --git a/src/app/common/select-list/select-list.component.html b/src/app/common/select-list/select-list.component.html
new file mode 100644
index 0000000..49a6672
--- /dev/null
+++ b/src/app/common/select-list/select-list.component.html
@@ -0,0 +1,59 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<h4>{{title}}</h4>
+<mat-form-field layout-gt-xs="row" layout-margin>
+ <input matInput flex placeholder="{{'Search...' | translate}}" tdAutoTrim [formControl]="term"/>
+</mat-form-field>
+<mat-card flex>
+ <mat-list flex>
+ <h3 mat-subheader translate>Results</h3>
+ <mat-list-item *ngFor="let row of data | async">
+ <div layout="row" layout-align="start center" style="width: 100%;">
+ <mat-icon matListAvatar>{{listIcon}}</mat-icon>
+ <div flex>
+ {{ row[id] }}
+ </div>
+ <div>
+ <button mat-raised-button color="accent" title="{{'Assign' | translate}}" (click)="doSelect(row[id])">{{'Select' | translate}}</button>
+ </div>
+ </div>
+ </mat-list-item>
+ <mat-list-item *ngIf="(data | async)?.length == 0">
+ {{noResultsMessage}}
+ </mat-list-item>
+ <h3 mat-subheader translate>Current selection</h3>
+ <mat-list-item *ngFor="let selection of selections" class="active">
+ <div layout="row" layout-align="start center" style="width: 100%;">
+ <mat-icon matListAvatar>{{listIcon}}</mat-icon>
+ <div flex>
+ {{selection}}
+ </div>
+ <div>
+ <button mat-raised-button color="warn" title="{{'Unassign' | translate}}" (click)="doDeselect(selection)">{{'Deselect' | translate}}</button>
+ </div>
+ </div>
+ </mat-list-item>
+ <mat-list-item *ngIf="!selections || selections.length === 0">
+ <div layout="row" layout-align="start center" style="width: 100%;">
+ <div flex>
+ {{noSelectionMessage}}
+ </div>
+ </div>
+ </mat-list-item>
+ </mat-list>
+</mat-card>
diff --git a/src/app/common/select-list/select-list.component.ts b/src/app/common/select-list/select-list.component.ts
new file mode 100644
index 0000000..40e294a
--- /dev/null
+++ b/src/app/common/select-list/select-list.component.ts
@@ -0,0 +1,88 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
+import {FormControl} from '@angular/forms';
+import {Observable} from 'rxjs/Observable';
+
+@Component({
+ selector: 'fims-select-list',
+ templateUrl: './select-list.component.html'
+})
+export class SelectListComponent implements OnInit {
+
+ selections: string[];
+
+ term = new FormControl();
+
+ @Input('preSelection') set preSelection(preSelection: string | string[]) {
+ preSelection = preSelection || [];
+ const selections: string[] = Array.isArray(preSelection) ? preSelection : [preSelection];
+ this.selections = selections;
+ };
+
+ @Input('listIcon') listIcon: string;
+
+ @Input('noResultsMessage') noResultsMessage: string;
+
+ @Input('noSelectionMessage') noSelectionMessage: string;
+
+ @Input('data') data: Observable<any[]>;
+
+ @Input('id') id: string;
+
+ @Input('title') title: string;
+
+ @Input('multiple') multiple = false;
+
+ @Output() onSearch = new EventEmitter<string>();
+
+ @Output() onSelectionChange = new EventEmitter<any>();
+
+ constructor() {}
+
+ ngOnInit(): void {
+ this.term.valueChanges
+ .debounceTime(500)
+ .subscribe((event) => this.onSearch.emit(event));
+ }
+
+ doSelect(id: any): void {
+ if (this.selections.indexOf(id) > -1) {
+ return;
+ }
+
+ if (this.multiple) {
+ this.selections.push(id);
+ }else {
+ this.selections = [id];
+ }
+
+ this.onSelectionChange.emit(this.selections);
+ }
+
+ doDeselect(id: any): void {
+ const index = this.selections.indexOf(id);
+
+ if (index > -1) {
+ this.selections.splice(index, 1);
+ }
+
+ this.onSelectionChange.emit(this.selections);
+ }
+}
diff --git a/src/app/common/state-display/state-display.component.html b/src/app/common/state-display/state-display.component.html
new file mode 100644
index 0000000..32f5f13
--- /dev/null
+++ b/src/app/common/state-display/state-display.component.html
@@ -0,0 +1,51 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ng-container [ngSwitch]="state">
+ <mat-list-item *ngSwitchCase="'PENDING'">
+ <mat-icon matListAvatar color="accent">hourglass_empty</mat-icon>
+ <h3 matLine translate>PENDING</h3>
+ </mat-list-item>
+ <mat-list-item *ngSwitchCase="'OPEN'">
+ <mat-icon matListAvatar color="accent">lock_open</mat-icon>
+ <h3 matLine translate>OPEN</h3>
+ </mat-list-item>
+ <mat-list-item *ngSwitchCase="'ACTIVE'">
+ <mat-icon matListAvatar color="primary">check_circle</mat-icon>
+ <h3 matLine translate>ACTIVE</h3>
+ </mat-list-item>
+ <mat-list-item *ngSwitchCase="'APPROVED'">
+ <mat-icon matListAvatar color="primary">done_all</mat-icon>
+ <h3 matLine translate>APPROVED</h3>
+ </mat-list-item>
+ <mat-list-item *ngSwitchCase="'LOCKED'">
+ <mat-icon matListAvatar color="warn">lock</mat-icon>
+ <h3 matLine translate>LOCKED</h3>
+ </mat-list-item>
+ <mat-list-item *ngSwitchCase="'CLOSED'">
+ <mat-icon matListAvatar color="warn">close</mat-icon>
+ <h3 matLine translate>CLOSED</h3>
+ </mat-list-item>
+ <mat-list-item *ngSwitchCase="'CREATED'">
+ <mat-icon matListAvatar color="accent">hourglass_empty</mat-icon>
+ <h3 matLine translate>CREATED</h3>
+ </mat-list-item>
+ <mat-list-item *ngSwitchCase="'PAUSED'">
+ <mat-icon matListAvatar color="accent">hourglass_empty</mat-icon>
+ <h3 matLine translate>PAUSED</h3>
+ </mat-list-item>
+</ng-container>
diff --git a/src/app/common/state-display/state-display.component.ts b/src/app/common/state-display/state-display.component.ts
new file mode 100644
index 0000000..3de751e
--- /dev/null
+++ b/src/app/common/state-display/state-display.component.ts
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input} from '@angular/core';
+
+@Component({
+ selector: 'fims-state-display',
+ templateUrl: 'state-display.component.html'
+})
+export class StateDisplayComponent {
+
+ @Input() state: string;
+}
diff --git a/src/app/common/store/form.reducer.spec.ts b/src/app/common/store/form.reducer.spec.ts
index b7fe33b..37dae56 100644
--- a/src/app/common/store/form.reducer.spec.ts
+++ b/src/app/common/store/form.reducer.spec.ts
@@ -17,7 +17,7 @@
* under the License.
*/
import {createFormReducer, FormState} from './form.reducer';
-import {Error} from '../../sevices/domain/error.model';
+import {Error} from '../../services/domain/error.model';
import {Action} from '@ngrx/store';
class CreateSuccessAction implements Action {
diff --git a/src/app/common/store/form.reducer.ts b/src/app/common/store/form.reducer.ts
index ae01813..750241f 100644
--- a/src/app/common/store/form.reducer.ts
+++ b/src/app/common/store/form.reducer.ts
@@ -17,7 +17,7 @@
* under the License.
*/
import {Action, ActionReducer} from '@ngrx/store';
-import {Error} from '../../sevices/domain/error.model';
+import {Error} from '../../services/domain/error.model';
export interface FormState {
error?: Error;
@@ -34,7 +34,7 @@
case `[${resource}] Create Fail`:
case `[${resource}] Update Fail`: {
return Object.assign({}, state, {
- // error: action.payload
+ error: action.payload
});
}
diff --git a/src/app/common/store/resource.reducer.ts b/src/app/common/store/resource.reducer.ts
index 54c0e84..0e360fa 100644
--- a/src/app/common/store/resource.reducer.ts
+++ b/src/app/common/store/resource.reducer.ts
@@ -63,7 +63,7 @@
const identifier = (resource: any) => resource[identifierName];
-/* return function (state: ResourceState = initialState, action: Action): ResourceState {
+ return function (state: ResourceState = initialState, action: Action): ResourceState {
switch (action.type) {
@@ -152,7 +152,6 @@
}
}
};
- */
};
export const getResourceEntities = (cacheState: ResourceState) => cacheState.entities;
diff --git a/src/app/common/store/search.reducer.ts b/src/app/common/store/search.reducer.ts
index c3439fd..4c397a5 100644
--- a/src/app/common/store/search.reducer.ts
+++ b/src/app/common/store/search.reducer.ts
@@ -17,7 +17,7 @@
* under the License.
*/
import {Action, ActionReducer} from '@ngrx/store';
-import {FetchRequest} from '../../sevices/domain/paging/fetch-request.model';
+import {FetchRequest} from '../../services/domain/paging/fetch-request.model';
import {createSelector} from 'reselect';
export function emptySearchResult(): SearchResult {
@@ -55,7 +55,7 @@
};
export const createSearchReducer = (entityName: string, reducer?: ActionReducer<SearchState>) => {
- /* return function(state: SearchState = initialState, action: Action): SearchState {
+ return function(state: SearchState = initialState, action: Action): SearchState {
switch (action.type) {
@@ -89,7 +89,6 @@
}
}
};
- */
};
export const getSearchEntities = (state: SearchState) => state.entities;
diff --git a/src/app/common/testing/permission-stubs.ts b/src/app/common/testing/permission-stubs.ts
index 970581f..dc76fec 100644
--- a/src/app/common/testing/permission-stubs.ts
+++ b/src/app/common/testing/permission-stubs.ts
@@ -17,7 +17,7 @@
* under the License.
*/
import {Directive, Input} from '@angular/core';
-import {FimsPermission} from '../../sevices/security/authz/fims-permission.model';
+import {FimsPermission} from '../../services/security/authz/fims-permission.model';
@Directive({
// tslint:disable-next-line:directive-selector
diff --git a/src/app/common/text-input/text-input.component.html b/src/app/common/text-input/text-input.component.html
new file mode 100644
index 0000000..613674c
--- /dev/null
+++ b/src/app/common/text-input/text-input.component.html
@@ -0,0 +1,55 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<mat-form-field layout-margin flex [formGroup]="form" *ngIf="show">
+ <input matInput
+ [id]="controlName"
+ [placeholder]="placeholder"
+ [formControlName]="controlName"
+ [attr.type]="type"
+ [attr.title]="title"
+ step="any"
+ tdAutoTrim
+ flex/>
+ <mat-error *ngIf="hasRequiredError" translate>
+ Required
+ </mat-error>
+ <mat-error *ngIf="hasMinLengthError">
+ {{ 'Must have at least characters.' | translate:{ value: form.get(controlName).getError('minlength')['requiredLength']} }}
+ </mat-error>
+ <mat-error *ngIf="hasMaxLengthError">
+ {{ 'Only characters allowed.' | translate:{ value: form.get(controlName).getError('maxlength')['requiredLength']} }}
+ </mat-error>
+ <mat-error *ngIf="hasEmailError" translate>
+ Invalid email
+ </mat-error>
+ <mat-error *ngIf="hasIsNumberError" translate>
+ Must be a number
+ </mat-error>
+ <mat-error *ngIf="hasMinValueError">
+ {{ 'Value must be greater than or equal to' | translate:{ value: form.get(controlName).getError('minValue').value} }}
+ </mat-error>
+ <mat-error *ngIf="hasMaxValueError">
+ {{ 'Value must be smaller than or equal to' | translate:{value: form.get(controlName).getError('maxValue').value} }}
+ </mat-error>
+ <mat-error *ngIf="hasGreaterThanValueError">
+ {{ 'Value must be greater than' | translate:{ value: form.get(controlName).getError('greaterThanValue').value} }}
+ </mat-error>
+ <mat-error *ngIf="hasMaxScaleError">
+ {{ 'Value scale must be smaller than or equal to' | translate:{ value: form.get(controlName).getError('maxScale').value} }}
+ </mat-error>
+</mat-form-field>
diff --git a/src/app/common/text-input/text-input.component.ts b/src/app/common/text-input/text-input.component.ts
new file mode 100644
index 0000000..2b8df6c
--- /dev/null
+++ b/src/app/common/text-input/text-input.component.ts
@@ -0,0 +1,90 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, HostBinding, Input} from '@angular/core';
+import {FormGroup} from '@angular/forms';
+
+@Component({
+ selector: 'fims-text-input',
+ templateUrl: './text-input.component.html'
+})
+export class TextInputComponent {
+
+ @HostBinding('attr.layout')
+ @Input() layout = 'row';
+
+ @Input() placeholder: string;
+
+ @Input() controlName: string;
+
+ @Input() form: FormGroup;
+
+ @Input() type: string;
+
+ @Input() hideIfDisabled = false;
+
+ @Input() title = '';
+
+ get hasRequiredError(): boolean {
+ return this.hasError('required');
+ }
+
+ get hasMinLengthError(): boolean {
+ return this.hasError('minlength');
+ }
+
+ get hasMaxLengthError(): boolean {
+ return this.hasError('maxlength');
+ }
+
+ get hasEmailError(): boolean {
+ return this.hasError('email');
+ }
+
+ get hasIsNumberError(): boolean {
+ return this.hasError('isNumber');
+ }
+
+ get hasMinValueError(): boolean {
+ return this.hasError('minValue');
+ }
+
+ get hasMaxValueError(): boolean {
+ return this.hasError('maxValue');
+ }
+
+ get hasGreaterThanValueError(): boolean {
+ return this.hasError('greaterThanValue');
+ }
+
+ get hasMaxScaleError(): boolean {
+ return this.hasError('maxScale');
+ }
+
+ hasError(key: string): boolean {
+ return this.form.get(this.controlName).hasError(key);
+ }
+
+ get show(): boolean {
+ if (this.hideIfDisabled) {
+ return this.form.get(this.controlName).status !== 'DISABLED';
+ }
+
+ return true;
+ }
+}
diff --git a/src/app/common/util/account-assignments.ts b/src/app/common/util/account-assignments.ts
index 1805ab7..9c2e738 100644
--- a/src/app/common/util/account-assignments.ts
+++ b/src/app/common/util/account-assignments.ts
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import {AccountAssignment} from '../../sevices/portfolio/domain/account-assignment.model';
+import {AccountAssignment} from '../../services/portfolio/domain/account-assignment.model';
export function findAccountDesignator(accountAssignments: AccountAssignment[], designator: string): AccountAssignment {
return accountAssignments.find(assignment => assignment.designator === designator);
diff --git a/src/app/common/validate-on-blur.directive.ts b/src/app/common/validate-on-blur.directive.ts
new file mode 100644
index 0000000..56e4dd9
--- /dev/null
+++ b/src/app/common/validate-on-blur.directive.ts
@@ -0,0 +1,71 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {NgControl} from '@angular/forms';
+import {Directive} from '@angular/core';
+
+@Directive({
+ // tslint:disable-next-line:directive-selector
+ selector: '[validate-onblur]',
+ // tslint:disable-next-line:use-host-property-decorator
+ host: {
+ '(focus)': 'onFocus($event)',
+ '(blur)': 'onBlur($event)',
+ '(keyup)': 'onKeyup($event)',
+ '(change)': 'onChange($event)',
+ '(ngModelChange)': 'onNgModelChange($event)'
+ }
+})
+export class ValidateOnBlurDirective {
+
+ private validators: any;
+
+ private asyncValidators: any;
+
+ private wasChanged: any;
+
+ constructor(public formControl: NgControl) {}
+
+ onFocus($event) {
+ this.wasChanged = false;
+ this.validators = this.formControl.control.validator;
+ this.asyncValidators = this.formControl.control.asyncValidator;
+ this.formControl.control.clearAsyncValidators();
+ this.formControl.control.clearValidators();
+ }
+
+ onKeyup($event) {
+ this.wasChanged = true;
+ }
+
+ onChange($event) {
+ this.wasChanged = true;
+ }
+
+ onNgModelChange($event) {
+ this.wasChanged = true;
+ }
+
+ onBlur($event) {
+ this.formControl.control.setAsyncValidators(this.asyncValidators);
+ this.formControl.control.setValidators(this.validators);
+ if (this.wasChanged) {
+ this.formControl.control.updateValueAndValidity();
+ }
+ }
+}
diff --git a/src/app/common/validator/account-exists.validator.ts b/src/app/common/validator/account-exists.validator.ts
new file mode 100644
index 0000000..43e8a60
--- /dev/null
+++ b/src/app/common/validator/account-exists.validator.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {AbstractControl, AsyncValidatorFn} from '@angular/forms';
+import {Observable} from 'rxjs/Observable';
+import {AccountingService} from '../../services/accounting/accounting.service';
+import {isEmptyInputValue, isString} from './validators';
+
+const invalid = Observable.of({
+ invalidAccount: true
+});
+
+export function accountExists(accountingService: AccountingService): AsyncValidatorFn {
+ return (control: AbstractControl): Observable<any> => {
+ if (!control.dirty || isEmptyInputValue(control.value)) {
+ return Observable.of(null);
+ }
+
+ if (isString(control.value) && control.value.trim().length === 0) {
+ return invalid;
+ }
+
+ return accountingService.findAccount(control.value, true)
+ .map(account => null)
+ .catch(() => invalid);
+ };
+
+}
diff --git a/src/app/common/validator/country-exists.validator.ts b/src/app/common/validator/country-exists.validator.ts
new file mode 100644
index 0000000..43ff459
--- /dev/null
+++ b/src/app/common/validator/country-exists.validator.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {CountryService} from '../../services/country/country.service';
+import {AbstractControl, AsyncValidatorFn} from '@angular/forms';
+import {Observable} from 'rxjs/Observable';
+
+export function countryExists(countryService: CountryService): AsyncValidatorFn {
+ return (control: AbstractControl): Observable<any> => {
+ if (!control.dirty || !control.value || control.value.length === 0) {
+ return Observable.of(null);
+ }
+
+ const country = control.value;
+ const displayName: string = country && typeof country === 'object' ? country.displayName : country;
+
+ return Observable.of(displayName)
+ .map(searchTerm => countryService.fetchCountries(displayName))
+ .map(countries => {
+ if (countries.length === 1 && countries[0].displayName === displayName) {
+ return null;
+ }
+ return {
+ invalidCountry: true
+ };
+ });
+ };
+}
diff --git a/src/app/common/validator/customer-exists.validator.ts b/src/app/common/validator/customer-exists.validator.ts
new file mode 100644
index 0000000..5fed4e0
--- /dev/null
+++ b/src/app/common/validator/customer-exists.validator.ts
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {AbstractControl, AsyncValidatorFn} from '@angular/forms';
+import {Observable} from 'rxjs/Observable';
+import {CustomerService} from '../../services/customer/customer.service';
+import {isString} from './validators';
+
+const invalid = Observable.of({
+ invalidCustomer: true
+});
+
+export function customerExists(customerService: CustomerService): AsyncValidatorFn {
+ return (control: AbstractControl): Observable<any> => {
+ if (!control.dirty || !control.value || control.value.length === 0) {
+ return Observable.of(null);
+ }
+
+ if (isString(control.value) && control.value.trim().length === 0) {
+ return invalid;
+ }
+
+ return customerService.getCustomer(control.value, true)
+ .map(customer => null)
+ .catch(() => invalid);
+ };
+}
diff --git a/src/app/common/validator/employee-exists.validator.ts b/src/app/common/validator/employee-exists.validator.ts
new file mode 100644
index 0000000..8faa7e2
--- /dev/null
+++ b/src/app/common/validator/employee-exists.validator.ts
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {OfficeService} from '../../services/office/office.service';
+import {AbstractControl, AsyncValidatorFn} from '@angular/forms';
+import {Observable} from 'rxjs/Observable';
+import {isString} from './validators';
+
+const invalid = Observable.of({
+ invalidEmployee: true
+});
+
+export function employeeExists(officeService: OfficeService): AsyncValidatorFn {
+ return (control: AbstractControl): Observable<any> => {
+ if (!control.dirty || !control.value || control.value.length === 0) {
+ return Observable.of(null);
+ }
+
+ if (isString(control.value) && control.value.trim().length === 0) {
+ return invalid;
+ }
+
+ return officeService.getEmployee(control.value, true)
+ .map(employee => null)
+ .catch(() => invalid);
+ };
+}
diff --git a/src/app/common/validator/exists.validator.spec.ts b/src/app/common/validator/exists.validator.spec.ts
new file mode 100644
index 0000000..078af8d
--- /dev/null
+++ b/src/app/common/validator/exists.validator.spec.ts
@@ -0,0 +1,129 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Observable} from 'rxjs/Observable';
+import {FormControl, ValidationErrors} from '@angular/forms';
+import {fakeAsync, tick} from '@angular/core/testing';
+import {accountExists} from './account-exists.validator';
+import {ledgerExists} from './ledger-exists.validator';
+import {employeeExists} from './employee-exists.validator';
+import {customerExists} from './customer-exists.validator';
+
+describe('exists validator', () => {
+
+ function createValidator(validator: any, methodName: string): any {
+ const service = jasmine.createSpyObj('service', [methodName]);
+
+ return (value: string, returnValue?: any): Observable<ValidationErrors> => {
+ service[methodName].and.returnValue(returnValue);
+
+ const control = new FormControl(value);
+ control.markAsDirty();
+
+ return validator(service)(control) as Observable<ValidationErrors>;
+ };
+ }
+
+ interface TestCase {
+ validator: any;
+ expectedResult: any;
+ }
+
+ const accountValidator = createValidator(accountExists, 'findAccount');
+ const ledgerValidator = createValidator(ledgerExists, 'findLedger');
+ const employeeValidator = createValidator(employeeExists, 'getEmployee');
+ const customerValidator = createValidator(customerExists, 'getCustomer');
+
+ const testWithValidResults: TestCase[] = [
+ { validator: accountValidator, expectedResult: null },
+ { validator: ledgerValidator, expectedResult: null },
+ { validator: employeeValidator, expectedResult: null },
+ { validator: customerValidator, expectedResult: null }
+ ];
+
+ const testWithInvalidResults: TestCase[] = [
+ { validator: accountValidator, expectedResult: { invalidAccount: true } },
+ { validator: ledgerValidator, expectedResult: { invalidLedger: true } },
+ { validator: employeeValidator, expectedResult: { invalidEmployee: true } },
+ { validator: customerValidator, expectedResult: { invalidCustomer: true } }
+ ];
+
+ testWithValidResults.forEach(test => {
+ it('should not return error when no value', fakeAsync(() => {
+ const validator = test.validator('');
+
+ let result = null;
+
+ validator.subscribe(validatorResult => result = validatorResult);
+
+ tick();
+
+ expect(result).toEqual(test.expectedResult);
+ }));
+ });
+
+ testWithInvalidResults.forEach(test => {
+ it('should not return error when no value', fakeAsync(() => {
+ const validator = test.validator(' ');
+
+ let result = null;
+
+ validator.subscribe(validatorResult => result = validatorResult);
+
+ tick();
+
+ expect(result).toEqual(test.expectedResult);
+ }));
+ });
+
+ testWithValidResults.forEach(test => {
+ it('should not return error when found', fakeAsync(() => {
+
+ const validator = test.validator('test', Observable.of({
+ identifier: 'test'
+ }));
+
+ let expectedResult = null;
+
+ validator.subscribe(result => expectedResult = result);
+
+ tick();
+
+ expect(expectedResult).toEqual(test.expectedResult);
+ }));
+ });
+
+
+ testWithInvalidResults.forEach(test => {
+ it('should return error when not found', fakeAsync(() => {
+ const validator = test.validator('test', Observable.throw({}));
+
+ let expectedResult = null;
+
+ validator.subscribe(result => expectedResult = result);
+
+ tick();
+
+ expect(expectedResult).toEqual(test.expectedResult);
+ }));
+ });
+
+
+
+
+});
diff --git a/src/app/common/validator/ledger-exists.validator.ts b/src/app/common/validator/ledger-exists.validator.ts
new file mode 100644
index 0000000..d10c637
--- /dev/null
+++ b/src/app/common/validator/ledger-exists.validator.ts
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {AbstractControl, AsyncValidatorFn, ValidationErrors} from '@angular/forms';
+import {Observable} from 'rxjs/Observable';
+import {AccountingService} from '../../services/accounting/accounting.service';
+import {isString} from './validators';
+
+const invalid = Observable.of({
+ invalidLedger: true
+});
+
+export function ledgerExists(accountingService: AccountingService): AsyncValidatorFn {
+ return (control: AbstractControl): Observable<ValidationErrors | null> => {
+ if (!control.dirty || !control.value || control.value.length === 0) {
+ return Observable.of(null);
+ }
+
+ if (isString(control.value) && control.value.trim().length === 0) {
+ return invalid;
+ }
+
+ return accountingService.findLedger(control.value, true)
+ .map(ledger => null)
+ .catch(() => invalid);
+ };
+}
diff --git a/src/app/common/validator/validators.spec.ts b/src/app/common/validator/validators.spec.ts
new file mode 100644
index 0000000..373b17b
--- /dev/null
+++ b/src/app/common/validator/validators.spec.ts
@@ -0,0 +1,314 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {FimsValidators} from './validators';
+import {FormControl, FormGroup} from '@angular/forms';
+import {dateAsISOString} from '../../services/domain/date.converter';
+
+describe('Validators', () => {
+ describe('urlSafe', () => {
+
+ it('should not return error when url safe', () => {
+ const result = FimsValidators.urlSafe(new FormControl('test-test'));
+ expect(result).toBeNull();
+ });
+
+ it('should return error when not url safe', () => {
+ const result = FimsValidators.urlSafe(new FormControl(' '));
+ expect(result).toEqual({ urlSafe: true });
+ });
+
+ const notAllowed: string[] = [
+ '!',
+ '\'',
+ '(',
+ ')',
+ '~',
+ ];
+
+ notAllowed.forEach(char => {
+ it(`should return error when when it contains ${char}`, () => {
+ const result = FimsValidators.urlSafe(new FormControl(`${char}`));
+ expect(result).toEqual({ urlSafe: true });
+ });
+ });
+
+ });
+
+ describe('scale', () => {
+
+ it('should not return error when scale matches', () => {
+ const validator = FimsValidators.scale(1);
+ expect(validator(new FormControl(1.1))).toBeNull();
+ });
+
+ it('should return error when scale 1', () => {
+ const validator = FimsValidators.scale(1);
+ expect(validator(new FormControl(1))).toEqual({
+ scale: {
+ valid: false,
+ value: 1
+ },
+ });
+ });
+
+ it('should return error when scale 0', () => {
+ const validator = FimsValidators.scale(0);
+ expect(validator(new FormControl(1.2))).toEqual({
+ scale: {
+ valid: false,
+ value: 0
+ }
+ });
+ });
+ });
+
+ describe('minValue with 0', () => {
+
+ it('should not return error when value 0', () => {
+ const validator = FimsValidators.minValue(0);
+ expect(validator(new FormControl(0))).toBeNull();
+ });
+
+ it('should return error when value -1', () => {
+ const validator = FimsValidators.minValue(0);
+ expect(validator(new FormControl(-1))).toEqual({
+ minValue: {
+ valid: false,
+ value: 0
+ }
+ });
+ });
+
+ });
+
+ describe('maxValue with 10', () => {
+ it('should not return error when value 10', () => {
+ const validator = FimsValidators.maxValue(10);
+ expect(validator(new FormControl(10))).toBeNull();
+ });
+
+ it('should return error when value 11', () => {
+ const validator = FimsValidators.maxValue(10);
+ expect(validator(new FormControl(11))).toEqual({
+ maxValue: {
+ valid: false,
+ value: 10
+ }
+ });
+ });
+
+ });
+
+ describe('greaterThanValue with 0', () => {
+
+ it('should not return error when value 1', () => {
+ const validator = FimsValidators.greaterThanValue(0);
+ expect(validator(new FormControl(1))).toBeNull();
+ });
+
+ it('should return error when value 0', () => {
+ const validator = FimsValidators.greaterThanValue(0);
+ expect(validator(new FormControl(0))).toEqual({
+ greaterThanValue: {
+ valid: false,
+ value: 0
+ }
+ });
+ });
+
+ it('should return error when value -1', () => {
+ const validator = FimsValidators.greaterThanValue(0);
+ expect(validator(new FormControl(-1))).toEqual({
+ greaterThanValue: {
+ valid: false,
+ value: 0
+ }
+ });
+ });
+
+ });
+
+ describe('matchRange', () => {
+ const dateOne = new Date();
+ const dateTwo = new Date(dateOne.getTime() + 1000);
+
+ it('should not return error when range is correct', () => {
+ const group = new FormGroup({f1: new FormControl(dateOne.toISOString()), f2: new FormControl(dateTwo.toISOString())});
+ const validator = FimsValidators.matchRange('f1', 'f2');
+ expect(validator(group)).toBeNull();
+ });
+
+ it('should return error when range not correct', () => {
+ const group = new FormGroup({f1: new FormControl(dateOne.toISOString()), f2: new FormControl(dateTwo.toISOString())});
+ const validator = FimsValidators.matchRange('f2', 'f1');
+ expect(validator(group)).toEqual({
+ rangeInvalid: true
+ });
+ });
+ });
+
+ describe('email', () => {
+ it('should return null when email is correct', () => {
+ const result = FimsValidators.email(new FormControl('test@test.de'));
+ expect(result).toBeNull();
+ });
+
+ it('should return error when email is "testtest.de"', () => {
+ const result = FimsValidators.email(new FormControl('testtest.de'));
+ expect(result).toEqual({
+ email: true
+ });
+ });
+ });
+
+ describe('requiredNotEmpty', () => {
+ it('should return null when value exists', () => {
+ const result = FimsValidators.requiredNotEmpty(new FormControl('value'));
+ expect(result).toBeNull();
+ });
+
+ it('should return error when no value exists', () => {
+ const result = FimsValidators.requiredNotEmpty(new FormControl(''));
+ expect(result).toEqual({
+ required: true
+ });
+ });
+
+ it('should return error when value with only whitespaces exists', () => {
+ const result = FimsValidators.requiredNotEmpty(new FormControl(' '));
+ expect(result).toEqual({
+ required: true
+ });
+ });
+ });
+
+ describe('greaterThan', () => {
+ it('should return null when min < max', () => {
+ const validator = FimsValidators.greaterThan('min', 'max');
+ const group = new FormGroup({
+ min: new FormControl(1),
+ max: new FormControl(2)
+ });
+
+ const result = validator(group);
+
+ expect(result).toBeNull();
+ });
+
+ it('should return error when min = max', () => {
+ const validator = FimsValidators.greaterThan('min', 'max');
+ const group = new FormGroup({
+ min: new FormControl(2),
+ max: new FormControl(2)
+ });
+
+ const result = validator(group);
+
+ expect(result).toEqual({
+ greaterThan: true
+ });
+ });
+
+ it('should return error when min > max', () => {
+ const validator = FimsValidators.greaterThan('min', 'max');
+ const group = new FormGroup({
+ min: new FormControl(2),
+ max: new FormControl(1)
+ });
+
+ const result = validator(group);
+
+ expect(result).toEqual({
+ greaterThan: true
+ });
+ });
+ });
+
+ describe('greaterThanEquals', () => {
+ it('should return null when min < max', () => {
+ const validator = FimsValidators.greaterThanEquals('min', 'max');
+ const group = new FormGroup({
+ min: new FormControl(1),
+ max: new FormControl(2)
+ });
+
+ const result = validator(group);
+
+ expect(result).toBeNull();
+ });
+
+ it('should return null when min = max', () => {
+ const validator = FimsValidators.greaterThanEquals('min', 'max');
+ const group = new FormGroup({
+ min: new FormControl(2),
+ max: new FormControl(2)
+ });
+
+ const result = validator(group);
+
+ expect(result).toBeNull();
+ });
+
+ it('should return error when min > max', () => {
+ const validator = FimsValidators.greaterThanEquals('min', 'max');
+ const group = new FormGroup({
+ min: new FormControl(2),
+ max: new FormControl(1)
+ });
+
+ const result = validator(group);
+
+ expect(result).toEqual({
+ greaterThanEquals: true
+ });
+ });
+ });
+
+ describe('beforeToday', () => {
+ it('should return null when date before today', () => {
+ const date = new Date();
+ date.setDate(date.getDate() - 1);
+
+ const result = FimsValidators.beforeToday(new FormControl(dateAsISOString(date)));
+
+ expect(result).toBeNull();
+ });
+
+ it('should return error when date after today', () => {
+ const date = new Date();
+ date.setDate(date.getDate() + 1);
+
+ const result = FimsValidators.beforeToday(new FormControl(dateAsISOString(date)));
+
+ expect(result).toEqual({
+ beforeToday: true
+ });
+ });
+
+ it('should return error when date equals today', () => {
+ const date = new Date();
+
+ const result = FimsValidators.beforeToday(new FormControl(dateAsISOString(date)));
+
+ expect(result).toEqual({
+ beforeToday: true
+ });
+ });
+ });
+});
diff --git a/src/app/common/validator/validators.ts b/src/app/common/validator/validators.ts
new file mode 100644
index 0000000..c6774fe
--- /dev/null
+++ b/src/app/common/validator/validators.ts
@@ -0,0 +1,274 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {AbstractControl, FormGroup, ValidationErrors, ValidatorFn} from '@angular/forms';
+import {todayAsISOString} from '../../services/domain/date.converter';
+
+// tslint:disable-next-line:max-line-length
+const EMAIL_REGEXP =
+ /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/;
+
+export function isEmptyInputValue(value: any): boolean {
+ return value == null || value.length === 0;
+}
+
+export function isString(value: any): boolean {
+ return typeof value === 'string';
+}
+
+export class FimsValidators {
+
+ static urlSafe(control: AbstractControl): ValidationErrors | null {
+ const notAllowed: string[] = [
+ '!',
+ '\'',
+ '(',
+ ')',
+ '~',
+ ];
+
+ const foundNotAllowed = notAllowed.find(char => control.value.indexOf(char) > -1);
+
+ if (control.value && (encodeURIComponent(control.value) !== control.value || !!foundNotAllowed)) {
+ return {
+ urlSafe: true
+ };
+ }
+ return null;
+ }
+
+ static isNumber(control: AbstractControl): ValidationErrors | null {
+ if (control.value && isNaN(control.value)) {
+ return {
+ isNumber: true
+ };
+ }
+ return null;
+ }
+
+ static scale(scale: number): ValidatorFn {
+ return (c: AbstractControl): ValidationErrors | null => {
+ if (!isEmptyInputValue(c.value)) {
+ const stringValue = String(c.value);
+
+ const valueChunks = stringValue.split('.');
+
+ if (valueChunks.length === 1 && scale === 0) {
+ return null;
+ }
+
+ if (valueChunks.length === 2 && valueChunks[1].length === scale) {
+ return null;
+ }
+
+ return {
+ scale: {
+ valid: false,
+ value: scale
+ }
+ };
+
+ }
+ return null;
+ };
+ }
+
+ static maxScale(scale: number): ValidatorFn {
+ return (c: AbstractControl): ValidationErrors | null => {
+ if (!isEmptyInputValue(c.value)) {
+ const stringValue = String(c.value);
+ const valueChunks = stringValue.split('.');
+
+ if (valueChunks.length === 2 && valueChunks[1].length > scale) {
+ return {
+ maxScale: {
+ valid: false,
+ value: scale
+ }
+ };
+ }
+ }
+ return null;
+ };
+ }
+
+ static minValue(minValue: number): ValidatorFn {
+ return (c: AbstractControl): ValidationErrors | null => {
+ if (!isEmptyInputValue(c.value) && (c.value < minValue)) {
+ return {
+ minValue: {
+ valid: false,
+ value: minValue
+ }
+ };
+ }
+ return null;
+ };
+ }
+
+ static maxValue(maxValue: number): ValidatorFn {
+ return (c: AbstractControl): ValidationErrors | null => {
+ if (!isEmptyInputValue(c.value) != null && (c.value > maxValue)) {
+ return {
+ maxValue: {
+ valid: false,
+ value: maxValue
+ }
+ };
+ }
+ return null;
+ };
+ }
+
+ static greaterThanValue(greaterThanValue: number): ValidatorFn {
+ return (c: AbstractControl): ValidationErrors | null => {
+ if (!isEmptyInputValue(c.value) && (c.value <= greaterThanValue)) {
+ return {
+ greaterThanValue: {
+ valid: false,
+ value: greaterThanValue
+ }
+ };
+ }
+ return null;
+ };
+ }
+
+ static greaterThan(firstValue: string, secondValue: string) {
+ return (group: FormGroup): ValidationErrors | null => {
+ const firstNumber: number = Number(group.controls[firstValue].value);
+ const secondNumber: number = Number(group.controls[secondValue].value);
+
+ if (firstNumber == null || secondNumber == null) {
+ return null;
+ }
+
+ if (firstNumber >= secondNumber) {
+ return {
+ greaterThan: true
+ };
+ }
+
+ return null;
+ };
+ }
+
+ static greaterThanEquals(firstValue: string, secondValue: string) {
+ return (group: FormGroup): ValidationErrors | null => {
+ const firstNumber: number = Number(group.controls[firstValue].value);
+ const secondNumber: number = Number(group.controls[secondValue].value);
+
+ if (firstNumber == null || secondNumber == null) {
+ return null;
+ }
+
+ if (firstNumber > secondNumber) {
+ return {
+ greaterThanEquals: true
+ };
+ }
+
+ return null;
+ };
+ }
+
+ static matchValues(firstValue: string, secondValue: string) {
+ return (group: FormGroup): ValidationErrors | null => {
+ const val1 = group.controls[firstValue];
+ const val2 = group.controls[secondValue];
+
+ if (val1.value !== val2.value) {
+ return {
+ mismatch: true
+ };
+ }
+
+ return null;
+ };
+ }
+
+ static matchRange(firstValue: string, secondValue: string) {
+ return (group: FormGroup): ValidationErrors | null => {
+ const val1: AbstractControl = group.controls[firstValue];
+ const val2: AbstractControl = group.controls[secondValue];
+
+ const dateStart: number = Date.parse(val1.value);
+ const dateEnd: number = Date.parse(val2.value);
+
+ if (dateStart > dateEnd) {
+ return {
+ rangeInvalid: true
+ };
+ }
+
+ return null;
+ };
+ }
+
+ static email(control: AbstractControl): ValidationErrors | null {
+ if (isEmptyInputValue(control.value)) {
+ return null;
+ }
+
+ return EMAIL_REGEXP.test(control.value) ? null : {'email': true};
+ }
+
+ static maxFileSize(maxSizeInKB: number) {
+ return (c: AbstractControl): ValidationErrors | null => {
+ const bytes: number = maxSizeInKB * 1024;
+ if (!isEmptyInputValue(c.value) && (c.value.size > bytes)) {
+ return {
+ maxFileSize: {
+ value: maxSizeInKB
+ }
+ };
+ }
+ return null;
+ };
+ }
+
+ static requiredNotEmpty(control: AbstractControl): ValidationErrors | null {
+ return isEmptyInputValue(control.value) || (isString(control.value) && control.value.trim() === '') ? {'required': true} : null;
+ }
+
+ static beforeToday(control: AbstractControl): ValidationErrors | null {
+ const date = new Date(Date.parse(control.value));
+
+ const today = new Date(Date.parse(todayAsISOString()));
+
+ if (date >= today) {
+ return {
+ beforeToday: true
+ };
+ }
+ return null;
+ }
+
+ static afterToday(control: AbstractControl): ValidationErrors | null {
+ const date = new Date(Date.parse(control.value));
+
+ const today = new Date(Date.parse(todayAsISOString()));
+
+ if (date <= today) {
+ return {
+ afterToday: true
+ };
+ }
+ return null;
+ }
+}
diff --git a/src/app/customers/cases/case-exists.guard.ts b/src/app/customers/cases/case-exists.guard.ts
new file mode 100644
index 0000000..d2d3a93
--- /dev/null
+++ b/src/app/customers/cases/case-exists.guard.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import {Injectable} from '@angular/core';
+import * as fromCases from './store';
+import {Observable} from 'rxjs/Observable';
+import {of} from 'rxjs/observable/of';
+import {CasesStore} from './store/index';
+import {PortfolioService} from '../../services/portfolio/portfolio.service';
+import {LoadAction} from './store/case.actions';
+import {ExistsGuardService} from '../../common/guards/exists-guard';
+
+@Injectable()
+export class CaseExistsGuard implements CanActivate {
+
+ constructor(private store: CasesStore,
+ private portfolioService: PortfolioService,
+ private existsGuardService: ExistsGuardService) {}
+
+ hasCaseInStore(id: string): Observable<boolean> {
+ const timestamp$ = this.store.select(fromCases.getCasesLoadedAt)
+ .map(loadedAt => loadedAt[id]);
+
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ hasCaseInApi(productId: string, caseId: string): Observable<boolean> {
+ const getCase$ = this.portfolioService.getCase(productId, caseId)
+ .map(caseEntity => new LoadAction({
+ resource: caseEntity
+ }))
+ .do((action: LoadAction) => this.store.dispatch(action))
+ .map(caseEntity => !!caseEntity);
+
+ return this.existsGuardService.routeTo404OnError(getCase$);
+ }
+
+ hasCase(productId: string, caseId: string): Observable<boolean> {
+ return this.hasCaseInStore(caseId)
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+
+ return this.hasCaseInApi(productId, caseId);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasCase(route.params['productId'], route.params['caseId']);
+ }
+}
diff --git a/src/app/customers/cases/case.detail.component.html b/src/app/customers/cases/case.detail.component.html
new file mode 100644
index 0000000..6f6111b
--- /dev/null
+++ b/src/app/customers/cases/case.detail.component.html
@@ -0,0 +1,82 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over [title]="caseInstance.identifier" *ngIf="caseInstance$ | async as caseInstance" [navigateBackTo]="['../../../../../../../../']">
+ <td-message *ngIf="caseInstance.currentState === 'APPROVED'" label="{{'Member loan approved' | translate }}" sublabel="{{'To activate this loan you need to disburse' | translate }}" color="accent" icon="error">
+ <button td-message-actions mat-button *hasPermission="{ id: 'portfolio_cases', accessLevel: 'CHANGE'}" [routerLink]="['tasks']" translate>GO TO TASKS</button>
+ </td-message>
+ <fims-two-column-layout>
+ <mat-nav-list left>
+ <h3 mat-subheader translate>Management</h3>
+ <a mat-list-item [routerLink]="['./payments']">
+ <mat-icon matListAvatar>payment</mat-icon>
+ <h3 matLine translate>Planned Payments</h3>
+ <p matLine translate>View payments</p>
+ </a>
+ <a mat-list-item [routerLink]="['./tasks']" *hasPermission="{ id: 'portfolio_cases', accessLevel: 'CHANGE'}">
+ <mat-icon matListAvatar>playlist_add_check</mat-icon>
+ <h3 matLine translate>Tasks</h3>
+ <p matLine translate>Change the status of the member loan</p>
+ </a>
+ <a mat-list-item [routerLink]="['./debtIncome']">
+ <mat-icon matListAvatar>show_chart</mat-icon>
+ <h3 matLine translate>Debt Income Report</h3>
+ <p matLine translate>View debt income report</p>
+ </a>
+ <a mat-list-item [routerLink]="['./documents']" *hasPermission="{ id: 'portfolio_documents', accessLevel: 'READ'}">
+ <mat-icon matListAvatar>content_paste</mat-icon>
+ <h3 matLine translate>Loan documents</h3>
+ <p matLine translate>Manage loan documents</p>
+ </a>
+ </mat-nav-list>
+ <mat-list right>
+ <h3 mat-subheader translate>Current status</h3>
+ <fims-state-display [state]="caseInstance.currentState"></fims-state-display>
+ <h3 mat-subheader translate>Details</h3>
+ <mat-list-item>
+ <h3 matLine translate>Loan product id</h3>
+ <p matLine>{{caseInstance.productIdentifier}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Principal amount</h3>
+ <p matLine>{{caseInstance.parameters.maximumBalance | number:numberFormat}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Interest</h3>
+ <p matLine>{{caseInstance.interest | number:numberFormat}}</p>
+ </mat-list-item>
+ <fims-case-detail-payment-cycle [paymentCycle]="caseInstance.parameters.paymentCycle"></fims-case-detail-payment-cycle>
+ <mat-list-item>
+ <h3 matLine translate>Term</h3>
+ <p matLine>{{caseInstance.parameters.termRange.maximum | number}} {{caseInstance.parameters.termRange.temporalUnit}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Member deposit account</h3>
+ <p matLine>{{caseInstance.depositAccountIdentifier}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Created by</h3>
+ <p matLine>{{caseInstance.createdBy}} - {{caseInstance.createdOn | date:'medium'}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Last modified by</h3>
+ <p matLine>{{caseInstance.lastModifiedBy}} - {{caseInstance.lastModifiedOn | date:'medium'}}</p>
+ </mat-list-item>
+ </mat-list>
+ </fims-two-column-layout>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Edit member loan' | translate}}" icon="mode_edit" [link]="['edit']" *ngIf="canEdit$ | async"></fims-fab-button>
diff --git a/src/app/customers/cases/case.detail.component.ts b/src/app/customers/cases/case.detail.component.ts
new file mode 100644
index 0000000..caa4ab3
--- /dev/null
+++ b/src/app/customers/cases/case.detail.component.ts
@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import * as fromCases from './store/index';
+import {CasesStore} from './store/index';
+import * as fromRoot from '../../store';
+import {Observable} from 'rxjs/Observable';
+import {FimsPermission} from '../../services/security/authz/fims-permission.model';
+import {FimsCase} from '../../services/portfolio/domain/fims-case.model';
+
+@Component({
+ templateUrl: './case.detail.component.html'
+})
+export class CaseDetailComponent implements OnInit {
+
+ numberFormat = '1.2-2';
+
+ caseInstance$: Observable<FimsCase>;
+
+ canEdit$: Observable<boolean>;
+
+ constructor(private casesStore: CasesStore) {}
+
+ ngOnInit(): void {
+ this.caseInstance$ = this.casesStore.select(fromCases.getSelectedCase);
+
+ this.canEdit$ = Observable.combineLatest(
+ this.casesStore.select(fromRoot.getPermissions),
+ this.caseInstance$,
+ (permissions, caseInstance: FimsCase) => ({
+ hasPermission: this.hasChangePermission(permissions),
+ isCreatedOrPending: caseInstance.currentState === 'PENDING' || caseInstance.currentState === 'CREATED'
+ }))
+ .map(result => result.hasPermission && result.isCreatedOrPending);
+ }
+
+ private hasChangePermission(permissions: FimsPermission[]): boolean {
+ return permissions.filter(permission =>
+ permission.id === 'portfolio_cases' &&
+ permission.accessLevel === 'CHANGE'
+ ).length > 0;
+ }
+
+}
diff --git a/src/app/customers/cases/case.index.component.html b/src/app/customers/cases/case.index.component.html
new file mode 100644
index 0000000..ca721b3
--- /dev/null
+++ b/src/app/customers/cases/case.index.component.html
@@ -0,0 +1,18 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<router-outlet></router-outlet>
diff --git a/src/app/customers/cases/case.index.component.ts b/src/app/customers/cases/case.index.component.ts
new file mode 100644
index 0000000..3d85488
--- /dev/null
+++ b/src/app/customers/cases/case.index.component.ts
@@ -0,0 +1,44 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {Subscription} from 'rxjs/Subscription';
+import {CasesStore} from './store/index';
+import {SelectAction} from './store/case.actions';
+
+@Component({
+ templateUrl: './case.index.component.html'
+})
+export class CaseIndexComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ constructor(private route: ActivatedRoute, private casesStore: CasesStore) {}
+
+ ngOnInit(): void {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectAction(params['caseId']))
+ .subscribe(this.casesStore);
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ }
+
+}
diff --git a/src/app/customers/cases/case.list.component.html b/src/app/customers/cases/case.list.component.html
new file mode 100644
index 0000000..0b5dbe6
--- /dev/null
+++ b/src/app/customers/cases/case.list.component.html
@@ -0,0 +1,27 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Manage member loans' | translate}}" [navigateBackTo]="['../../../../']">
+ <fims-data-table flex
+ (onFetch)="fetchCases($event)"
+ (onActionCellClick)="rowSelect($event)"
+ [columns]="columns"
+ [pageable]="true"
+ [data]="casesData$ | async">
+ </fims-data-table>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Create new loan for member ' | translate}}" icon="add" [link]="['create']" *ngIf="canAdd$ | async"></fims-fab-button>
diff --git a/src/app/customers/cases/case.list.component.spec.ts b/src/app/customers/cases/case.list.component.spec.ts
new file mode 100644
index 0000000..174bbb2
--- /dev/null
+++ b/src/app/customers/cases/case.list.component.spec.ts
@@ -0,0 +1,136 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {CaseListComponent} from './case.list.component';
+import {ActivatedRouteStub, RouterLinkStubDirective, RouterStub} from '../../common/testing/router-stubs';
+import {ActivatedRoute, Router} from '@angular/router';
+import {TranslateModule} from '@ngx-translate/core';
+import {Observable} from 'rxjs/Observable';
+import * as fromCases from './store/index';
+import {CasesStore} from './store/index';
+import * as fromCustomers from '../store';
+import * as fromRoot from '../../store';
+import {By} from '@angular/platform-browser';
+import {CUSTOM_ELEMENTS_SCHEMA, DebugElement} from '@angular/core';
+import {Customer} from '../../services/customer/domain/customer.model';
+import {CustomerState} from '../../services/customer/domain/customer-state.model';
+import {FimsPermission} from '../../services/security/authz/fims-permission.model';
+
+describe('Test case list component', () => {
+
+ let component: CaseListComponent;
+ let fixture: ComponentFixture<CaseListComponent>;
+
+ beforeEach(() => {
+ const activatedRoute = new ActivatedRouteStub();
+ const routerStub = new RouterStub();
+
+ TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot()
+ ],
+ declarations: [
+ RouterLinkStubDirective,
+ CaseListComponent
+ ],
+ providers: [
+ { provide: ActivatedRoute, useValue: activatedRoute },
+ { provide: Router, useValue: routerStub },
+ { provide: CasesStore, useValue: jasmine.createSpyObj('casesStore', ['select', 'dispatch']) }
+ ],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA]
+ });
+
+ fixture = TestBed.createComponent(CaseListComponent);
+
+ component = fixture.componentInstance;
+ });
+
+ function setup(currentState: CustomerState, hasChangePermission: boolean) {
+ const customer: Customer = {
+ identifier: 'test',
+ type: 'BUSINESS',
+ givenName: 'test',
+ surname: 'test',
+ dateOfBirth: undefined,
+ address: undefined,
+ customValues: [],
+ member: false,
+ currentState
+ };
+
+ const permissions: FimsPermission[] = [];
+
+ if (hasChangePermission) {
+ permissions.push({
+ id: 'portfolio_cases',
+ accessLevel: 'CHANGE'
+ });
+ }
+
+ const casesStore = TestBed.get(CasesStore);
+
+ casesStore.select.and.callFake(selector => {
+ if (selector === fromCases.getCaseSearchResults) {
+ return Observable.of({});
+ }
+ if (selector === fromCustomers.getSelectedCustomer) {
+ return Observable.of(customer);
+ }
+ if (selector === fromRoot.getPermissions) {
+ return Observable.of(permissions);
+ }
+ });
+ }
+
+ function getCreateButton(): DebugElement {
+ return fixture.debugElement.query(By.css('fims-fab-button'));
+ }
+
+ it('should not display add button when customer is not active but has change permission', () => {
+ setup('PENDING', true);
+
+ fixture.detectChanges();
+
+ const button = getCreateButton();
+
+ expect(button).toBeNull();
+ });
+
+ it('should not display add button when customer is active but has no change permission', () => {
+ setup('ACTIVE', false);
+
+ fixture.detectChanges();
+
+ const button = getCreateButton();
+
+ expect(button).toBeNull();
+ });
+
+ it('should display add button when customer is active and has change permission', () => {
+ setup('ACTIVE', true);
+
+ fixture.detectChanges();
+
+ const button = getCreateButton();
+
+ expect(button).not.toBeNull();
+ });
+
+});
diff --git a/src/app/customers/cases/case.list.component.ts b/src/app/customers/cases/case.list.component.ts
new file mode 100644
index 0000000..428221d
--- /dev/null
+++ b/src/app/customers/cases/case.list.component.ts
@@ -0,0 +1,106 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {TableData} from '../../common/data-table/data-table.component';
+import {FetchRequest} from '../../services/domain/paging/fetch-request.model';
+import {Case} from '../../services/portfolio/domain/case.model';
+import {Customer} from '../../services/customer/domain/customer.model';
+import * as fromCases from './store/index';
+import {CasesStore} from './store/index';
+import * as fromCustomers from '../store';
+import * as fromRoot from '../../store';
+import {Observable} from 'rxjs/Observable';
+import {Subscription} from 'rxjs/Subscription';
+import {SEARCH} from './store/case.actions';
+import {FimsPermission} from '../../services/security/authz/fims-permission.model';
+
+@Component({
+ templateUrl: './case.list.component.html'
+})
+export class CaseListComponent implements OnInit, OnDestroy {
+
+ private customerSubscription: Subscription;
+
+ private customer: Customer;
+
+ casesData$: Observable<TableData>;
+
+ canAdd$: Observable<boolean>;
+
+ columns: any[] = [
+ { name: 'identifier', label: 'Id' },
+ { name: 'productIdentifier', label: 'Loan product id' },
+ { name: 'parameters', label: 'Principal', format: v => v.maximumBalance },
+ { name: 'interest', label: 'Interest' },
+ { name: 'currentState', label: 'Current status' }
+ ];
+
+ constructor(private router: Router, private route: ActivatedRoute, private casesStore: CasesStore) {}
+
+ ngOnInit(): void {
+ this.casesData$ = this.casesStore.select(fromCases.getCaseSearchResults)
+ .map(casePage => ({
+ totalElements: casePage.totalElements,
+ totalPages: casePage.totalPages,
+ data: casePage.cases
+ }));
+
+ const customer$ = this.casesStore.select(fromCustomers.getSelectedCustomer)
+ .filter(customer => !!customer);
+
+ this.customerSubscription = customer$
+ .subscribe(customer => {
+ this.customer = customer;
+ this.fetchCases();
+ });
+
+ this.canAdd$ = Observable.combineLatest(
+ this.casesStore.select(fromRoot.getPermissions),
+ customer$,
+ (permissions, customer: Customer) => ({
+ hasPermission: this.hasChangePermission(permissions),
+ isCustomerActive: customer.currentState === 'ACTIVE'
+ }))
+ .map(result => result.hasPermission && result.isCustomerActive);
+ }
+
+ ngOnDestroy(): void {
+ this.customerSubscription.unsubscribe();
+ }
+
+ fetchCases(fetchRequest?: FetchRequest): void {
+ this.casesStore.dispatch({ type: SEARCH, payload: {
+ customerId: this.customer.identifier,
+ fetchRequest
+ }});
+ }
+
+ rowSelect(caseInstance: Case): void {
+ this.router.navigate(['products', caseInstance.productIdentifier, 'detail', caseInstance.identifier], { relativeTo: this.route });
+ }
+
+ private hasChangePermission(permissions: FimsPermission[]): boolean {
+ return permissions.filter(permission =>
+ permission.id === 'portfolio_cases' &&
+ permission.accessLevel === 'CHANGE'
+ ).length > 0;
+ }
+
+}
diff --git a/src/app/customers/cases/case.module.ts b/src/app/customers/cases/case.module.ts
new file mode 100644
index 0000000..0a9a3f3
--- /dev/null
+++ b/src/app/customers/cases/case.module.ts
@@ -0,0 +1,170 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {FimsSharedModule} from '../../common/common.module';
+import {CaseRoutes} from './case.routes';
+import {NgModule} from '@angular/core';
+import {RouterModule} from '@angular/router';
+import {CasePaymentsComponent} from './payments/payments.component';
+import {CaseDetailFormComponent} from './form/detail/detail.component';
+import {CaseCreateComponent} from './form/create.component';
+import {CaseFormComponent} from './form/form.component';
+import {CaseListComponent} from './case.list.component';
+import {CaseDetailComponent} from './case.detail.component';
+import {CaseEditComponent} from './form/edit.component';
+import {CasesStore, caseStoreFactory} from './store/index';
+import {Store} from '@ngrx/store';
+import {CaseExistsGuard} from './case-exists.guard';
+import {CaseDetailPaymentCycleComponent} from './payment-cycle/payment-cycle.component';
+import {CasePaymentsApiEffects} from './store/payments/effects/service.effects';
+import {EffectsModule} from '@ngrx/effects';
+import {CaseTasksApiEffects} from './store/tasks/effects/service.effects';
+import {CaseNotificationEffects} from './store/effects/notification.effects';
+import {CaseRouteEffects} from './store/effects/route.effects';
+import {CaseApiEffects} from './store/effects/service.effects';
+import {TranslateModule} from '@ngx-translate/core';
+import {CommonModule} from '@angular/common';
+import {ReactiveFormsModule} from '@angular/forms';
+import {
+ MatAutocompleteModule,
+ MatButtonModule,
+ MatCardModule,
+ MatCheckboxModule,
+ MatIconModule,
+ MatInputModule,
+ MatListModule,
+ MatOptionModule,
+ MatRadioModule,
+ MatSelectModule,
+ MatTabsModule,
+ MatToolbarModule,
+ MatTooltipModule
+} from '@angular/material';
+import {
+ CovalentCommonModule,
+ CovalentDataTableModule,
+ CovalentFileModule,
+ CovalentMessageModule,
+ CovalentNotificationsModule,
+ CovalentStepsModule
+} from '@covalent/core';
+import {CaseStatusComponent} from './status/status.component';
+import {CaseCreditFactorFormComponent} from './form/components/credit-factor.component';
+import {CaseCoSignerFormComponent} from './form/co-signer/co-signer.component';
+import {CaseDebtToIncomeFormComponent} from './form/debt-to-income/debt-to-income.component';
+import {CaseDebtIncomeComponent} from './debt-income/debt-income.component';
+import {CaseTasksComponent} from './status/tasks.component';
+import {CaseCommandComponent} from './status/command.component';
+import {CaseTasksNotificationEffects} from './store/tasks/effects/notification.effects';
+import {CaseCommandConfirmationComponent} from './status/confirmation/confirmation.component';
+import {CaseCommandConfirmationFormComponent} from './status/confirmation/form.component';
+import {CaseTaskComponent} from './status/task.component';
+import {CaseIndexComponent} from './case.index.component';
+import {FeeService} from './status/services/fee.service';
+import {CaseDocumentComponent} from './documents/documents.component';
+import {CaseDocumentFormComponent} from './documents/form/form.component';
+import {CaseDocumentEditComponent} from './documents/form/edit.component';
+import {CaseDocumentCreateComponent} from './documents/form/create.component';
+import {CaseDocumentIndexComponent} from './documents/document.index.component';
+import {CaseDocumentDetailComponent} from './documents/document.detail.component';
+import {DocumentExistsGuard} from './documents/document-exists.guard';
+import {DocumentsService} from './store/documents/effects/services/documents.service';
+import {CaseDocumentApiEffects} from './store/documents/effects/service.effects';
+import {CaseDocumentRouteEffects} from './store/documents/effects/route.effects';
+import {CaseDocumentNotificationEffects} from './store/documents/effects/notification.effects';
+import {UploadPageFormComponent} from './documents/form/upload/upload-page.form.component';
+import {CreateDocumentPageComponent} from './documents/form/upload/create.form.component';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(CaseRoutes),
+ FimsSharedModule,
+ TranslateModule,
+ CommonModule,
+ ReactiveFormsModule,
+ MatTooltipModule,
+ MatTabsModule,
+ MatIconModule,
+ MatListModule,
+ MatToolbarModule,
+ MatInputModule,
+ MatButtonModule,
+ MatOptionModule,
+ MatSelectModule,
+ MatRadioModule,
+ MatCardModule,
+ MatCheckboxModule,
+ MatAutocompleteModule,
+ CovalentCommonModule,
+ CovalentStepsModule,
+ CovalentDataTableModule,
+ CovalentMessageModule,
+ CovalentFileModule,
+ CovalentNotificationsModule,
+
+ EffectsModule.run(CaseApiEffects),
+ EffectsModule.run(CaseRouteEffects),
+ EffectsModule.run(CaseNotificationEffects),
+
+ EffectsModule.run(CaseTasksApiEffects),
+ EffectsModule.run(CaseTasksNotificationEffects),
+ EffectsModule.run(CasePaymentsApiEffects),
+
+ EffectsModule.run(CaseDocumentApiEffects),
+ EffectsModule.run(CaseDocumentRouteEffects),
+ EffectsModule.run(CaseDocumentNotificationEffects),
+
+ ],
+ declarations: [
+ CaseListComponent,
+ CaseIndexComponent,
+ CaseFormComponent,
+ CaseCreateComponent,
+ CaseEditComponent,
+ CaseDetailFormComponent,
+ CaseDetailComponent,
+ CaseDetailPaymentCycleComponent,
+ CasePaymentsComponent,
+ CaseCommandComponent,
+ CaseTasksComponent,
+ CaseTaskComponent,
+ CaseCommandConfirmationComponent,
+ CaseCommandConfirmationFormComponent,
+ CaseStatusComponent,
+ CaseDebtToIncomeFormComponent,
+ CaseCreditFactorFormComponent,
+ CaseCoSignerFormComponent,
+ CaseDebtIncomeComponent,
+ CaseDocumentComponent,
+ CaseDocumentIndexComponent,
+ CaseDocumentDetailComponent,
+ CaseDocumentFormComponent,
+ CaseDocumentCreateComponent,
+ CaseDocumentEditComponent,
+ CreateDocumentPageComponent,
+ UploadPageFormComponent
+ ],
+ providers: [
+ CaseExistsGuard,
+ DocumentExistsGuard,
+ FeeService,
+ DocumentsService,
+ { provide: CasesStore, useFactory: caseStoreFactory, deps: [Store]}
+ ]
+})
+export class CaseModule {}
diff --git a/src/app/customers/cases/case.routes.ts b/src/app/customers/cases/case.routes.ts
new file mode 100644
index 0000000..b2c1586
--- /dev/null
+++ b/src/app/customers/cases/case.routes.ts
@@ -0,0 +1,137 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Routes} from '@angular/router';
+import {CasePaymentsComponent} from './payments/payments.component';
+import {CaseDetailComponent} from './case.detail.component';
+import {CaseCreateComponent} from './form/create.component';
+import {CaseListComponent} from './case.list.component';
+import {CaseEditComponent} from './form/edit.component';
+import {CaseExistsGuard} from './case-exists.guard';
+import {CaseStatusComponent} from './status/status.component';
+import {CaseDebtIncomeComponent} from './debt-income/debt-income.component';
+import {CaseCommandConfirmationComponent} from './status/confirmation/confirmation.component';
+import {CaseIndexComponent} from './case.index.component';
+import {CaseDocumentComponent} from './documents/documents.component';
+import {CaseDocumentIndexComponent} from './documents/document.index.component';
+import {CaseDocumentDetailComponent} from './documents/document.detail.component';
+import {CaseDocumentCreateComponent} from './documents/form/create.component';
+import {CaseDocumentEditComponent} from './documents/form/edit.component';
+import {DocumentExistsGuard} from './documents/document-exists.guard';
+import {CreateDocumentPageComponent} from './documents/form/upload/create.form.component';
+
+export const CaseRoutes: Routes = [
+ {
+ path: '',
+ component: CaseListComponent,
+ data: {
+ hasPermission: {id: 'portfolio_cases', accessLevel: 'READ'}
+ }
+ },
+ {
+ path: 'create',
+ component: CaseCreateComponent,
+ data: {
+ hasPermission: {id: 'portfolio_cases', accessLevel: 'CHANGE'}
+ }
+ },
+ {
+ path: 'products/:productId/detail/:caseId',
+ component: CaseIndexComponent,
+ canActivate: [CaseExistsGuard],
+ data: {
+ hasPermission: {id: 'portfolio_cases', accessLevel: 'READ'}
+ },
+ children: [
+ {
+ path: '', component: CaseDetailComponent
+ },
+ {
+ path: 'edit',
+ component: CaseEditComponent,
+ data: {
+ hasPermission: {id: 'portfolio_cases', accessLevel: 'CHANGE'}
+ }
+ },
+ {
+ path: 'payments', component: CasePaymentsComponent
+ },
+ {
+ path: 'tasks',
+ component: CaseStatusComponent,
+ data: {
+ hasPermission: {id: 'portfolio_cases', accessLevel: 'CHANGE'}
+ }
+ },
+ {
+ path: 'tasks/:action/confirmation',
+ component: CaseCommandConfirmationComponent,
+ data: {
+ hasPermission: {id: 'portfolio_cases', accessLevel: 'CHANGE'}
+ }
+ },
+ {
+ path: 'debtIncome',
+ component: CaseDebtIncomeComponent
+ },
+ {
+ path: 'documents',
+ component: CaseDocumentComponent,
+ data: {
+ hasPermission: {id: 'portfolio_documents', accessLevel: 'READ'}
+ }
+ },
+ {
+ path: 'documents/detail/:documentId',
+ component: CaseDocumentIndexComponent,
+ canActivate: [DocumentExistsGuard],
+ data: {
+ hasPermission: {id: 'portfolio_documents', accessLevel: 'READ'}
+ },
+ children: [
+ {
+ path: '',
+ component: CaseDocumentDetailComponent
+ },
+ {
+ path: 'edit',
+ component: CaseDocumentEditComponent,
+ data: {
+ hasPermission: {id: 'portfolio_documents', accessLevel: 'CHANGE'}
+ }
+ },
+ {
+ path: 'upload',
+ component: CreateDocumentPageComponent,
+ data: {
+ hasPermission: {id: 'portfolio_documents', accessLevel: 'CHANGE'}
+ }
+ }
+ ]
+ },
+ {
+ path: 'documents/create',
+ component: CaseDocumentCreateComponent,
+ data: {
+ hasPermission: {id: 'portfolio_documents', accessLevel: 'CHANGE'}
+ }
+ }
+ ]
+ }
+
+];
diff --git a/src/app/customers/cases/debt-income/debt-income.component.html b/src/app/customers/cases/debt-income/debt-income.component.html
new file mode 100644
index 0000000..6092c36
--- /dev/null
+++ b/src/app/customers/cases/debt-income/debt-income.component.html
@@ -0,0 +1,53 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'View debt income ratio' | translate}}" [navigateBackTo]="['../']">
+ <mat-tab-group>
+ <mat-tab label="Member(Ratio: {{customerOverview.ratio ? (customerOverview.ratio | number:numberFormat) : '-'}})" *ngIf="customerOverview$ | async as customerOverview">
+ <div class="mat-content inset" flex>
+ <h5>Debts(Total: {{customerOverview.debtTotal | number:numberFormat}})</h5>
+ <fims-data-table flex
+ [columns]="columns"
+ [data]="customerOverview.debtTableData"
+ [actionColumn]="false">
+ </fims-data-table>
+ <h5>Income(Total: {{customerOverview.incomeTotal | number:numberFormat}})</h5>
+ <fims-data-table flex
+ [columns]="columns"
+ [data]="customerOverview.incomeTableData"
+ [actionColumn]="false">
+ </fims-data-table>
+ </div>
+ </mat-tab>
+ <mat-tab label="Co-signer(Ratio: {{cosignerOverview.ratio ? (cosignerOverview.ratio | number:numberFormat) : '-'}})" *ngIf="cosignerOverview$ | async as cosignerOverview">
+ <div class="mat-content inset" flex>
+ <h5>Debts(Total: {{cosignerOverview.debtTotal | number:numberFormat}})</h5>
+ <fims-data-table flex
+ [columns]="columns"
+ [data]="cosignerOverview.debtTableData"
+ [actionColumn]="false">
+ </fims-data-table>
+ <h5>Income(Total: {{cosignerOverview.incomeTotal | number:numberFormat}})</h5>
+ <fims-data-table flex
+ [columns]="columns"
+ [data]="cosignerOverview.incomeTableData"
+ [actionColumn]="false">
+ </fims-data-table>
+ </div>
+ </mat-tab>
+ </mat-tab-group>
+</fims-layout-card-over>
diff --git a/src/app/customers/cases/debt-income/debt-income.component.ts b/src/app/customers/cases/debt-income/debt-income.component.ts
new file mode 100644
index 0000000..5909198
--- /dev/null
+++ b/src/app/customers/cases/debt-income/debt-income.component.ts
@@ -0,0 +1,120 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import * as fromCases from '../store/index';
+import {CasesStore} from '../store/index';
+import {Observable} from 'rxjs/Observable';
+import {CreditWorthinessFactor} from '../../../services/portfolio/domain/individuallending/credit-worthiness-factor.model';
+import {CreditWorthinessSnapshot} from '../../../services/portfolio/domain/individuallending/credit-worthiness-snapshot.model';
+import * as fromCustomers from '../../store/index';
+import {Customer} from '../../../services/customer/domain/customer.model';
+import {TableData} from '../../../common/data-table/data-table.component';
+import {FimsCase} from '../../../services/portfolio/domain/fims-case.model';
+
+interface IncomeDebtOverview {
+ debtTableData: TableData;
+ incomeTableData: TableData;
+ ratio: number;
+ debtTotal: number;
+ incomeTotal: number;
+}
+
+@Component({
+ templateUrl: './debt-income.component.html'
+})
+export class CaseDebtIncomeComponent implements OnInit {
+
+ numberFormat = '2.2-2';
+
+ columns: any[] = [
+ { name: 'description', label: 'Description' },
+ { name: 'amount', label: 'Amount' }
+ ];
+
+ customerOverview$: Observable<IncomeDebtOverview>;
+
+ cosignerOverview$: Observable<IncomeDebtOverview>;
+
+ constructor(private store: CasesStore) {}
+
+ ngOnInit(): void {
+ const selectedCustomer$: Observable<Customer> = this.store.select(fromCustomers.getSelectedCustomer);
+
+ const snapshots$: Observable<CreditWorthinessSnapshot[]> = this.store.select(fromCases.getSelectedCase)
+ .map((fimsCase: FimsCase) => fimsCase.parameters.creditWorthinessSnapshots);
+
+ const combined$ = Observable.combineLatest(selectedCustomer$, snapshots$, (customer, snapshots) => ({
+ customer,
+ snapshots
+ }));
+
+ this.customerOverview$ = combined$
+ .map(result => result.snapshots.find(snapshot => snapshot.forCustomer === result.customer.identifier))
+ .map(snapshot => snapshot ? snapshot : this.mapEmptySnapshot())
+ .map(snapshot => this.mapToOverview(snapshot));
+
+ this.cosignerOverview$ = combined$
+ .map(result => result.snapshots.find(snapshot => snapshot.forCustomer !== result.customer.identifier))
+ .map(snapshot => snapshot ? snapshot : this.mapEmptySnapshot())
+ .map(snapshot => this.mapToOverview(snapshot));
+ }
+
+ private mapEmptySnapshot(): CreditWorthinessSnapshot {
+ return {
+ forCustomer: '',
+ incomeSources: [],
+ debts: [],
+ assets: []
+ };
+ }
+
+ private mapToOverview(snapshot: CreditWorthinessSnapshot): IncomeDebtOverview {
+ const debtTotal = this.sum(snapshot.debts);
+ const incomeTotal = this.sum(snapshot.incomeSources);
+ const ratio = this.divideIfNotZero(debtTotal, incomeTotal);
+ return {
+ debtTableData: this.mapToTableData(snapshot.debts),
+ incomeTableData: this.mapToTableData(snapshot.incomeSources),
+ debtTotal,
+ incomeTotal,
+ ratio
+ };
+ }
+
+ mapToTableData(data: CreditWorthinessFactor[]): TableData {
+ return {
+ data,
+ totalPages: 1,
+ totalElements: data.length
+ };
+ }
+
+ divideIfNotZero(numerator, denominator): number {
+ if (denominator === 0 || isNaN(denominator)) {
+ return null;
+ } else {
+ return numerator / denominator;
+ }
+ }
+
+ sum(factors: CreditWorthinessFactor[]): number {
+ return factors.reduce((acc, val) => acc + parseFloat(val.amount), 0);
+ }
+
+}
diff --git a/src/app/customers/cases/documents/document-exists.guard.ts b/src/app/customers/cases/documents/document-exists.guard.ts
new file mode 100644
index 0000000..8c0cdab
--- /dev/null
+++ b/src/app/customers/cases/documents/document-exists.guard.ts
@@ -0,0 +1,84 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import {CasesStore} from '../store/index';
+import {ExistsGuardService} from '../../../common/guards/exists-guard';
+import {PortfolioService} from '../../../services/portfolio/portfolio.service';
+import {Observable} from 'rxjs/Observable';
+import * as fromCases from '../store';
+import {LoadAction} from '../store/documents/document.actions';
+import {of} from 'rxjs/observable/of';
+import {CustomerService} from '../../../services/customer/customer.service';
+import {CaseCustomerDocuments, Document} from '../../../services/portfolio/domain/case-customer-documents.model';
+import {CustomerDocument} from '../../../services/customer/domain/customer-document.model';
+
+@Injectable()
+export class DocumentExistsGuard implements CanActivate {
+
+ constructor(private store: CasesStore,
+ private customerService: CustomerService,
+ private portfolioService: PortfolioService,
+ private existsGuardService: ExistsGuardService) {}
+
+ hasDocumentInStore(id: string): Observable<boolean> {
+ const timestamp$ = this.store.select(fromCases.getCaseDocumentLoadedAt)
+ .map(loadedAt => loadedAt[id]);
+
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ hasDocumentInApi(productId: string, caseId: string, documentId: string): Observable<boolean> {
+ const getDocument$ = this.portfolioService.getCaseDocuments(productId, caseId)
+ .switchMap((documents: CaseCustomerDocuments) => this.findDocument(documentId, documents.documents))
+ .switchMap((document: Document) => this.customerService.getDocument(document.customerId, document.documentId))
+ .map((customerDocument: CustomerDocument) => new LoadAction({
+ resource: customerDocument
+ }))
+ .do((action: LoadAction) => this.store.dispatch(action))
+ .map(document => !!document);
+
+ return this.existsGuardService.routeTo404OnError(getDocument$);
+ }
+
+ private findDocument(documentId: string, documents: Document[]): Observable<Document> {
+ const foundDocument = documents.find(document => document.documentId === documentId);
+
+ if (!foundDocument) {
+ return Observable.throw(new Error('Document not found'));
+ }
+
+ return Observable.of(foundDocument);
+ }
+
+ hasDocument(productId: string, caseId: string, documentId: string): Observable<boolean> {
+ return this.hasDocumentInStore(documentId)
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+
+ return this.hasDocumentInApi(productId, caseId, documentId);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasDocument(route.parent.params['productId'], route.parent.params['caseId'], route.params['documentId']);
+ }
+}
diff --git a/src/app/customers/cases/documents/document.detail.component.html b/src/app/customers/cases/documents/document.detail.component.html
new file mode 100644
index 0000000..957278f
--- /dev/null
+++ b/src/app/customers/cases/documents/document.detail.component.html
@@ -0,0 +1,64 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ng-container *ngIf="customerDocument$ | async as customerDocument">
+ <fims-layout-card-over title="{{ 'Document' | translate }}" [navigateBackTo]="['../../../../../../../../../../']" *ngIf="currentSelection$ | async as currentSelection">
+ <fims-layout-card-over-header-menu layout="row" layout-align="end center">
+ <a mat-icon-button title="{{'Edit document' | translate}}" [routerLink]="['edit']" *ngIf="canEdit$ | async">
+ <mat-icon>mode_edit</mat-icon>
+ </a>
+ <button mat-icon-button (click)="deleteDocument(currentSelection, customerDocument)" title="{{'Delete document' | translate}}" *ngIf="canEdit$ | async">
+ <mat-icon>delete</mat-icon>
+ </button>
+ </fims-layout-card-over-header-menu>
+ <td-message *ngIf="canEdit$ | async" label="{{'Document not locked' | translate }}"
+ sublabel="{{'You can lock this document' | translate }}"
+ color="accent" icon="lock_open">
+ <button td-message-actions mat-button (click)="lock(currentSelection, customerDocument)" translate>
+ LOCK
+ </button>
+ </td-message>
+ <td-message *ngIf="customerDocument.completed" label="{{'Document locked' | translate }}" color="warn" icon="lock">
+ </td-message>
+ <div class="mat-content inset" flex>
+ <mat-list>
+ <mat-list-item>
+ <h3 matLine translate>Description</h3>
+ <p matLine>{{customerDocument.description}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Created by</h3>
+ <p matLine>{{customerDocument.createdBy}} - {{customerDocument.createdOn | date:'medium'}}</p>
+ </mat-list-item>
+ </mat-list>
+ <mat-list *ngIf="pageNumbers$ | async as pageNumbers">
+ <h3 mat-subheader translate>Pages uploaded</h3>
+ <mat-list-item *ngFor="let pageNumber of pageNumbers">
+ <mat-icon mat-list-icon>content_paste</mat-icon>
+ <h4 matLine>{{pageNumber}}</h4>
+ <button mat-button color="warn" (click)="deletePage(currentSelection, customerDocument, pageNumber)" *ngIf="canEdit$ | async" translate>
+ Delete
+ </button>
+ <button mat-raised-button (click)="viewPage(currentSelection, customerDocument, pageNumber)" translate>
+ View
+ </button>
+ </mat-list-item>
+ </mat-list>
+ </div>
+ </fims-layout-card-over>
+ <fims-fab-button title="{{'Upload page' | translate}}" icon="content_paste" [link]="['upload']" *ngIf="canEdit$ | async"></fims-fab-button>
+</ng-container>
diff --git a/src/app/customers/cases/documents/document.detail.component.ts b/src/app/customers/cases/documents/document.detail.component.ts
new file mode 100644
index 0000000..104a596
--- /dev/null
+++ b/src/app/customers/cases/documents/document.detail.component.ts
@@ -0,0 +1,168 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {CasesStore} from '../store/index';
+import * as fromRoot from '../../../store';
+import * as fromCases from '../store';
+import {Observable} from 'rxjs/Observable';
+import {DeleteDocumentAction, DeletePageAction, LoadAllPagesAction, LockDocumentAction} from '../store/documents/document.actions';
+import {TranslateService} from '@ngx-translate/core';
+import {TdDialogService} from '@covalent/core';
+import {CaseSelection} from '../store/model/case-selection.model';
+import {ActivatedRoute} from '@angular/router';
+import {Subscription} from 'rxjs/Subscription';
+import {CustomerDocument} from '../../../services/customer/domain/customer-document.model';
+import {CustomerService} from '../../../services/customer/customer.service';
+import {ImageComponent} from '../../../common/image/image.component';
+import {FimsPermission} from '../../../services/security/authz/fims-permission.model';
+
+@Component({
+ templateUrl: './document.detail.component.html'
+})
+export class CaseDocumentDetailComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ currentSelection$: Observable<CaseSelection>;
+
+ customerDocument$: Observable<CustomerDocument>;
+
+ pageNumbers$: Observable<number[]>;
+
+ canEdit$: Observable<boolean>;
+
+ constructor(private route: ActivatedRoute, private casesStore: CasesStore,
+ private translate: TranslateService, private dialogService: TdDialogService, private customerService: CustomerService) {}
+
+ ngOnInit(): void {
+ this.customerDocument$ = this.casesStore.select(fromCases.getSelectedCaseDocument)
+ .filter(document => !!document);
+ this.currentSelection$ = this.casesStore.select(fromCases.getCaseSelection);
+ this.pageNumbers$ = this.casesStore.select(fromCases.getAllDocumentPages);
+
+ this.actionsSubscription = Observable.combineLatest(
+ this.customerDocument$,
+ this.currentSelection$,
+ (document, selection) => ({
+ document,
+ selection
+ })
+ ).map(result => new LoadAllPagesAction({
+ customerId: result.selection.customerId,
+ documentId: result.document.identifier
+ })
+ ).subscribe(this.casesStore);
+
+ this.canEdit$ = Observable.combineLatest(
+ this.casesStore.select(fromRoot.getPermissions),
+ this.customerDocument$,
+ (permissions, document: CustomerDocument) => ({
+ hasPermission: this.hasChangePermission(permissions),
+ isLocked: document.completed
+ }))
+ .map(result => result.hasPermission && !result.isLocked);
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ }
+
+ private showTranslatedDialog(title: string, message: string, button: string): Observable<boolean> {
+ return this.translate.get([title, message, button])
+ .mergeMap(result =>
+ this.dialogService.openConfirm({
+ message: result[message],
+ title: result[title],
+ acceptButton: result[button]
+ }).afterClosed()
+ );
+ }
+
+ confirmDeletePage(): Observable<boolean> {
+ const message = 'Do you want to delete this page?';
+ const title = 'Confirm deletion';
+ const button = 'DELETE PAGE';
+
+ return this.showTranslatedDialog(title, message, button);
+ }
+
+ deletePage(selection: CaseSelection, document: CustomerDocument, pageNumber: number): void {
+ this.confirmDeletePage()
+ .filter(accept => accept)
+ .subscribe(() => {
+ const action = new DeletePageAction({
+ customerId: selection.customerId,
+ documentId: document.identifier,
+ pageNumber
+ });
+
+ this.casesStore.dispatch(action);
+ });
+ }
+
+ viewPage(selection: CaseSelection, document: CustomerDocument, pageNumber: number): void {
+ this.customerService.getDocumentPage(selection.customerId, document.identifier, pageNumber)
+ .subscribe(blob => {
+ this.dialogService.open(ImageComponent, {
+ data: blob
+ });
+ });
+ }
+
+ confirmDeleteDocument(): Observable<boolean> {
+ const message = 'Do you want to delete this document?';
+ const title = 'Confirm deletion';
+ const button = 'DELETE DOCUMENT';
+
+ return this.showTranslatedDialog(title, message, button);
+ }
+
+ deleteDocument(selection: CaseSelection, document: CustomerDocument): void {
+ this.confirmDeleteDocument()
+ .filter(accept => accept)
+ .subscribe(() => {
+ const action = new DeleteDocumentAction({
+ customerId: selection.customerId,
+ productId: selection.productId,
+ caseId: selection.caseId,
+ document,
+ activatedRoute: this.route
+ });
+
+ this.casesStore.dispatch(action);
+ });
+ }
+
+ lock(selection: CaseSelection, document: CustomerDocument): void {
+ const action = new LockDocumentAction({
+ customerId: selection.customerId,
+ documentId: document.identifier
+ });
+
+ this.casesStore.dispatch(action);
+ }
+
+ private hasChangePermission(permissions: FimsPermission[]): boolean {
+ return permissions.filter(permission =>
+ permission.id === 'portfolio_documents' &&
+ permission.accessLevel === 'CHANGE'
+ ).length > 0;
+ }
+
+}
diff --git a/src/app/customers/cases/documents/document.index.component.html b/src/app/customers/cases/documents/document.index.component.html
new file mode 100644
index 0000000..ca721b3
--- /dev/null
+++ b/src/app/customers/cases/documents/document.index.component.html
@@ -0,0 +1,18 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<router-outlet></router-outlet>
diff --git a/src/app/customers/cases/documents/document.index.component.ts b/src/app/customers/cases/documents/document.index.component.ts
new file mode 100644
index 0000000..53a9b54
--- /dev/null
+++ b/src/app/customers/cases/documents/document.index.component.ts
@@ -0,0 +1,44 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {Subscription} from 'rxjs/Subscription';
+import {ActivatedRoute} from '@angular/router';
+import {CasesStore} from '../store/index';
+import {SelectAction} from '../store/documents/document.actions';
+
+@Component({
+ templateUrl: './document.index.component.html'
+})
+export class CaseDocumentIndexComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ constructor(private route: ActivatedRoute, private casesStore: CasesStore) {}
+
+ ngOnInit(): void {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectAction(params['documentId']))
+ .subscribe(this.casesStore);
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ }
+
+}
diff --git a/src/app/customers/cases/documents/documents.component.html b/src/app/customers/cases/documents/documents.component.html
new file mode 100644
index 0000000..6b1cfb9
--- /dev/null
+++ b/src/app/customers/cases/documents/documents.component.html
@@ -0,0 +1,26 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Manage documents' | translate}}" [navigateBackTo]="['../']">
+ <fims-data-table flex
+ (onActionCellClick)="rowSelect($event)"
+ [columns]="columns"
+ [pageable]="false"
+ [data]="documentData$ | async">
+ </fims-data-table>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Create new document' | translate}}" icon="add" [link]="['create']"></fims-fab-button>
diff --git a/src/app/customers/cases/documents/documents.component.ts b/src/app/customers/cases/documents/documents.component.ts
new file mode 100644
index 0000000..d0bfcc6
--- /dev/null
+++ b/src/app/customers/cases/documents/documents.component.ts
@@ -0,0 +1,79 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {TableData} from '../../../common/data-table/data-table.component';
+import {CasesStore} from '../store/index';
+import * as fromCases from '../store';
+import {LoadAllAction} from '../store/documents/document.actions';
+import {CaseSelection} from '../store/model/case-selection.model';
+import {Subscription} from 'rxjs/Subscription';
+import {CustomerDocument} from '../../../services/customer/domain/customer-document.model';
+import {ActivatedRoute, Router} from '@angular/router';
+import {DatePipe} from '@angular/common';
+
+@Component({
+ templateUrl: './documents.component.html',
+ providers: [DatePipe]
+})
+export class CaseDocumentComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ currentSelection$: Observable<CaseSelection>;
+
+ documentData$: Observable<TableData>;
+
+ columns: any[] = [
+ { name: 'description', label: 'Description' },
+ { name: 'createdBy', label: 'Created by' },
+ { name: 'createdOn', label: 'Created on', format: value => this.datePipe.transform(value, 'short') }
+ ];
+
+ constructor(private casesStore: CasesStore, private router: Router, private route: ActivatedRoute, private datePipe: DatePipe) {
+ this.currentSelection$ = casesStore.select(fromCases.getCaseSelection);
+
+ this.documentData$ = casesStore.select(fromCases.getAllCaseDocumentEntities)
+ .map(entities => ({
+ data: entities,
+ totalElements: entities.length,
+ totalPages: 1
+ }))
+ }
+
+ ngOnInit(): void {
+ this.actionsSubscription = this.currentSelection$
+ .map(selection => new LoadAllAction({
+ customerId: selection.customerId,
+ productId: selection.productId,
+ caseId: selection.caseId
+ }))
+ .subscribe(this.casesStore);
+ }
+
+ ngOnDestroy(): void {
+ if (this.actionsSubscription) {
+ this.actionsSubscription.unsubscribe();
+ }
+ }
+
+ rowSelect(document: CustomerDocument): void {
+ this.router.navigate(['detail', document.identifier], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/customers/cases/documents/form/create.component.html b/src/app/customers/cases/documents/form/create.component.html
new file mode 100644
index 0000000..970823b
--- /dev/null
+++ b/src/app/customers/cases/documents/form/create.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Create document' | translate }}" *ngIf="currentSelection$ | async as currentSelection">
+ <fims-case-document-form #form
+ [document]="document"
+ (onSave)="onSave(currentSelection, $event)"
+ (onCancel)="onCancel()"
+ [editMode]="false">
+ </fims-case-document-form>
+</fims-layout-card-over>
diff --git a/src/app/customers/cases/documents/form/create.component.ts b/src/app/customers/cases/documents/form/create.component.ts
new file mode 100644
index 0000000..535acfc
--- /dev/null
+++ b/src/app/customers/cases/documents/form/create.component.ts
@@ -0,0 +1,59 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component} from '@angular/core';
+import {CustomerDocument} from '../../../../services/customer/domain/customer-document.model';
+import {ActivatedRoute, Router} from '@angular/router';
+import {CasesStore} from '../../store/index';
+import {CREATE, CreateDocumentAction} from '../../store/documents/document.actions';
+import * as fromCases from '../../store';
+import {Observable} from 'rxjs/Observable';
+import {CaseSelection} from '../../store/model/case-selection.model';
+
+@Component({
+ templateUrl: './create.component.html'
+})
+export class CaseDocumentCreateComponent {
+
+ currentSelection$: Observable<CaseSelection>;
+
+ document: CustomerDocument = {
+ identifier: ''
+ };
+
+ constructor(private router: Router, private route: ActivatedRoute, private casesStore: CasesStore) {
+ this.currentSelection$ = casesStore.select(fromCases.getCaseSelection);
+ }
+
+ onSave(selection: CaseSelection, document: CustomerDocument) {
+ const action = new CreateDocumentAction({
+ productId: selection.productId,
+ caseId: selection.caseId,
+ customerId: selection.customerId,
+ document,
+ activatedRoute: this.route
+ });
+
+ this.casesStore.dispatch(action);
+ }
+
+ onCancel() {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+
+}
diff --git a/src/app/customers/cases/documents/form/edit.component.html b/src/app/customers/cases/documents/form/edit.component.html
new file mode 100644
index 0000000..bcd5746
--- /dev/null
+++ b/src/app/customers/cases/documents/form/edit.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Edit document' | translate }}" *ngIf="currentSelection$ | async as currentSelection">
+ <fims-case-document-form #form
+ [document]="document$ | async"
+ (onSave)="onSave(currentSelection, $event)"
+ (onCancel)="onCancel()"
+ [editMode]="true">
+ </fims-case-document-form>
+</fims-layout-card-over>
diff --git a/src/app/customers/cases/documents/form/edit.component.ts b/src/app/customers/cases/documents/form/edit.component.ts
new file mode 100644
index 0000000..22db4a4
--- /dev/null
+++ b/src/app/customers/cases/documents/form/edit.component.ts
@@ -0,0 +1,58 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy} from '@angular/core';
+import {CustomerDocument} from '../../../../services/customer/domain/customer-document.model';
+import {Observable} from 'rxjs/Observable';
+import {ActivatedRoute, Router} from '@angular/router';
+import {CaseSelection} from '../../store/model/case-selection.model';
+import {CasesStore} from '../../store/index';
+import * as fromCases from '../../store';
+import {Subscription} from 'rxjs/Subscription';
+import {UpdateDocumentAction} from '../../store/documents/document.actions';
+
+@Component({
+ templateUrl: './edit.component.html'
+})
+export class CaseDocumentEditComponent {
+
+ currentSelection$: Observable<CaseSelection>;
+
+ document$: Observable<CustomerDocument>;
+
+ constructor(private casesStore: CasesStore, private router: Router, private route: ActivatedRoute) {
+ this.currentSelection$ = casesStore.select(fromCases.getCaseSelection);
+ this.document$ = casesStore.select(fromCases.getSelectedCaseDocument);
+ }
+
+ onSave(selection: CaseSelection, document: CustomerDocument) {
+ const action = new UpdateDocumentAction({
+ customerId: selection.customerId,
+ productId: selection.productId,
+ caseId: selection.caseId,
+ document,
+ activatedRoute: this.route
+ });
+
+ this.casesStore.dispatch(action)
+ }
+
+ onCancel() {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/customers/cases/documents/form/form.component.html b/src/app/customers/cases/documents/form/form.component.html
new file mode 100644
index 0000000..944b9a0
--- /dev/null
+++ b/src/app/customers/cases/documents/form/form.component.html
@@ -0,0 +1,40 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Details' | translate}}" [active]="true">
+ <div layout="row" [formGroup]="form">
+ <mat-form-field layout-margin flex>
+ <textarea matInput placeholder="{{'Description(Optional)' | translate}}" formControlName="description"></textarea>
+ <mat-error *ngIf="form.get('description').hasError('maxlength')">
+ {{ 'Only characters allowed.' | translate:{ value: form.get('description').getError('maxlength')['requiredLength']} }}
+ </mat-error>
+ </mat-form-field>
+ </div>
+ </td-step>
+ <td-step label="{{'Final step' | translate}}" [state]="'complete'">
+ <ng-template td-step-summary>
+ <fims-form-final-action
+ [resourceName]="'DOCUMENT'"
+ [editMode]="editMode"
+ [disabled]="form.invalid"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/customers/cases/documents/form/form.component.ts b/src/app/customers/cases/documents/form/form.component.ts
new file mode 100644
index 0000000..7730f1b
--- /dev/null
+++ b/src/app/customers/cases/documents/form/form.component.ts
@@ -0,0 +1,66 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild} from '@angular/core';
+import {CustomerDocument} from '../../../../services/customer/domain/customer-document.model';
+import {FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {TdStepComponent} from '@covalent/core';
+
+@Component({
+ selector: 'fims-case-document-form',
+ templateUrl: './form.component.html'
+})
+export class CaseDocumentFormComponent implements OnChanges {
+
+ form: FormGroup;
+
+ @Input() document: CustomerDocument;
+
+ @Input() editMode: boolean;
+
+ @Output('onSave') onSave = new EventEmitter<CustomerDocument>();
+
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder) {
+ this.form = formBuilder.group({
+ description: ['', Validators.maxLength(4096)]
+ })
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes.document) {
+ this.form.reset({
+ description: this.document.description
+ })
+ }
+ }
+
+ save(): void {
+ const document: CustomerDocument = Object.assign({}, this.document, {
+ description: this.form.get('description').value
+ });
+
+ this.onSave.emit(document);
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+}
diff --git a/src/app/customers/cases/documents/form/upload/create.form.component.html b/src/app/customers/cases/documents/form/upload/create.form.component.html
new file mode 100644
index 0000000..c6f7542
--- /dev/null
+++ b/src/app/customers/cases/documents/form/upload/create.form.component.html
@@ -0,0 +1,23 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Upload new page' | translate}}">
+ <fims-upload-page-form #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()">
+ </fims-upload-page-form>
+</fims-layout-card-over>
diff --git a/src/app/customers/cases/documents/form/upload/create.form.component.ts b/src/app/customers/cases/documents/form/upload/create.form.component.ts
new file mode 100644
index 0000000..3393bb3
--- /dev/null
+++ b/src/app/customers/cases/documents/form/upload/create.form.component.ts
@@ -0,0 +1,75 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {Component, OnDestroy} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {UploadPageFormData} from './upload-page.form.component';
+import {UploadPageAction} from '../../../store/documents/document.actions';
+import {CasesStore} from '../../../store/index';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Subscription} from 'rxjs/Subscription';
+import {CaseSelection} from '../../../store/model/case-selection.model';
+import * as fromCases from '../../../store';
+import {CustomerDocument} from '../../../../../services/customer/domain/customer-document.model';
+
+@Component({
+ templateUrl: './create.form.component.html'
+})
+export class CreateDocumentPageComponent implements OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ private currentSelection$: Observable<CaseSelection>;
+
+ private document$: Observable<CustomerDocument>;
+
+ constructor(private router: Router, private route: ActivatedRoute, private casesStore: CasesStore) {
+ this.currentSelection$ = casesStore.select(fromCases.getCaseSelection);
+ this.document$ = casesStore.select(fromCases.getSelectedCaseDocument);
+ }
+
+ ngOnDestroy(): void {
+ if (this.actionsSubscription) {
+ this.actionsSubscription.unsubscribe();
+ }
+ }
+
+ onSave(formData: UploadPageFormData): void {
+ this.actionsSubscription = Observable.combineLatest(
+ this.currentSelection$,
+ this.document$,
+ (selection, document) => ({
+ selection,
+ document
+ })
+ ).map(result => new UploadPageAction({
+ customerId: result.selection.customerId,
+ documentId: result.document.identifier,
+ page: formData.file,
+ pageNumber: formData.pageNumber,
+ activatedRoute: this.route
+ })
+ ).subscribe(this.casesStore);
+ }
+
+ onCancel(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+
+}
diff --git a/src/app/customers/cases/documents/form/upload/upload-page.form.component.html b/src/app/customers/cases/documents/form/upload/upload-page.form.component.html
new file mode 100644
index 0000000..24f1cf0
--- /dev/null
+++ b/src/app/customers/cases/documents/form/upload/upload-page.form.component.html
@@ -0,0 +1,66 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Upload new page' | translate}}"
+ [state]="form.valid ? 'complete' : form.pristine ? 'none' : 'required'" [active]="true">
+ <form [formGroup]="form" layout="column">
+ <mat-form-field flex layout-margin>
+ <input matInput type="number" placeholder="{{'Page number' | translate}}" formControlName="pageNumber"/>
+ <mat-error *ngIf="form.get('pageNumber').hasError('required')" translate>
+ Required
+ </mat-error>
+ <mat-error *ngIf="form.get('pageNumber').hasError('minValue')">
+ {{ 'Value must be greater than or equal to' | translate:{ value: form.get('pageNumber').getError('minValue').value} }}
+ </mat-error>
+ </mat-form-field>
+ <div layout="row">
+ <mat-form-field tdFileDrop
+ [disabled]="true"
+ flex layout-margin>
+ <input matInput
+ placeholder="{{'Selected file' | translate }}"
+ [value]="form.get('file').value ? form.get('file').value.name : ''"
+ [disabled]="true"
+ readonly/>
+ <mat-hint class="tc-red-500" *ngIf="form.get('file').hasError('maxFileSize')">
+ {{ 'Max file size' | translate:{ value: form.get('file').getError('maxFileSize')['value']} }}
+ </mat-hint>
+ </mat-form-field>
+ <button mat-icon-button *ngIf="form.get('file').value" (click)="fileInput.clear()"
+ (keyup.enter)="fileInput.clear()">
+ <mat-icon>cancel</mat-icon>
+ </button>
+ <td-file-input class="push-left-sm push-right-sm" #fileInput formControlName="file" accept=".jpg,.png">
+ <mat-icon>folder</mat-icon>
+ <span class="text-upper" translate>Browse...</span>
+ </td-file-input>
+ </div>
+ </form>
+ </td-step>
+ <td-step label="{{'Final step' | translate}}" [state]="'complete'">
+ <ng-template td-step-summary>
+ <fims-form-final-action
+ [resourceName]="'PAGE'"
+ [editMode]="false"
+ [disabled]="!form.valid"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/customers/cases/documents/form/upload/upload-page.form.component.ts b/src/app/customers/cases/documents/form/upload/upload-page.form.component.ts
new file mode 100644
index 0000000..36e4a40
--- /dev/null
+++ b/src/app/customers/cases/documents/form/upload/upload-page.form.component.ts
@@ -0,0 +1,57 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
+import {FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {FimsValidators} from '../../../../../common/validator/validators';
+
+export interface UploadPageFormData {
+ pageNumber: number;
+ file: File;
+}
+
+@Component({
+ selector: 'fims-upload-page-form',
+ templateUrl: './upload-page.form.component.html'
+})
+export class UploadPageFormComponent implements OnInit {
+
+ form: FormGroup;
+
+ @Output() onSave = new EventEmitter<UploadPageFormData>();
+
+ @Output() onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder) {}
+
+ ngOnInit() {
+ this.form = this.formBuilder.group({
+ pageNumber: ['', [Validators.required, FimsValidators.minValue(0)]],
+ file: ['', [Validators.required, FimsValidators.maxFileSize(512)]]
+ });
+ }
+
+ save(): void {
+ this.onSave.emit(this.form.getRawValue());
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+}
diff --git a/src/app/customers/cases/form/co-signer/co-signer.component.html b/src/app/customers/cases/form/co-signer/co-signer.component.html
new file mode 100644
index 0000000..bbae2cb
--- /dev/null
+++ b/src/app/customers/cases/form/co-signer/co-signer.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<form [formGroup]="form">
+ <fims-customer-select title="{{'Member' | translate}}" formControlName="customerIdentifier">
+ <ng-container *ngIf="form.get('customerIdentifier').hasError('invalidCustomer')" translate>
+ Invalid member
+ </ng-container>
+ </fims-customer-select>
+ <fims-case-debt-to-income-form #debtToIncomeForm [formData]="debtToIncomeFormData"></fims-case-debt-to-income-form>
+</form>
diff --git a/src/app/customers/cases/form/co-signer/co-signer.component.ts b/src/app/customers/cases/form/co-signer/co-signer.component.ts
new file mode 100644
index 0000000..700a91f
--- /dev/null
+++ b/src/app/customers/cases/form/co-signer/co-signer.component.ts
@@ -0,0 +1,74 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input, OnInit, ViewChild} from '@angular/core';
+import {FormComponent} from '../../../../common/forms/form.component';
+import {CreditWorthinessFactor} from '../../../../services/portfolio/domain/individuallending/credit-worthiness-factor.model';
+import {CaseDebtToIncomeFormComponent, DebtToIncomeFormData} from '../debt-to-income/debt-to-income.component';
+import {FormBuilder} from '@angular/forms';
+import {customerExists} from '../../../../common/validator/customer-exists.validator';
+import {CustomerService} from '../../../../services/customer/customer.service';
+
+export interface CoSignerFormData {
+ customerId: string;
+ incomeSources: CreditWorthinessFactor[];
+ debts: CreditWorthinessFactor[];
+}
+
+@Component({
+ selector: 'fims-case-co-signer-form',
+ templateUrl: './co-signer.component.html'
+})
+export class CaseCoSignerFormComponent extends FormComponent<CoSignerFormData> implements OnInit {
+
+ private _formData: CoSignerFormData;
+
+ @Input('formData') set formData(formData: CoSignerFormData) {
+ this._formData = formData;
+ }
+
+ @ViewChild('debtToIncomeForm') debtToIncomeForm: CaseDebtToIncomeFormComponent;
+ debtToIncomeFormData: DebtToIncomeFormData;
+
+ constructor(private formBuilder: FormBuilder, private customerService: CustomerService) {
+ super();
+ }
+
+ ngOnInit(): void {
+ this.form = this.formBuilder.group({
+ customerIdentifier: [this._formData.customerId, [], customerExists(this.customerService)]
+ });
+
+ this.debtToIncomeFormData = {
+ incomeSources: this._formData.incomeSources,
+ debts: this._formData.debts
+ };
+ }
+
+ get formData(): CoSignerFormData {
+ return {
+ customerId: this.form.get('customerIdentifier').value,
+ incomeSources: this.debtToIncomeForm.formData.incomeSources,
+ debts: this.debtToIncomeForm.formData.debts
+ };
+ }
+
+ get valid(): boolean {
+ return this.form.valid && this.debtToIncomeForm.valid;
+ }
+}
diff --git a/src/app/customers/cases/form/components/credit-factor.component.html b/src/app/customers/cases/form/components/credit-factor.component.html
new file mode 100644
index 0000000..dd5e0b0
--- /dev/null
+++ b/src/app/customers/cases/form/components/credit-factor.component.html
@@ -0,0 +1,33 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<form [formGroup]="form">
+ <div layout-gt-xs="column" layout-margin formArrayName="factors">
+ <mat-card *ngFor="let factor of factorControls; let i=index" [formGroupName]="i">
+ <mat-card-content>
+ <div layout="row">
+ <fims-text-input [form]="factor" controlName="description" placeholder="{{'Description' | translate}}"></fims-text-input>
+ <fims-text-input type="number" [form]="factor" controlName="amount" placeholder="{{'Amount' | translate}}"></fims-text-input>
+ <button mat-button (click)="removeFactor(i)">{{'REMOVE ' + factorName | translate}}</button>
+ </div>
+ </mat-card-content>
+ </mat-card>
+ <div layout="row">
+ <button flex mat-button mat-raised-button (click)="addFactor()">{{'ADD ' + factorName | translate}}</button>
+ </div>
+ </div>
+</form>
diff --git a/src/app/customers/cases/form/components/credit-factor.component.ts b/src/app/customers/cases/form/components/credit-factor.component.ts
new file mode 100644
index 0000000..22a7f6d
--- /dev/null
+++ b/src/app/customers/cases/form/components/credit-factor.component.ts
@@ -0,0 +1,80 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input, OnInit} from '@angular/core';
+import {CreditWorthinessFactor} from '../../../../services/portfolio/domain/individuallending/credit-worthiness-factor.model';
+import {AbstractControl, FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {FimsValidators} from '../../../../common/validator/validators';
+import {FormComponent} from '../../../../common/forms/form.component';
+
+
+@Component({
+ selector: 'fims-case-credit-factor-form',
+ templateUrl: './credit-factor.component.html'
+})
+export class CaseCreditFactorFormComponent extends FormComponent<CreditWorthinessFactor[]> implements OnInit {
+
+ @Input() factors: CreditWorthinessFactor[];
+
+ @Input() factorName: string;
+
+ form: FormGroup;
+
+ constructor(private formBuilder: FormBuilder) {
+ super();
+ }
+
+ ngOnInit(): void {
+ this.form = this.formBuilder.group({
+ factors: this.initFactors(this.factors)
+ });
+ }
+
+ get formData(): CreditWorthinessFactor[] {
+ return this.formFactors.value;
+ }
+
+ private initFactors(factors: CreditWorthinessFactor[]): FormArray {
+ const formControls: FormGroup[] = [];
+ factors.forEach(factor => formControls.push(this.initFactor(factor)));
+ return this.formBuilder.array(formControls);
+ }
+
+ private initFactor(factor?: CreditWorthinessFactor): FormGroup {
+ return this.formBuilder.group({
+ description: [factor ? factor.description : ''],
+ amount: [factor ? factor.amount : 0, [ Validators.required, FimsValidators.minValue(0)] ]
+ });
+ }
+
+ get formFactors(): FormArray {
+ return this.form.get('factors') as FormArray;
+ }
+
+ addFactor(): void {
+ this.formFactors.push(this.initFactor());
+ }
+
+ removeFactor(index: number): void {
+ this.formFactors.removeAt(index);
+ }
+
+ get factorControls(): AbstractControl[] {
+ return this.formFactors.controls;
+ }
+}
diff --git a/src/app/customers/cases/form/create.component.html b/src/app/customers/cases/form/create.component.html
new file mode 100644
index 0000000..63a9ebd
--- /dev/null
+++ b/src/app/customers/cases/form/create.component.html
@@ -0,0 +1,28 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Create new loan for member' | translate:{value: fullName} }}">
+ <fims-case-form-component #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [products]="products$ | async"
+ [productInstances]="productsInstances$ | async"
+ [case]="caseInstance"
+ [customerId]="(customer$ | async).identifier"
+ [editMode]="false">
+ </fims-case-form-component>
+</fims-layout-card-over>
diff --git a/src/app/customers/cases/form/create.component.ts b/src/app/customers/cases/form/create.component.ts
new file mode 100644
index 0000000..bacacef
--- /dev/null
+++ b/src/app/customers/cases/form/create.component.ts
@@ -0,0 +1,115 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {CaseFormComponent} from './form.component';
+import * as fromCases from '../store/index';
+import {CasesStore} from '../store/index';
+import * as fromCustomers from '../../store/index';
+import {Subscription} from 'rxjs/Subscription';
+import {CREATE, RESET_FORM} from '../store/case.actions';
+import {Error} from '../../../services/domain/error.model';
+import {Product} from '../../../services/portfolio/domain/product.model';
+import {PortfolioService} from '../../../services/portfolio/portfolio.service';
+import {Observable} from 'rxjs/Observable';
+import {DepositAccountService} from '../../../services/depositAccount/deposit-account.service';
+import {ProductInstance} from '../../../services/depositAccount/domain/instance/product-instance.model';
+import {FimsCase} from '../../../services/portfolio/domain/fims-case.model';
+import {Customer} from '../../../services/customer/domain/customer.model';
+
+@Component({
+ templateUrl: './create.component.html'
+})
+export class CaseCreateComponent implements OnInit, OnDestroy {
+
+ private formStateSubscription: Subscription;
+
+ fullName: string;
+
+ @ViewChild('form') formComponent: CaseFormComponent;
+
+ products$: Observable<Product[]>;
+
+ productsInstances$: Observable<ProductInstance[]>;
+
+ customer$: Observable<Customer>;
+
+ caseInstance: FimsCase = {
+ currentState: 'CREATED',
+ identifier: '',
+ productIdentifier: '',
+ parameters: {
+ customerIdentifier: '',
+ maximumBalance: 0,
+ paymentCycle: {
+ alignmentDay: null,
+ alignmentMonth: null,
+ alignmentWeek: null,
+ period: 1,
+ temporalUnit: 'MONTHS',
+ },
+ termRange: {
+ temporalUnit: 'MONTHS',
+ maximum: 1
+ },
+ creditWorthinessSnapshots: []
+ },
+ interest: 0,
+ depositAccountIdentifier: ''
+ };
+
+ constructor(private router: Router, private route: ActivatedRoute, private casesStore: CasesStore,
+ private portfolioService: PortfolioService, private depositService: DepositAccountService) {}
+
+ ngOnInit(): void {
+ this.customer$ = this.casesStore.select(fromCustomers.getSelectedCustomer)
+ .filter(customer => !!customer)
+ .do(customer => this.fullName = `${customer.givenName} ${customer.surname}`);
+
+ this.formStateSubscription = this.casesStore.select(fromCases.getCaseFormError)
+ .filter((error: Error) => !!error)
+ .subscribe((error: Error) => this.formComponent.showIdentifierValidationError());
+
+ this.products$ = this.portfolioService.findAllProducts(false)
+ .map(productPage => productPage.elements);
+
+ this.productsInstances$ = this.customer$
+ .switchMap(customer => this.depositService.fetchProductInstances(customer.identifier))
+ .map((instances: ProductInstance[]) => instances.filter(instance => instance.state === 'ACTIVE'));
+ }
+
+ ngOnDestroy(): void {
+ this.formStateSubscription.unsubscribe();
+
+ this.casesStore.dispatch({ type: RESET_FORM });
+ }
+
+ onSave(caseInstance: FimsCase): void {
+ this.casesStore.dispatch({ type: CREATE, payload: {
+ productId: caseInstance.productIdentifier,
+ caseInstance,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+
+}
diff --git a/src/app/customers/cases/form/debt-to-income/debt-to-income.component.html b/src/app/customers/cases/form/debt-to-income/debt-to-income.component.html
new file mode 100644
index 0000000..652dd93
--- /dev/null
+++ b/src/app/customers/cases/form/debt-to-income/debt-to-income.component.html
@@ -0,0 +1,23 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<h4>Debts(Total: {{debtTotal | number:numberFormat}})</h4>
+<fims-case-credit-factor-form #debtForm [factorName]="'DEBT'" [factors]="debts"></fims-case-credit-factor-form>
+<h4>Incomes(Total: {{incomeTotal | number:numberFormat}})</h4>
+<fims-case-credit-factor-form #incomeForm [factorName]="'INCOME'" [factors]="incomeSources"></fims-case-credit-factor-form>
+<h3>Ratio</h3>
+{{ratio | number:numberFormat}}
diff --git a/src/app/customers/cases/form/debt-to-income/debt-to-income.component.ts b/src/app/customers/cases/form/debt-to-income/debt-to-income.component.ts
new file mode 100644
index 0000000..fa98efd
--- /dev/null
+++ b/src/app/customers/cases/form/debt-to-income/debt-to-income.component.ts
@@ -0,0 +1,88 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input, ViewChild} from '@angular/core';
+import {CaseCreditFactorFormComponent} from '../components/credit-factor.component';
+import {CreditWorthinessFactor} from '../../../../services/portfolio/domain/individuallending/credit-worthiness-factor.model';
+
+export interface DebtToIncomeFormData {
+ incomeSources: CreditWorthinessFactor[];
+ debts: CreditWorthinessFactor[];
+}
+
+@Component({
+ selector: 'fims-case-debt-to-income-form',
+ templateUrl: './debt-to-income.component.html'
+})
+export class CaseDebtToIncomeFormComponent {
+
+ numberFormat = '2.2-2';
+
+ @ViewChild('incomeForm') incomeFactorComponent: CaseCreditFactorFormComponent;
+ incomeSources: CreditWorthinessFactor[] = [];
+
+ @ViewChild('debtForm') debtsFactorComponent: CaseCreditFactorFormComponent;
+ debts: CreditWorthinessFactor[] = [];
+
+ @Input('formData') set formData(formData: DebtToIncomeFormData) {
+ this.incomeSources = formData.incomeSources;
+ this.debts = formData.debts;
+ }
+
+ constructor() {}
+
+ get formData(): DebtToIncomeFormData {
+ return {
+ incomeSources: this.incomeFactorComponent.formData,
+ debts: this.debtsFactorComponent.formData
+ };
+ }
+
+ get valid(): boolean {
+ return this.incomeFactorComponent.valid && this.debtsFactorComponent.valid;
+ }
+
+ get pristine(): boolean {
+ return this.incomeFactorComponent.pristine || this.debtsFactorComponent.pristine;
+ }
+
+ get ratio(): number {
+ return this.divideIfNotZero(this.debtTotal, this.incomeTotal);
+ }
+
+ divideIfNotZero(numerator, denominator): number {
+ if (denominator === 0 || isNaN(denominator)) {
+ return null;
+ } else {
+ return numerator / denominator;
+ }
+ }
+
+ get incomeTotal(): number {
+ return this.sum(this.formData.incomeSources);
+ }
+
+ get debtTotal(): number {
+ return this.sum(this.formData.debts);
+ }
+
+ private sum(factors: CreditWorthinessFactor[]): number {
+ return factors.reduce((acc, val) => acc + parseFloat(val.amount), 0);
+ }
+
+}
diff --git a/src/app/customers/cases/form/detail/detail.component.html b/src/app/customers/cases/form/detail/detail.component.html
new file mode 100644
index 0000000..d8231cc
--- /dev/null
+++ b/src/app/customers/cases/form/detail/detail.component.html
@@ -0,0 +1,127 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<form [formGroup]="form" layout="column">
+ <mat-form-field layout-margin>
+ <mat-select placeholder="{{'Choose product' | translate}}" formControlName="productIdentifier">
+ <mat-option *ngFor="let product of products" [value]="product.identifier" matTooltip="{{product.description}}">{{ product.name }}</mat-option>
+ </mat-select>
+ </mat-form-field>
+ <fims-id-input placeholder="{{'Short name' | translate}}" [form]="form" controlName="identifier" [readonly]="editMode"></fims-id-input>
+ <fims-number-input
+ [form]="form"
+ controlName="principalAmount"
+ placeholder="{{'Principal amount' | translate}}"
+ [decimalLimit]="product?.minorCurrencyUnitDigits"
+ hint="{{product?.balanceRange.minimum | number:numberFormat}} - {{product?.balanceRange.maximum | number:numberFormat}}">
+ </fims-number-input>
+ <fims-number-input
+ [form]="form"
+ controlName="interest"
+ placeholder="{{'Interest rate' | translate}}"
+ [decimalLimit]="product?.minorCurrencyUnitDigits"
+ hint="{{product?.interestRange.minimum | number:numberFormat}} - {{product?.interestRange.maximum | number:numberFormat}}">
+ </fims-number-input>
+ <div layout="column" layout-margin>
+ <div layout="row" layout-align="start center">
+ <fims-text-input type="number" [form]="form" controlName="term" placeholder="{{'Term' | translate}}"></fims-text-input>
+ <mat-form-field>
+ <mat-select formControlName="termTemporalUnit">
+ <mat-option *ngFor="let basis of temporalOptions" [value]="basis.type">
+ {{basis.label | translate}}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ </div>
+ <p *ngIf="form.hasError('maxTerm')" class="tc-red-600 text-sm" layout-margin translate>
+ Invalid term. Maximum allowed are {{form.getError('maxTerm').maxWeeks}} week(s), {{form.getError('maxTerm').maxMonths}} month(s) or {{form.getError('maxTerm').maxYears}} year(s).
+ </p>
+ </div>
+ <div layout="column" layout-margin>
+ <div layout="row" layout-align="start center">
+ <fims-text-input type="number" [form]="form" controlName="paymentPeriod" placeholder="{{'Repay every' | translate}}"></fims-text-input>
+ <mat-form-field>
+ <mat-select formControlName="paymentTemporalUnit">
+ <mat-option *ngFor="let alignment of temporalOptions" [value]="alignment.type">
+ {{alignment.label | translate}}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ </div>
+ <p *ngIf="form.hasError('maxPayment')" class="tc-red-600 text-sm" layout-margin translate>
+ Invalid payment period. Maximum allowed are {{form.getError('maxPayment').maxWeeks}} week(s), {{form.getError('maxPayment').maxMonths}} month(s) or {{form.getError('maxPayment').maxYears}} year(s).
+ </p>
+ </div>
+ <!-- Days in weeks -->
+ <div layout="row" *ngIf="displayDaysInWeek">
+ <mat-form-field layout-margin>
+ <mat-select formControlName="dayInWeek" placeholder="{{'on' | translate}}">
+ <mat-option *ngFor="let day of weekDays" [value]="day.type">
+ {{day.label | translate}}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ </div>
+ <!-- Month setting -->
+ <div layout="column" *ngIf="displayMonthSetting">
+ <mat-radio-group formControlName="monthSetting">
+ <div layout="row" layout-align="start center">
+ <mat-radio-button layout-margin value="DAY"></mat-radio-button>
+ <mat-form-field layout-margin>
+ <mat-select formControlName="monthSettingDay" placeholder="{{'on the' | translate}}">
+ <mat-option *ngFor="let day of monthDays" [value]="day.type">
+ {{day.label}}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ <p translate>Day</p>
+ </div>
+ <div layout="row" layout-align="start center">
+ <mat-radio-button layout-margin value="WEEK_AND_DAY"></mat-radio-button>
+ <mat-form-field layout-margin>
+ <mat-select formControlName="monthSettingWeek" placeholder="{{'on the' | translate}}">
+ <mat-option *ngFor="let alignment of weekAlignments" [value]="alignment.type">
+ {{alignment.label | translate}}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ <mat-form-field layout-margin>
+ <mat-select formControlName="monthSettingDayInWeek">
+ <mat-option *ngFor="let weekDay of weekDays" [value]="weekDay.type">
+ {{weekDay.label | translate}}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ </div>
+ </mat-radio-group>
+ </div>
+ <!-- Months -->
+ <mat-form-field layout-margin *ngIf="displayMonths">
+ <mat-select formControlName="month" placeholder="{{'in' | translate}}">
+ <mat-option *ngFor="let month of months" [value]="month.type">
+ {{month.label | translate}}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ <mat-form-field layout-margin>
+ <mat-select formControlName="depositAccountIdentifier" placeholder="{{ 'Deposit account used for fees and disbursal' | translate }}">
+ <mat-option *ngFor="let instance of productInstances" [value]="instance.accountIdentifier">
+ {{instance.accountIdentifier}}({{instance.productIdentifier}})
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+</form>
diff --git a/src/app/customers/cases/form/detail/detail.component.spec.ts b/src/app/customers/cases/form/detail/detail.component.spec.ts
new file mode 100644
index 0000000..17773ca
--- /dev/null
+++ b/src/app/customers/cases/form/detail/detail.component.spec.ts
@@ -0,0 +1,212 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {Component, ViewChild} from '@angular/core';
+import {ReactiveFormsModule} from '@angular/forms';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {CaseDetailFormComponent, DetailFormData} from './detail.component';
+import {FimsSharedModule} from '../../../../common/common.module';
+import {Product} from '../../../../services/portfolio/domain/product.model';
+import {ProductInstance} from '../../../../services/depositAccount/domain/instance/product-instance.model';
+import {MatInputModule, MatOptionModule, MatRadioModule, MatSelectModule, MatTooltipModule} from '@angular/material';
+
+describe('Test case detail form component', () => {
+
+ const products: Product[] = [
+ {
+ identifier: 'productIdentifier',
+ name: 'product',
+ termRange: {
+ temporalUnit: 'WEEKS',
+ maximum: 2
+ },
+ balanceRange: {
+ minimum: 1,
+ maximum: 2
+ },
+ interestRange: {
+ minimum: 1,
+ maximum: 2
+ },
+ interestBasis: 'CURRENT_BALANCE',
+ patternPackage: '',
+ description: '',
+ accountAssignments: [],
+ parameters: null,
+ currencyCode: '',
+ minorCurrencyUnitDigits: 2
+ }
+ ];
+
+ const productInstances: ProductInstance[] = [
+ {
+ customerIdentifier: 'customerIdentifier',
+ accountIdentifier: 'depositAccountIdentifier',
+ productIdentifier: 'productIdentifier'
+ }
+ ];
+
+ const validFormData: DetailFormData = {
+ identifier: 'identifier',
+ productIdentifier: 'productIdentifier',
+ interest: '1.00',
+ principalAmount: '1.00',
+ term: 1,
+ termTemporalUnit: 'WEEKS',
+ paymentTemporalUnit: 'WEEKS',
+ paymentPeriod: 1,
+ paymentAlignmentDay: 1,
+ paymentAlignmentWeek: undefined,
+ paymentAlignmentMonth: undefined,
+ depositAccountIdentifier: 'depositAccountIdentifier'
+ };
+
+ let component: TestComponent;
+ let fixture: ComponentFixture<TestComponent>;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot(),
+ ReactiveFormsModule,
+ MatRadioModule,
+ MatInputModule,
+ MatSelectModule,
+ MatOptionModule,
+ MatTooltipModule,
+ FimsSharedModule,
+ NoopAnimationsModule
+ ],
+ declarations: [
+ TestComponent,
+ CaseDetailFormComponent
+ ]
+ });
+
+ fixture = TestBed.createComponent(TestComponent);
+
+ component = fixture.componentInstance;
+
+ component.products = products;
+ component.productInstances = productInstances;
+ });
+
+ it('should return same form data', () => {
+ component.formData = validFormData;
+ fixture.detectChanges();
+ expect(component.form.formData).toEqual(component.formData);
+ });
+
+ it('should mark form as valid', () => {
+ component.formData = validFormData;
+ fixture.detectChanges();
+ expect(component.form.valid).toBeTruthy();
+ });
+
+ it('should mark form as invalid when principal amount is invalid', () => {
+ component.formData = Object.assign({}, validFormData, {
+ principalAmount: '3'
+ });
+ fixture.detectChanges();
+ expect(component.form.valid).toBeFalsy();
+ });
+
+ it('should mark form as invalid when interest is invalid', () => {
+ component.formData = Object.assign({}, validFormData, {
+ interest: '3'
+ });
+ fixture.detectChanges();
+ expect(component.form.valid).toBeFalsy();
+ });
+
+ it('should mark form as invalid when term range is invalid', () => {
+ component.formData = Object.assign({}, validFormData, {
+ term: 3
+ });
+ fixture.detectChanges();
+ expect(component.form.valid).toBeFalsy();
+ });
+
+ describe('weeks selection', () => {
+ it('should mark form as invalid when no week day is given', () => {
+ component.formData = Object.assign({}, validFormData, {
+ paymentTemporalUnit: 'WEEKS',
+ paymentAlignmentDay: undefined
+ });
+ fixture.detectChanges();
+ expect(component.form.valid).toBeFalsy();
+ });
+ });
+
+ describe('months selection', () => {
+ it('should mark form as invalid when no week day is given on DAY', () => {
+ component.formData = Object.assign({}, validFormData, {
+ paymentTemporalUnit: 'MONTHS',
+ paymentAlignmentDay: undefined,
+ paymentAlignmentWeek: undefined,
+ paymentAlignmentMonth: undefined
+ });
+ fixture.detectChanges();
+ expect(component.form.valid).toBeFalsy();
+ });
+
+ it('should mark form as invalid when no week day is given on WEEK_AND_DAY', () => {
+ component.formData = Object.assign({}, validFormData, {
+ paymentTemporalUnit: 'MONTHS',
+ paymentAlignmentDay: undefined,
+ paymentAlignmentWeek: 1,
+ paymentAlignmentMonth: undefined
+ });
+ fixture.detectChanges();
+ expect(component.form.valid).toBeFalsy();
+ });
+
+ });
+
+ describe('years selection', () => {
+ it('should mark form as invalid when no month is given', () => {
+ component.formData = Object.assign({}, validFormData, {
+ paymentTemporalUnit: 'YEARS',
+ paymentAlignmentDay: 1,
+ paymentAlignmentWeek: 1,
+ paymentAlignmentMonth: undefined,
+ });
+ fixture.detectChanges();
+ expect(component.form.valid).toBeFalsy();
+ });
+ });
+
+});
+
+@Component({
+ template: `
+ <fims-case-detail-form #form [formData]="formData" [products]="products" [productInstances]="productInstances">
+ </fims-case-detail-form>`
+})
+class TestComponent {
+
+ @ViewChild('form') form: CaseDetailFormComponent;
+
+ formData: DetailFormData;
+
+ products: Product[];
+
+ productInstances: ProductInstance[];
+}
diff --git a/src/app/customers/cases/form/detail/detail.component.ts b/src/app/customers/cases/form/detail/detail.component.ts
new file mode 100644
index 0000000..68e8760
--- /dev/null
+++ b/src/app/customers/cases/form/detail/detail.component.ts
@@ -0,0 +1,311 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core';
+import {FormComponent} from '../../../../common/forms/form.component';
+import {FormBuilder, FormControl, Validators} from '@angular/forms';
+import {FimsValidators} from '../../../../common/validator/validators';
+import {ChronoUnit} from '../../../../services/portfolio/domain/chrono-unit.model';
+import {alignmentOptions} from '../../../../common/domain/alignment.model';
+import {weekDayOptions} from '../../../../common/domain/week-days.model';
+import {monthOptions} from '../../../../common/domain/months.model';
+import {temporalOptionList} from '../../../../common/domain/temporal.domain';
+import {Product} from '../../../../services/portfolio/domain/product.model';
+import {ProductInstance} from '../../../../services/depositAccount/domain/instance/product-instance.model';
+import {maxPayment, maxTerm} from './validator/max-term.validators';
+
+export interface DetailFormData {
+ identifier: string;
+ productIdentifier: string;
+ interest: string;
+ principalAmount: string;
+ term: number;
+ termTemporalUnit: ChronoUnit;
+ paymentTemporalUnit: ChronoUnit;
+ paymentPeriod: number;
+ paymentAlignmentDay: number;
+ paymentAlignmentWeek: number;
+ paymentAlignmentMonth: number;
+ depositAccountIdentifier: string;
+}
+
+type MonthSetting = 'DAY' | 'WEEK_AND_DAY';
+
+@Component({
+ selector: 'fims-case-detail-form',
+ templateUrl: './detail.component.html'
+})
+export class CaseDetailFormComponent extends FormComponent<DetailFormData> implements OnInit, OnChanges {
+
+ private _formData: DetailFormData;
+
+ numberFormat = '1.2-2';
+
+ @Input() editMode: boolean;
+
+ @Input() products: Product[];
+
+ @Input() productInstances: ProductInstance[];
+
+ @Input() set formData(formData: DetailFormData) {
+ this._formData = formData;
+ };
+
+ product: Product;
+
+ weekAlignments: any[] = alignmentOptions;
+
+ monthDays: any[] = [];
+
+ weekDays: any[] = weekDayOptions;
+
+ months: any[] = monthOptions;
+
+ temporalOptions = temporalOptionList;
+
+ displayDaysInWeek: boolean;
+
+ displayMonthSetting: boolean;
+
+ displayMonths: boolean;
+
+ constructor(private formBuilder: FormBuilder) {
+ super();
+
+ this.form = this.formBuilder.group({
+ identifier: ['', [Validators.required, Validators.minLength(3), Validators.maxLength(32), FimsValidators.urlSafe]],
+ productIdentifier: ['', [Validators.required]],
+ interest: ['', [Validators.required]],
+ principalAmount: [''],
+ term: ['', [ Validators.required, FimsValidators.minValue(1), FimsValidators.maxScale(0)]],
+ termTemporalUnit: ['', Validators.required],
+ paymentTemporalUnit: ['', [ Validators.required ]],
+ paymentPeriod: ['', [ Validators.required, FimsValidators.minValue(1), FimsValidators.maxScale(0)]],
+
+ dayInWeek: ['', Validators.required],
+
+ monthSetting: ['', Validators.required],
+
+ monthSettingDay: ['', Validators.required],
+ monthSettingDayInWeek: ['', Validators.required],
+ monthSettingWeek: ['', Validators.required],
+
+ month: ['', Validators.required],
+
+ depositAccountIdentifier: ['', Validators.required]
+ });
+
+ this.form.get('productIdentifier').valueChanges
+ .filter(() => !!this.products)
+ .map(identifier => this.products.find(product => product.identifier === identifier))
+ .subscribe(product => this.toggleProduct(product));
+
+ this.form.get('paymentTemporalUnit').valueChanges
+ .subscribe(unit => this.toggleTemporalUnit(unit));
+
+ this.form.get('monthSetting').valueChanges
+ .subscribe(setting => this.toggleMonthSetting(setting));
+ }
+
+ ngOnInit(): void {
+ for (let i = 0; i < 30; i++) {
+ this.monthDays.push({
+ type: i, label: `${i + 1}.`
+ });
+ }
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes.formData) {
+ this.form.reset({
+ identifier: this._formData.identifier,
+ productIdentifier: this._formData.productIdentifier,
+ interest: this._formData.interest,
+ principalAmount: this._formData.principalAmount,
+ term: this._formData.term,
+ termTemporalUnit: this._formData.termTemporalUnit,
+ paymentTemporalUnit: this._formData.paymentTemporalUnit,
+ paymentPeriod: this._formData.paymentPeriod,
+
+ dayInWeek: this._formData.paymentAlignmentDay,
+
+ monthSetting: this._formData.paymentAlignmentWeek != null ? 'WEEK_AND_DAY' : 'DAY',
+
+ monthSettingDay: this._formData.paymentAlignmentDay,
+ monthSettingDayInWeek: this._formData.paymentAlignmentDay,
+ monthSettingWeek: this._formData.paymentAlignmentWeek,
+
+ month: this._formData.paymentAlignmentMonth,
+
+ depositAccountIdentifier: this._formData.depositAccountIdentifier
+ });
+ }
+
+ if (changes.products && changes.products.currentValue && this._formData.productIdentifier) {
+ const foundProduct = this.products.find(product => product.identifier === this._formData.productIdentifier);
+ this.toggleProduct(foundProduct);
+ }
+ }
+
+ private toggleProduct(product: Product) {
+ this.product = product;
+
+ // Override validator with product constraints
+ const principalAmount = this.form.get('principalAmount') as FormControl;
+ this.toggleDisabledState(principalAmount, product.balanceRange.minimum, product.balanceRange.maximum, product.minorCurrencyUnitDigits);
+
+ const interest: FormControl = this.form.get('interest') as FormControl;
+ this.toggleDisabledState(interest, product.interestRange.minimum, product.interestRange.maximum, product.minorCurrencyUnitDigits);
+
+ this.form.setValidators([maxTerm(product.termRange), maxPayment()]);
+ }
+
+ private toggleDisabledState(formControl: FormControl, minimum: number, maximum: number, maxScale: number): void {
+ const hasRange: boolean = minimum !== maximum;
+
+ if (hasRange) {
+ formControl.enable();
+ formControl.setValidators([
+ Validators.required,
+ FimsValidators.minValue(minimum),
+ FimsValidators.maxValue(maximum)
+ ]);
+ } else {
+ formControl.setValue(minimum.toFixed(maxScale));
+ formControl.disable();
+ }
+
+ formControl.updateValueAndValidity();
+ }
+
+ toggleTemporalUnit(chronoUnit: ChronoUnit): void {
+ const dayInWeek = this.form.get('dayInWeek');
+ const month = this.form.get('month');
+
+ if (chronoUnit === 'WEEKS') {
+ this.enableMonthSetting(false);
+ dayInWeek.enable();
+ month.disable();
+
+ this.displayDaysInWeek = true;
+ this.displayMonths = false;
+ } else if (chronoUnit === 'MONTHS') {
+ this.enableMonthSetting(true);
+ dayInWeek.disable();
+ month.disable();
+
+ this.displayDaysInWeek = false;
+ this.displayMonths = false;
+ } else {
+ this.enableMonthSetting(true);
+ dayInWeek.disable();
+ month.enable();
+
+ this.displayDaysInWeek = false;
+ this.displayMonths = true;
+ }
+ }
+
+ enableMonthSetting(enable: boolean): void {
+ const monthSetting = this.form.get('monthSetting');
+ const monthSettingDay = this.form.get('monthSettingDay');
+ const monthSettingDayInWeek = this.form.get('monthSettingDayInWeek');
+ const monthSettingWeek = this.form.get('monthSettingWeek');
+
+ if (enable) {
+ this.displayMonthSetting = true;
+ monthSetting.enable();
+ } else {
+ this.displayMonthSetting = false;
+ monthSetting.disable();
+ monthSettingDay.disable();
+ monthSettingDayInWeek.disable();
+ monthSettingWeek.disable();
+ }
+ }
+
+ toggleMonthSetting(setting: MonthSetting): void {
+ const monthSettingDay = this.form.get('monthSettingDay');
+ const monthSettingDayInWeek = this.form.get('monthSettingDayInWeek');
+ const monthSettingWeek = this.form.get('monthSettingWeek');
+
+ if (setting === 'DAY') {
+ monthSettingDay.enable();
+ monthSettingDayInWeek.disable();
+ monthSettingWeek.disable();
+ } else {
+ monthSettingDay.disable();
+ monthSettingDayInWeek.enable();
+ monthSettingWeek.enable();
+ }
+ }
+
+ get formData(): DetailFormData {
+ const paymentTemporalUnit = this.form.get('paymentTemporalUnit').value;
+ const dayInWeek = this.form.get('dayInWeek').value;
+
+ const monthSetting: MonthSetting = this.form.get('monthSetting').value;
+ const monthSettingDay = this.form.get('monthSettingDay').value;
+ const monthSettingDayInWeek = this.form.get('monthSettingDayInWeek').value;
+ const monthSettingWeek = this.form.get('monthSettingWeek').value;
+ const month = this.form.get('month').value;
+
+ let paymentAlignmentDay: number;
+ let paymentAlignmentWeek: number;
+ let paymentAlignmentMonth: number;
+
+ if (paymentTemporalUnit === 'WEEKS') {
+ paymentAlignmentDay = dayInWeek;
+ }
+
+ if (paymentTemporalUnit === 'MONTHS' || paymentTemporalUnit === 'YEARS') {
+ if (monthSetting === 'DAY') {
+ paymentAlignmentDay = monthSettingDay;
+ } else {
+ paymentAlignmentDay = monthSettingDayInWeek;
+ paymentAlignmentWeek = monthSettingWeek;
+ }
+ }
+
+ if (paymentTemporalUnit === 'YEARS') {
+ paymentAlignmentMonth = month;
+ }
+
+ const formData: DetailFormData = {
+ identifier: this.form.get('identifier').value,
+ productIdentifier: this.form.get('productIdentifier').value,
+ interest: this.form.get('interest').value,
+ principalAmount: this.form.get('principalAmount').value,
+ term: this.form.get('term').value,
+ termTemporalUnit: this.form.get('termTemporalUnit').value,
+ paymentTemporalUnit,
+ paymentPeriod: this.form.get('paymentPeriod').value,
+ paymentAlignmentDay,
+ paymentAlignmentWeek,
+ paymentAlignmentMonth,
+ depositAccountIdentifier: this.form.get('depositAccountIdentifier').value
+ };
+
+ return formData;
+ }
+
+ showIdentifierValidationError(): void {
+ this.setError('identifier', 'unique', true);
+ }
+
+}
diff --git a/src/app/customers/cases/form/detail/validator/max-term.validators.ts b/src/app/customers/cases/form/detail/validator/max-term.validators.ts
new file mode 100644
index 0000000..1d4f98b
--- /dev/null
+++ b/src/app/customers/cases/form/detail/validator/max-term.validators.ts
@@ -0,0 +1,151 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {FormGroup, ValidationErrors, ValidatorFn} from '@angular/forms';
+import {isEmptyInputValue} from '../../../../../common/validator/validators';
+import {TermRange} from '../../../../../services/portfolio/domain/term-range.model';
+import {ChronoUnit} from '../../../../../services/portfolio/domain/chrono-unit.model';
+
+interface MaxValues {
+ maxWeeks: number;
+ maxMonths: number;
+ maxYears: number;
+}
+
+export function maxTerm(termRange: TermRange): ValidatorFn {
+ return (group: FormGroup): ValidationErrors | null => {
+ const term: number = parseInt(group.get('term').value, 10);
+
+ if (isEmptyInputValue(term)) {
+ return null;
+ }
+
+ const maxValues = getMaxValues(termRange.temporalUnit, termRange.maximum);
+
+ const termTemporalUnit = group.get('termTemporalUnit').value;
+
+ if (!isValid(term, termTemporalUnit, maxValues)) {
+ return {
+ maxTerm: {
+ maxWeeks: maxValues.maxWeeks,
+ maxMonths: maxValues.maxMonths,
+ maxYears: maxValues.maxYears
+ },
+ };
+ }
+
+ return null;
+ };
+}
+
+export function maxPayment(): ValidatorFn {
+ return (group: FormGroup): ValidationErrors | null => {
+ const term: number = parseInt(group.get('term').value, 10);
+ const termTemporalUnit = group.get('termTemporalUnit').value;
+
+ const paymentPeriod: number = parseInt(group.get('paymentPeriod').value, 10);
+ const paymentTemporalUnit = group.get('paymentTemporalUnit').value;
+
+ if (isEmptyInputValue(term) || isEmptyInputValue(paymentPeriod)) {
+ return null;
+ }
+
+ const maxValues = getMaxValues(termTemporalUnit, term);
+
+ if (!isValid(paymentPeriod, paymentTemporalUnit, maxValues)) {
+ return {
+ maxPayment: {
+ maxWeeks: maxValues.maxWeeks,
+ maxMonths: maxValues.maxMonths,
+ maxYears: maxValues.maxYears
+ },
+ };
+ }
+
+ return null;
+ };
+}
+
+function isValid(term: number, temporalUnit: ChronoUnit, maxValues: MaxValues): boolean {
+
+ let valid = false;
+
+ switch (temporalUnit) {
+ case 'WEEKS': {
+ valid = term <= maxValues.maxWeeks;
+ break;
+ }
+
+ case 'MONTHS': {
+ valid = term <= maxValues.maxMonths;
+ break;
+ }
+
+ case 'YEARS': {
+ valid = term <= maxValues.maxYears;
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ return valid;
+}
+
+function getMaxValues(temporalUnit: ChronoUnit, maximum: number): MaxValues {
+
+ const weekBase = 52;
+ const monthBase = 12;
+
+ let maxWeeks = 0;
+ let maxMonths = 0;
+ let maxYears = 0;
+
+ switch (temporalUnit) {
+ case 'WEEKS': {
+ maxWeeks = maximum;
+ maxMonths = (maximum * monthBase) / weekBase;
+ maxYears = maximum / weekBase;
+ break;
+ }
+
+ case 'MONTHS': {
+ maxWeeks = (maximum * weekBase) / monthBase;
+ maxMonths = maximum;
+ maxYears = maximum / monthBase;
+ break;
+ }
+
+ case 'YEARS': {
+ maxWeeks = maximum * weekBase;
+ maxMonths = maximum * monthBase;
+ maxYears = maximum;
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ return {
+ maxWeeks: Math.floor(maxWeeks),
+ maxMonths: Math.floor(maxMonths),
+ maxYears: Math.floor(maxYears)
+ };
+}
diff --git a/src/app/customers/cases/form/edit.component.html b/src/app/customers/cases/form/edit.component.html
new file mode 100644
index 0000000..1ecfc6b
--- /dev/null
+++ b/src/app/customers/cases/form/edit.component.html
@@ -0,0 +1,28 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Edit loan for member' | translate:{value: fullName} }}">
+ <fims-case-form-component #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [products]="products$ | async"
+ [productInstances]="productsInstances$ | async"
+ [case]="caseInstance$ | async"
+ [customerId]="(customer$ | async).identifier"
+ [editMode]="true">
+ </fims-case-form-component>
+</fims-layout-card-over>
diff --git a/src/app/customers/cases/form/edit.component.ts b/src/app/customers/cases/form/edit.component.ts
new file mode 100644
index 0000000..05ade01
--- /dev/null
+++ b/src/app/customers/cases/form/edit.component.ts
@@ -0,0 +1,85 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Customer} from '../../../services/customer/domain/customer.model';
+import * as fromCases from '../store/index';
+import {CasesStore} from '../store/index';
+import {UPDATE} from '../store/case.actions';
+import * as fromCustomers from '../../store/index';
+import {Product} from '../../../services/portfolio/domain/product.model';
+import {Observable} from 'rxjs/Observable';
+import {PortfolioService} from '../../../services/portfolio/portfolio.service';
+import {ProductInstance} from '../../../services/depositAccount/domain/instance/product-instance.model';
+import {DepositAccountService} from '../../../services/depositAccount/deposit-account.service';
+import {FimsCase} from '../../../services/portfolio/domain/fims-case.model';
+
+@Component({
+ templateUrl: './edit.component.html'
+})
+export class CaseEditComponent implements OnInit {
+
+ private productId: string;
+
+ fullName: string;
+
+ products$: Observable<Product[]>;
+
+ productsInstances$: Observable<ProductInstance[]>;
+
+ customer$: Observable<Customer>;
+
+ caseInstance$: Observable<FimsCase>;
+
+ constructor(private router: Router, private route: ActivatedRoute, private casesStore: CasesStore,
+ private portfolioService: PortfolioService, private depositService: DepositAccountService) {}
+
+ ngOnInit() {
+ this.route.parent.params.subscribe(params => {
+ this.productId = params['productId'];
+ });
+
+ this.caseInstance$ = this.casesStore.select(fromCases.getSelectedCase)
+ .filter(caseInstance => !!caseInstance);
+
+ this.customer$ = this.casesStore.select(fromCustomers.getSelectedCustomer)
+ .filter(customer => !!customer)
+ .do(customer => this.fullName = `${customer.givenName} ${customer.surname}`);
+
+ this.products$ = this.portfolioService.findAllProducts(false)
+ .map(productPage => productPage.elements);
+
+ this.productsInstances$ = this.customer$
+ .switchMap(customer => this.depositService.fetchProductInstances(customer.identifier))
+ .map((instances: ProductInstance[]) => instances.filter(instance => instance.state === 'ACTIVE'));
+ }
+
+ onSave(caseInstance: FimsCase) {
+ this.casesStore.dispatch({ type: UPDATE, payload: {
+ productId: this.productId,
+ caseInstance,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel() {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+
+}
diff --git a/src/app/customers/cases/form/form.component.html b/src/app/customers/cases/form/form.component.html
new file mode 100644
index 0000000..714bd57
--- /dev/null
+++ b/src/app/customers/cases/form/form.component.html
@@ -0,0 +1,39 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Loan details' | translate}}" [state]="detailFormState">
+ <fims-case-detail-form #detailForm [formData]="detailFormData" [products]="products" [productInstances]="productInstances"></fims-case-detail-form>
+ </td-step>
+ <td-step #debtToIncomeStep label="{{'Debt to income ratio' | translate}}" [state]="debtToIncomeFormState">
+ <fims-case-debt-to-income-form #debtToIncomeForm [formData]="debtToIncomeFormData"></fims-case-debt-to-income-form>
+ </td-step>
+ <td-step #coSignerStep label="{{'Co-signer' | translate}}" [state]="coSignerFormState">
+ <fims-case-co-signer-form #coSignerForm [formData]="coSignerFormData"></fims-case-co-signer-form>
+ </td-step>
+ <td-step label="{{'Final step' | translate}}" [state]="'complete'">
+ <ng-template td-step-summary>
+ <fims-form-final-action
+ [resourceName]="'MEMBER LOAN'"
+ [editMode]="editMode"
+ [disabled]="!isValid"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/customers/cases/form/form.component.ts b/src/app/customers/cases/form/form.component.ts
new file mode 100644
index 0000000..5c7861d
--- /dev/null
+++ b/src/app/customers/cases/form/form.component.ts
@@ -0,0 +1,202 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
+import {TdStepComponent} from '@covalent/core';
+import {CaseParameters} from '../../../services/portfolio/domain/individuallending/case-parameters.model';
+import {CaseDetailFormComponent, DetailFormData} from './detail/detail.component';
+import {CreditWorthinessSnapshot} from '../../../services/portfolio/domain/individuallending/credit-worthiness-snapshot.model';
+import {CaseDebtToIncomeFormComponent, DebtToIncomeFormData} from './debt-to-income/debt-to-income.component';
+import {CaseCoSignerFormComponent, CoSignerFormData} from './co-signer/co-signer.component';
+import {Product} from '../../../services/portfolio/domain/product.model';
+import {ProductInstance} from '../../../services/depositAccount/domain/instance/product-instance.model';
+import {FimsCase} from '../../../services/portfolio/domain/fims-case.model';
+
+@Component({
+ selector: 'fims-case-form-component',
+ templateUrl: './form.component.html'
+})
+export class CaseFormComponent implements OnInit {
+
+ private _caseInstance: FimsCase;
+
+ @ViewChild('detailsStep') detailsStep: TdStepComponent;
+
+ @ViewChild('detailForm') detailForm: CaseDetailFormComponent;
+ detailFormData: DetailFormData;
+
+ @ViewChild('debtToIncomeForm') debtToIncomeForm: CaseDebtToIncomeFormComponent;
+ debtToIncomeFormData: DebtToIncomeFormData;
+
+ @ViewChild('coSignerForm') coSignerForm: CaseCoSignerFormComponent;
+ coSignerFormData: CoSignerFormData;
+
+ @Input('products') products: Product[];
+
+ @Input('productInstances') productInstances: ProductInstance[];
+
+ @Input('editMode') editMode: boolean;
+
+ @Input('customerId') customerId: string;
+
+ @Input('case') set caseInstance(caseInstance: FimsCase) {
+ this._caseInstance = caseInstance;
+
+ this.prepareDetailForm(caseInstance);
+ this.prepareDeptToIncomeForm(caseInstance.parameters.creditWorthinessSnapshots);
+ this.prepareCosignerForm(caseInstance.parameters.creditWorthinessSnapshots);
+ };
+
+ get caseInstance(): FimsCase {
+ return this._caseInstance;
+ }
+
+ @Output('onSave') onSave = new EventEmitter<FimsCase>();
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ constructor() {}
+
+ ngOnInit(): void {
+ this.detailsStep.open();
+ }
+
+ private prepareDetailForm(caseInstance: FimsCase): void {
+ this.detailFormData = {
+ identifier: caseInstance.identifier,
+ productIdentifier: caseInstance.productIdentifier,
+ interest: caseInstance.interest.toFixed(2),
+ principalAmount: caseInstance.parameters.maximumBalance.toFixed(2),
+ term: caseInstance.parameters.termRange.maximum,
+ termTemporalUnit: caseInstance.parameters.termRange.temporalUnit,
+ paymentTemporalUnit: caseInstance.parameters.paymentCycle.temporalUnit,
+ paymentPeriod: caseInstance.parameters.paymentCycle.period,
+ paymentAlignmentDay: caseInstance.parameters.paymentCycle.alignmentDay,
+ paymentAlignmentWeek: caseInstance.parameters.paymentCycle.alignmentWeek,
+ paymentAlignmentMonth: caseInstance.parameters.paymentCycle.alignmentMonth,
+ depositAccountIdentifier: caseInstance.depositAccountIdentifier
+ };
+ }
+
+ private prepareDeptToIncomeForm(snapshots: CreditWorthinessSnapshot[]): void {
+ const foundSnapshot: CreditWorthinessSnapshot = snapshots.find(snapshot => snapshot.forCustomer === this.customerId);
+ if (foundSnapshot) {
+ this.debtToIncomeFormData = {
+ incomeSources: foundSnapshot.incomeSources,
+ debts: foundSnapshot.debts
+ };
+ } else {
+ this.debtToIncomeFormData = {
+ incomeSources: [],
+ debts: []
+ };
+ }
+ }
+
+ private prepareCosignerForm(snapshots: CreditWorthinessSnapshot[]): void {
+ const foundSnapshot: CreditWorthinessSnapshot = snapshots.find(snapshot => snapshot.forCustomer !== this.customerId);
+ if (foundSnapshot) {
+ this.coSignerFormData = {
+ customerId: foundSnapshot.forCustomer,
+ incomeSources: foundSnapshot.incomeSources,
+ debts: foundSnapshot.debts
+ };
+ } else {
+ this.coSignerFormData = {
+ customerId: null,
+ incomeSources: [],
+ debts: []
+ };
+ }
+ }
+
+ get isValid(): boolean {
+ return this.detailForm.valid &&
+ this.debtToIncomeForm.valid &&
+ this.coSignerForm.valid;
+ }
+
+ save(): void {
+ const customerSnapshot: CreditWorthinessSnapshot = {
+ forCustomer: this.customerId,
+ incomeSources: this.debtToIncomeForm.formData.incomeSources,
+ debts: this.debtToIncomeForm.formData.debts,
+ assets: []
+ };
+
+ const cosignerSnapshot: CreditWorthinessSnapshot = {
+ forCustomer: this.coSignerForm.formData.customerId,
+ incomeSources: this.coSignerForm.formData.incomeSources,
+ debts: this.coSignerForm.formData.debts,
+ assets: []
+ };
+
+ const creditWorthinessSnapshots = [customerSnapshot];
+ if (cosignerSnapshot.forCustomer) {
+ creditWorthinessSnapshots.push(cosignerSnapshot);
+ }
+
+ const caseParameters: CaseParameters = {
+ customerIdentifier: this.customerId,
+ maximumBalance: parseFloat(this.detailForm.formData.principalAmount),
+ paymentCycle: {
+ alignmentDay: this.detailForm.formData.paymentAlignmentDay,
+ alignmentMonth: this.detailForm.formData.paymentAlignmentMonth,
+ alignmentWeek: this.detailForm.formData.paymentAlignmentWeek,
+ period: this.detailForm.formData.paymentPeriod,
+ temporalUnit: this.detailForm.formData.paymentTemporalUnit
+ },
+ termRange: {
+ temporalUnit: this.detailForm.formData.termTemporalUnit,
+ maximum: this.detailForm.formData.term
+ },
+ creditWorthinessSnapshots
+ };
+
+ const caseToSave: FimsCase = {
+ currentState: this.caseInstance.currentState,
+ identifier: this.detailForm.formData.identifier,
+ productIdentifier: this.detailForm.formData.productIdentifier,
+ interest: parseFloat(this.detailForm.formData.interest),
+ parameters: caseParameters,
+ depositAccountIdentifier: this.detailForm.formData.depositAccountIdentifier
+ };
+
+ this.onSave.emit(caseToSave);
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+ get detailFormState(): string {
+ return this.detailForm.valid ? 'complete' : this.detailForm.pristine ? 'none' : 'required';
+ }
+
+ get debtToIncomeFormState(): string {
+ return this.debtToIncomeForm.valid ? 'complete' : this.debtToIncomeForm.pristine ? 'none' : 'required';
+ }
+
+ get coSignerFormState(): string {
+ return this.coSignerForm.valid ? 'complete' : this.coSignerForm.pristine ? 'none' : 'required';
+ }
+
+ showIdentifierValidationError(): void {
+ this.detailForm.showIdentifierValidationError();
+ this.detailsStep.open();
+ }
+}
diff --git a/src/app/customers/cases/payment-cycle/payment-cycle.component.html b/src/app/customers/cases/payment-cycle/payment-cycle.component.html
new file mode 100644
index 0000000..d7dee2f
--- /dev/null
+++ b/src/app/customers/cases/payment-cycle/payment-cycle.component.html
@@ -0,0 +1,33 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<mat-list-item>
+ <h3 matLine translate>Payment cycle</h3>
+ <p matLine>Repay every {{paymentCycle.period + ' ' + getTemporalOption(paymentCycle.temporalUnit)?.label}}</p>
+ <p matLine *ngIf="paymentCycle.temporalUnit === 'WEEKS'">
+ on {{getWeekDayOption(paymentCycle.alignmentDay)?.label}}
+ </p>
+ <p matLine *ngIf="paymentCycle.temporalUnit !== 'WEEKS' && alignmentDaySetting === 'fixed'">
+ on the {{paymentCycle.alignmentDay +1}}. day
+ </p>
+ <p matLine *ngIf="paymentCycle.temporalUnit !== 'WEEKS' && alignmentDaySetting === 'relative'">
+ on the {{getAlignment(paymentCycle.alignmentWeek)?.label + ' ' + getWeekDayOption(paymentCycle.alignmentDay)?.label}}
+ </p>
+ <p matLine *ngIf="paymentCycle.temporalUnit === 'YEARS'">
+ in {{getMonthOption(paymentCycle.alignmentMonth)?.label}}
+ </p>
+</mat-list-item>
diff --git a/src/app/customers/cases/payment-cycle/payment-cycle.component.ts b/src/app/customers/cases/payment-cycle/payment-cycle.component.ts
new file mode 100644
index 0000000..c9b78ea
--- /dev/null
+++ b/src/app/customers/cases/payment-cycle/payment-cycle.component.ts
@@ -0,0 +1,53 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input} from '@angular/core';
+import {temporalOptionList} from '../../../common/domain/temporal.domain';
+import {weekDayOptions} from '../../../common/domain/week-days.model';
+import {alignmentOptions} from '../../../common/domain/alignment.model';
+import {PaymentCycle} from '../../../services/portfolio/domain/payment-cycle.model';
+import {monthOptions} from '../../../common/domain/months.model';
+
+@Component({
+ selector: 'fims-case-detail-payment-cycle',
+ templateUrl: './payment-cycle.component.html'
+})
+export class CaseDetailPaymentCycleComponent {
+
+ @Input() paymentCycle: PaymentCycle;
+
+ get alignmentDaySetting(): string {
+ return this.paymentCycle.alignmentWeek ? 'relative' : 'fixed';
+ }
+
+ getAlignment(id: number): any {
+ return alignmentOptions.find(alignment => alignment.type === id);
+ }
+
+ getWeekDayOption(id: number): any {
+ return weekDayOptions.find(weekDayOption => weekDayOption.type === id);
+ }
+
+ getMonthOption(id: number): any {
+ return monthOptions.find(monthOption => monthOption.type === id);
+ }
+
+ getTemporalOption(id: string): any {
+ return temporalOptionList.find(monthOption => monthOption.type === id);
+ }
+}
diff --git a/src/app/customers/cases/payments/payments.component.html b/src/app/customers/cases/payments/payments.component.html
new file mode 100644
index 0000000..de52528
--- /dev/null
+++ b/src/app/customers/cases/payments/payments.component.html
@@ -0,0 +1,69 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Planned payments' | translate}}" [navigateBackTo]="['../']">
+ <div layout="row" layout-margin>
+ <mat-form-field layout-margin>
+ <input matInput type="date" placeholder="{{'Start date' | translate}}" [(ngModel)]="startDate">
+ </mat-form-field>
+ <span>
+ <button layout-margin mat-button mat-icon-button (click)="fetchPayments(startDate)"><mat-icon>search</mat-icon></button>
+ </span>
+ </div>
+ <div layout="row" class="mat-content">
+ <table td-data-table>
+ <thead>
+ <tr td-data-table-column-row>
+ <th td-data-table-column [active]="true" translate>
+ Date
+ </th>
+ <th td-data-table-column [active]="true" [numeric]="true" translate>
+ Payment
+ </th>
+ <th td-data-table-column [active]="true" [numeric]="true" translate>
+ Interest
+ </th>
+ <th td-data-table-column [active]="true" [numeric]="true" translate>
+ Principal
+ </th>
+ <th td-data-table-column [active]="true" [numeric]="true" translate>
+ Balance
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr td-data-table-row *ngFor="let row of (rows | async)">
+ <td td-data-table-cell>
+ {{row.date | date}}
+ </td>
+ <td td-data-table-cell [numeric]="true">
+ {{row.payment | number}}
+ </td>
+ <td td-data-table-cell [numeric]="true">
+ {{row.interest | number}}
+ </td>
+ <td td-data-table-cell [numeric]="true">
+ {{row.principal | number}}
+ </td>
+ <td td-data-table-cell [numeric]="true">
+ {{row.balance | number}}
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+</fims-layout-card-over>
diff --git a/src/app/customers/cases/payments/payments.component.ts b/src/app/customers/cases/payments/payments.component.ts
new file mode 100644
index 0000000..19cc421
--- /dev/null
+++ b/src/app/customers/cases/payments/payments.component.ts
@@ -0,0 +1,125 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {PlannedPaymentPage} from '../../../services/portfolio/domain/individuallending/planned-payment-page.model';
+import * as fromCases from '../store/index';
+import {CasesStore} from '../store/index';
+import {Observable} from 'rxjs/Observable';
+import {Subscription} from 'rxjs/Subscription';
+import {SEARCH} from '../store/payments/payment.actions';
+import {PlannedPayment} from '../../../services/portfolio/domain/individuallending/planned-payment.model';
+import {CostComponent} from '../../../services/portfolio/domain/cost-component.model';
+import {ChargeName} from '../../../services/portfolio/domain/individuallending/charge-name.model';
+import {todayAsISOString} from '../../../services/domain/date.converter';
+import {FimsCase} from '../../../services/portfolio/domain/fims-case.model';
+
+interface CostComponents {
+ [id: string]: CostComponent;
+}
+
+interface PaymentRow {
+ date?: string;
+ payment?: number;
+ interest?: number;
+ principal?: number;
+ balance: number;
+}
+
+@Component({
+ templateUrl: './payments.component.html'
+})
+export class CasePaymentsComponent implements OnInit, OnDestroy {
+
+ private caseSubscription: Subscription;
+
+ startDate: string = todayAsISOString();
+
+ caseInstance: FimsCase;
+
+ rows: Observable<PaymentRow[]>;
+
+ columns: Observable<ChargeName[]>;
+
+ constructor(private casesStore: CasesStore) {}
+
+ private createRows(plannedPayments: PlannedPayment[]): PaymentRow[] {
+ const rows: PaymentRow[] = [];
+
+ plannedPayments.forEach((plannedPayment, index) => {
+ const interest = this.getChargeAmount(plannedPayment.payment.costComponents, 'repay-interest');
+ const principal = this.getChargeAmount(plannedPayment.payment.costComponents, 'repay-principal');
+ const payment = plannedPayment.payment.balanceAdjustments.ey * -1;
+ const balance = plannedPayment.balances.clp;
+
+ if (index === 0) {
+ rows.push({
+ balance
+ });
+
+ return;
+ }
+
+ rows.push({
+ date: plannedPayment.payment.date,
+ payment,
+ interest,
+ principal,
+ balance
+ });
+ });
+
+ return rows;
+ }
+
+ private getChargeAmount(costComponents: CostComponent[], chargeIdentifier: string): number {
+ const foundComponent = costComponents.find(component => component.chargeIdentifier === chargeIdentifier);
+
+ if (foundComponent) {
+ return foundComponent.amount;
+ }
+
+ return 0;
+ };
+
+ ngOnInit(): void {
+ this.columns = this.casesStore.select(fromCases.getSearchCasePaymentPage)
+ .map((page: PlannedPaymentPage) => page.chargeNames);
+
+ this.rows = this.casesStore.select(fromCases.getSearchCasePaymentPage)
+ .map((page: PlannedPaymentPage) => this.createRows(page.elements));
+
+ this.caseSubscription = this.casesStore.select(fromCases.getSelectedCase)
+ .subscribe(caseInstance => {
+ this.caseInstance = caseInstance;
+ this.fetchPayments();
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.caseSubscription.unsubscribe();
+ }
+
+ fetchPayments(startDate?: string): void {
+ this.casesStore.dispatch({ type: SEARCH, payload: {
+ productIdentifier: this.caseInstance.productIdentifier,
+ caseIdentifier: this.caseInstance.identifier,
+ initialDisbursalDate: startDate
+ }});
+ }
+}
diff --git a/src/app/customers/cases/status/command.component.html b/src/app/customers/cases/status/command.component.html
new file mode 100644
index 0000000..b6ba481
--- /dev/null
+++ b/src/app/customers/cases/status/command.component.html
@@ -0,0 +1,29 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-case-tasks
+ [caseCreator]="caseCreator"
+ [currentUser]="currentUser"
+ [action]="statusCommand.action"
+ [productId]="productId"
+ [caseId]="caseId"
+ [tasks]="statusCommand.tasks"
+ (onExecuteTask)="executeTask($event)">
+</fims-case-tasks>
+<div layout="row" layout-align="end center" layout-margin>
+ <button mat-raised-button style="margin-left: 10px;" color="accent" (click)="executeCommand()" [disabled]="disabled">{{statusCommand.action}}</button>
+</div>
diff --git a/src/app/customers/cases/status/command.component.ts b/src/app/customers/cases/status/command.component.ts
new file mode 100644
index 0000000..a1ce1ca
--- /dev/null
+++ b/src/app/customers/cases/status/command.component.ts
@@ -0,0 +1,62 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
+import {StatusCommand} from '../store/model/fims-command.model';
+import {ExecuteTaskEvent} from './tasks.component';
+import {WorkflowAction} from '../../../services/portfolio/domain/individuallending/workflow-action.model';
+
+@Component({
+ selector: 'fims-case-command',
+ templateUrl: './command.component.html'
+})
+
+export class CaseCommandComponent implements OnInit {
+
+ @Input() caseId: string;
+
+ @Input() caseCreator: string;
+
+ @Input() currentUser: string;
+
+ @Input() productId: string;
+
+ @Input() statusCommand: StatusCommand;
+
+ @Output() onExecuteTask = new EventEmitter<ExecuteTaskEvent>();
+
+ @Output() onExecuteCommand = new EventEmitter<WorkflowAction>();
+
+ constructor() {
+ }
+
+ ngOnInit() {}
+
+ executeTask(event: ExecuteTaskEvent): void {
+ this.onExecuteTask.emit(event);
+ }
+
+ executeCommand(): void {
+ this.onExecuteCommand.emit(this.statusCommand.action);
+ }
+
+ get disabled(): boolean {
+ const notExecuted = this.statusCommand.tasks.filter(task => task.taskDefinition.mandatory && !task.executedOn);
+ return notExecuted.length > 0;
+ }
+}
diff --git a/src/app/customers/cases/status/confirmation/confirmation.component.html b/src/app/customers/cases/status/confirmation/confirmation.component.html
new file mode 100644
index 0000000..4b52ffd
--- /dev/null
+++ b/src/app/customers/cases/status/confirmation/confirmation.component.html
@@ -0,0 +1,26 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{ 'Confirm action' | translate }}">
+ <fims-case-command-confirmation-form
+ [fimsCase]="fimsCase$ | async"
+ [fees]="fees$ | async"
+ [action]="(params$ | async)?.action"
+ (onExecuteCommand)="executeCommand($event)"
+ (onCancel)="cancel()">
+ </fims-case-command-confirmation-form>
+</fims-layout-card-over>
diff --git a/src/app/customers/cases/status/confirmation/confirmation.component.ts b/src/app/customers/cases/status/confirmation/confirmation.component.ts
new file mode 100644
index 0000000..e5f1820
--- /dev/null
+++ b/src/app/customers/cases/status/confirmation/confirmation.component.ts
@@ -0,0 +1,95 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {ActivatedRoute, Router} from '@angular/router';
+import * as fromCases from '../../store/index';
+import {CasesStore} from '../../store/index';
+import {EXECUTE_COMMAND} from '../../store/case.actions';
+import {ExecuteCommandEvent} from './form.component';
+import {CaseCommand} from '../../../../services/portfolio/domain/case-command.model';
+import {FimsCase} from '../../../../services/portfolio/domain/fims-case.model';
+import {Fee} from '../services/domain/fee.model';
+import {FeeService} from '../services/fee.service';
+
+interface Parameter {
+ productId: string;
+ caseId: string;
+ action: string;
+}
+
+@Component({
+ templateUrl: './confirmation.component.html'
+})
+export class CaseCommandConfirmationComponent implements OnInit {
+
+ fimsCase$: Observable<FimsCase>;
+
+ fees$: Observable<Fee[]>;
+
+ params$: Observable<Parameter>;
+
+ constructor(private router: Router, private route: ActivatedRoute, private casesStore: CasesStore,
+ private feeService: FeeService) {}
+
+ ngOnInit() {
+ const parentParams$ = this.route.parent.params
+ .map(params => ({
+ caseId: params['caseId'],
+ productId: params['productId']
+ }));
+
+ this.params$ = this.route.params
+ .map(params => ({
+ action: params['action']
+ }));
+
+ this.fees$ = Observable.combineLatest(
+ parentParams$,
+ this.params$,
+ (parentParams, params) => ({
+ productId: parentParams.productId,
+ caseId: parentParams.caseId,
+ action: params.action
+ }))
+ .switchMap(params => this.feeService.getFees(params.productId, params.caseId, params.action));
+
+ this.fimsCase$ = this.casesStore.select(fromCases.getSelectedCase);
+ }
+
+ executeCommand(event: ExecuteCommandEvent): void {
+ const command: CaseCommand = {
+ note: event.note,
+ paymentSize: event.paymentSize,
+ createdOn: event.createdOn
+ };
+
+ this.casesStore.dispatch({ type: EXECUTE_COMMAND, payload: {
+ productId: event.productId,
+ caseId: event.caseId,
+ action: event.action,
+ command: command,
+ activatedRoute: this.route
+ } });
+ }
+
+ cancel(): void {
+ this.router.navigate(['../../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/customers/cases/status/confirmation/form.component.html b/src/app/customers/cases/status/confirmation/form.component.html
new file mode 100644
index 0000000..f240562
--- /dev/null
+++ b/src/app/customers/cases/status/confirmation/form.component.html
@@ -0,0 +1,69 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #confirmationStep label="{{'Confirmation' | translate}}" [active]="true">
+ <div layout="row" [formGroup]="formGroup">
+ <mat-form-field layout-margin flex>
+ <textarea matInput placeholder="{{'Note(optional)' | translate}}" formControlName="note"></textarea>
+ </mat-form-field>
+ </div>
+ <div layout-gt-xs="column">
+ <h4 translate>Principal amount: {{fimsCase?.parameters?.maximumBalance | number:numberFormat}}</h4>
+ <h3 translate>Costs</h3>
+ <table td-data-table>
+ <thead>
+ <tr td-data-table-column-row>
+ <th td-data-table-column>
+ <span translate>Name</span>
+ </th>
+ <th td-data-table-column>
+ <span translate>Description</span>
+ </th>
+ <th td-data-table-column>
+ <span translate>Amount</span>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr td-data-table-row *ngFor="let row of fees">
+ <td td-data-table-cell>
+ {{row['name']}}
+ </td>
+ <td td-data-table-cell>
+ {{row['description']}}
+ </td>
+ <td td-data-table-cell>
+ {{row['amount'] | number:numberFormat}}
+ </td>
+ </tr>
+ <tr td-data-table-row>
+ <td td-data-table-cell translate colspan="2">Total</td>
+ <td td-data-table-cell>{{totalAmount | number:numberFormat}}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </td-step>
+ <td-step label="{{'Final step' | translate}}" [state]="'complete'">
+ <ng-template td-step-summary>
+ <button mat-raised-button color="primary" (click)="executeCommand()">{{action | translate}}</button>
+ <span flex></span>
+ <button mat-button (click)="cancel()">{{'CANCEL' | translate}}</button>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/customers/cases/status/confirmation/form.component.ts b/src/app/customers/cases/status/confirmation/form.component.ts
new file mode 100644
index 0000000..95a3b75
--- /dev/null
+++ b/src/app/customers/cases/status/confirmation/form.component.ts
@@ -0,0 +1,83 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
+import {WorkflowAction} from '../../../../services/portfolio/domain/individuallending/workflow-action.model';
+import {FormBuilder, FormGroup} from '@angular/forms';
+import {FimsCase} from '../../../../services/portfolio/domain/fims-case.model';
+import {Fee} from '../services/domain/fee.model';
+
+export interface ExecuteCommandEvent {
+ productId: string;
+ caseId: string;
+ action: WorkflowAction;
+ note: string;
+ paymentSize: number;
+ createdOn: string;
+}
+
+@Component({
+ selector: 'fims-case-command-confirmation-form',
+ templateUrl: './form.component.html'
+})
+export class CaseCommandConfirmationFormComponent implements OnInit {
+
+ numberFormat = '2.2-2';
+
+ formGroup: FormGroup;
+
+ @Input() fees: Fee[];
+
+ @Input() action: WorkflowAction;
+
+ @Input() fimsCase: FimsCase;
+
+ @Output() onExecuteCommand = new EventEmitter<ExecuteCommandEvent>();
+
+ @Output() onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder) {}
+
+ ngOnInit() {
+ this.formGroup = this.formBuilder.group({
+ note: ['']
+ });
+ }
+
+ get totalAmount(): number {
+ if (!this.fees) {
+ return 0;
+ }
+ return this.fees.reduce((acc, val) => acc + val.amount, 0);
+ }
+
+ executeCommand(): void {
+ this.onExecuteCommand.emit({
+ caseId: this.fimsCase.identifier,
+ productId: this.fimsCase.productIdentifier,
+ note: this.formGroup.get('note').value,
+ action: this.action,
+ paymentSize: this.action === 'DISBURSE' ? this.fimsCase.parameters.maximumBalance : 0,
+ createdOn: new Date().toISOString()
+ });
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+}
diff --git a/src/app/customers/cases/status/services/domain/fee.model.ts b/src/app/customers/cases/status/services/domain/fee.model.ts
new file mode 100644
index 0000000..1327ab1
--- /dev/null
+++ b/src/app/customers/cases/status/services/domain/fee.model.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface Fee {
+ name: string;
+ description: string;
+ amount: number;
+}
diff --git a/src/app/customers/cases/status/services/fee.service.ts b/src/app/customers/cases/status/services/fee.service.ts
new file mode 100644
index 0000000..2ec5db5
--- /dev/null
+++ b/src/app/customers/cases/status/services/fee.service.ts
@@ -0,0 +1,58 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {PortfolioService} from '../../../../services/portfolio/portfolio.service';
+import {Payment} from '../../../../services/portfolio/domain/payment.model';
+import {ChargeDefinition} from '../../../../services/portfolio/domain/charge-definition.model';
+import {Fee} from './domain/fee.model';
+
+@Injectable()
+export class FeeService {
+
+ constructor(private portfolioService: PortfolioService) {}
+
+ getFees(productIdentifier: string, caseIdentifier: string, action: string): Observable<Fee[]> {
+ return Observable.combineLatest(
+ this.portfolioService.getCostComponentsForAction(productIdentifier, caseIdentifier, action),
+ this.portfolioService.findAllChargeDefinitionsForProduct(productIdentifier),
+ (payment, chargeDefinitions) => ({
+ payment,
+ chargeDefinitions
+ }))
+ .map(result => this.map(result.payment, result.chargeDefinitions));
+ }
+
+ private map(payment: Payment, chargeDefinitions: ChargeDefinition[]): Fee[] {
+ return payment.costComponents.reduce((fees, component) => {
+ const foundDefinition = chargeDefinitions.find(definition => definition.identifier === component.chargeIdentifier);
+
+ if (foundDefinition) {
+ return fees.concat({
+ name: foundDefinition.name,
+ description: foundDefinition.description,
+ amount: component.amount
+ });
+ }
+
+ return fees;
+ }, []);
+ }
+
+}
diff --git a/src/app/customers/cases/status/status.component.html b/src/app/customers/cases/status/status.component.html
new file mode 100644
index 0000000..2626f4e
--- /dev/null
+++ b/src/app/customers/cases/status/status.component.html
@@ -0,0 +1,37 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Tasks' | translate}}" *ngIf="caseInstance$ | async as caseInstance" [navigateBackTo]="['../']">
+ <div layout="row" layout-align="start start" layout-margin>
+ <div layout="column" flex="100">
+ <div layout="row" layout-align="start start" *ngFor="let command of statusCommands$ | async">
+ <div layout="column" flex="100" *ngIf="command.preStates.indexOf(caseInstance.currentState) > -1">
+ <fims-case-command
+ [caseCreator]="caseInstance.createdBy"
+ [currentUser]="currentUser$ | async"
+ [productId]="productId$ | async"
+ [caseId]="caseInstance.identifier"
+ [statusCommand]="command"
+ (onExecuteTask)="executeTask($event)"
+ (onExecuteCommand)="executeCommand($event)">
+ </fims-case-command>
+ </div>
+ <mat-divider></mat-divider>
+ </div>
+ </div>
+ </div>
+</fims-layout-card-over>
diff --git a/src/app/customers/cases/status/status.component.ts b/src/app/customers/cases/status/status.component.ts
new file mode 100644
index 0000000..863be34
--- /dev/null
+++ b/src/app/customers/cases/status/status.component.ts
@@ -0,0 +1,89 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {Subscription} from 'rxjs/Subscription';
+import {ActivatedRoute, Router} from '@angular/router';
+import * as fromCases from '../store/index';
+import {CasesStore} from '../store/index';
+import * as fromRoot from '../../../store';
+import {Observable} from 'rxjs/Observable';
+import {EXECUTE_TASK, LoadAllAction} from '../store/tasks/task.actions';
+import {ExecuteTaskEvent} from './tasks.component';
+import {StatusCommand} from '../store/model/fims-command.model';
+import {WorkflowAction} from '../../../services/portfolio/domain/individuallending/workflow-action.model';
+import {FimsCase} from '../../../services/portfolio/domain/fims-case.model';
+
+@Component({
+ templateUrl: './status.component.html'
+})
+export class CaseStatusComponent implements OnInit, OnDestroy {
+
+ private actionSubscription: Subscription;
+
+ currentUser$: Observable<string>;
+
+ productId$: Observable<string>;
+
+ caseInstance$: Observable<FimsCase>;
+
+ statusCommands$: Observable<StatusCommand[]>;
+
+ constructor(private router: Router, private route: ActivatedRoute, private casesStore: CasesStore) {}
+
+ ngOnInit(): void {
+ this.productId$ = this.route.parent.params
+ .map(params => params['productId']);
+
+ this.currentUser$ = this.casesStore.select(fromRoot.getUsername);
+
+ this.caseInstance$ = this.casesStore.select(fromCases.getSelectedCase);
+
+ this.statusCommands$ = this.casesStore.select(fromCases.getCaseCommands);
+
+ this.actionSubscription = Observable.combineLatest(
+ this.productId$,
+ this.caseInstance$,
+ (productId, caseInstance) => ({
+ productId,
+ caseId: caseInstance.identifier
+ })
+ ).map(({ productId, caseId }) => new LoadAllAction({
+ productId,
+ caseId
+ })).subscribe(this.casesStore);
+ }
+
+ ngOnDestroy(): void {
+ this.actionSubscription.unsubscribe();
+ }
+
+ executeCommand(action: WorkflowAction): void {
+ this.router.navigate([action, 'confirmation'], {
+ relativeTo: this.route
+ });
+ }
+
+ executeTask(event: ExecuteTaskEvent): void {
+ this.casesStore.dispatch({
+ type: EXECUTE_TASK,
+ payload: event
+ });
+ }
+
+}
diff --git a/src/app/customers/cases/status/task.component.html b/src/app/customers/cases/status/task.component.html
new file mode 100644
index 0000000..4585ed4
--- /dev/null
+++ b/src/app/customers/cases/status/task.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<mat-list-item [matTooltip]="disabled ? 'This is a four eyes task and must be executed by somebody else than you.' : ''" [matTooltipPosition]="'after'">
+ <mat-icon matListAvatar *ngIf="task.taskDefinition.fourEyes">people</mat-icon>
+ <mat-icon matListAvatar *ngIf="!task.taskDefinition.fourEyes">person</mat-icon>
+ <h3 matLine>{{task.taskDefinition.name}}</h3>
+ <h4 matLine>{{task.taskDefinition.description}}</h4>
+ <p matLine>{{task.executedBy}}, {{task.executedOn | date:'medium'}}</p>
+ <mat-checkbox class="list-checkbox" title="{{'Execute task' | translate}}" [checked]="!!task.executedOn" (change)="selectTask($event)" [disabled]="disabled"></mat-checkbox>
+</mat-list-item>
diff --git a/src/app/customers/cases/status/task.component.ts b/src/app/customers/cases/status/task.component.ts
new file mode 100644
index 0000000..c55e296
--- /dev/null
+++ b/src/app/customers/cases/status/task.component.ts
@@ -0,0 +1,50 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
+import {FimsTaskInstance} from '../store/model/fims-task-instance.model';
+import {MatCheckboxChange} from '@angular/material';
+
+export interface SelectTaskEvent {
+ taskIdentifier: string;
+ checked: boolean;
+}
+
+@Component({
+ selector: 'fims-case-task',
+ templateUrl: './task.component.html'
+})
+export class CaseTaskComponent implements OnInit {
+
+ @Input() task: FimsTaskInstance;
+
+ @Input() disabled: boolean;
+
+ @Output() onSelectTask = new EventEmitter<SelectTaskEvent>();
+
+ constructor() {}
+
+ ngOnInit() {}
+
+ selectTask(change: MatCheckboxChange): void {
+ this.onSelectTask.emit({
+ taskIdentifier: this.task.taskDefinition.identifier,
+ checked: change.checked
+ });
+ }
+}
diff --git a/src/app/customers/cases/status/tasks.component.html b/src/app/customers/cases/status/tasks.component.html
new file mode 100644
index 0000000..73764bd
--- /dev/null
+++ b/src/app/customers/cases/status/tasks.component.html
@@ -0,0 +1,28 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<mat-list *ngIf="tasks?.length > 0">
+ <ng-container *ngIf="mandatoryTasks && mandatoryTasks.length > 0">
+ <h3 mat-subheader>Mandatory tasks(These tasks must be executed)</h3>
+ <fims-case-task *ngFor="let task of mandatoryTasks" [task]="task" [disabled]="taskDisabled(task)" (onSelectTask)="executeTask($event)"></fims-case-task>
+ </ng-container>
+
+ <ng-container *ngIf="optionalTasks && optionalTasks.length > 0">
+ <h3 mat-subheader translate>Optional tasks</h3>
+ <fims-case-task *ngFor="let task of optionalTasks" [task]="task" [disabled]="taskDisabled(task)" (onSelectTask)="executeTask($event)"></fims-case-task>
+ </ng-container>
+</mat-list>
diff --git a/src/app/customers/cases/status/tasks.component.ts b/src/app/customers/cases/status/tasks.component.ts
new file mode 100644
index 0000000..29ce17e
--- /dev/null
+++ b/src/app/customers/cases/status/tasks.component.ts
@@ -0,0 +1,82 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
+import {FimsTaskInstance} from '../store/model/fims-task-instance.model';
+import {SelectTaskEvent} from './task.component';
+
+export interface ExecuteTaskEvent {
+ action: string;
+ productIdentifier: string;
+ caseIdentifier: string;
+ taskIdentifier: string;
+ executedBy: string;
+ executed: boolean;
+}
+
+@Component({
+ selector: 'fims-case-tasks',
+ templateUrl: './tasks.component.html'
+})
+export class CaseTasksComponent implements OnInit {
+
+ @Input() tasks: FimsTaskInstance[];
+
+ @Input() action: string;
+
+ @Input() productId: string;
+
+ @Input() caseId: string;
+
+ @Input() caseCreator: string;
+
+ @Input() currentUser: string;
+
+ @Output() onExecuteTask = new EventEmitter<ExecuteTaskEvent>();
+
+ constructor() {}
+
+ ngOnInit() {}
+
+ executeTask(event: SelectTaskEvent): void {
+ this.onExecuteTask.emit({
+ action: this.action,
+ productIdentifier: this.productId,
+ caseIdentifier: this.caseId,
+ taskIdentifier: event.taskIdentifier,
+ executedBy: this.currentUser,
+ executed: event.checked
+ });
+ }
+
+ taskDisabled(task: FimsTaskInstance): boolean {
+ return task.taskDefinition.fourEyes ? this.caseCreator === this.currentUser : false;
+ }
+
+ get mandatoryTasks(): FimsTaskInstance[] {
+ return this.tasks.filter(task => task.taskDefinition.mandatory);
+ }
+
+ get optionalTasks(): FimsTaskInstance[] {
+ return this.tasks.filter(task => !task.taskDefinition.mandatory);
+ }
+
+ get sameUser(): boolean {
+ return this.caseCreator === this.currentUser;
+ }
+}
diff --git a/src/app/customers/cases/store/case.actions.ts b/src/app/customers/cases/store/case.actions.ts
new file mode 100644
index 0000000..638601b
--- /dev/null
+++ b/src/app/customers/cases/store/case.actions.ts
@@ -0,0 +1,214 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Action} from '@ngrx/store';
+import {Error} from '../../../services/domain/error.model';
+import {type} from '../../../store/util';
+import {RoutePayload} from '../../../common/store/route-payload';
+import {FetchRequest} from '../../../services/domain/paging/fetch-request.model';
+import {Product} from '../../../services/portfolio/domain/product.model';
+import {SearchResult} from '../../../common/store/search.reducer';
+import {
+ CreateResourceSuccessPayload,
+ LoadResourcePayload,
+ SelectResourcePayload,
+ UpdateResourceSuccessPayload
+} from '../../../common/store/resource.reducer';
+import {CaseCommand} from '../../../services/portfolio/domain/case-command.model';
+import {WorkflowAction} from '../../../services/portfolio/domain/individuallending/workflow-action.model';
+import {FimsCase} from '../../../services/portfolio/domain/fims-case.model';
+
+export const SEARCH = type('[Case] Search');
+export const SEARCH_COMPLETE = type('[Case] Search Complete');
+
+export const LOAD = type('[Case] Load');
+export const SELECT = type('[Case] Select');
+
+export const CREATE = type('[Case] Create');
+export const CREATE_SUCCESS = type('[Case] Create Success');
+export const CREATE_FAIL = type('[Case] Create Fail');
+
+export const UPDATE = type('[Case] Update');
+export const UPDATE_SUCCESS = type('[Case] Update Success');
+export const UPDATE_FAIL = type('[Case] Update Fail');
+
+export const LOAD_PRODUCT = type('[Case] Form Load Product');
+export const LOAD_PRODUCT_SUCCESS = type('[Case] Form Load Product Success');
+export const LOAD_PRODUCT_FAIL = type('[Case] Form Load Product Fail');
+
+export const UNLOAD_PRODUCT = type('[Case] Form Unload Product');
+export const RESET_FORM = type('[Case] Reset Form');
+
+export const EXECUTE_COMMAND = type('[Case] Execute Command');
+export const EXECUTE_COMMAND_SUCCESS = type('[Case] Execute Command Success');
+export const EXECUTE_COMMAND_FAIL = type('[Case] Execute Command Fail');
+
+export const LOAD_ALL_COST_COMPONENTS = type('[Case] Load All Cost Components');
+export const LOAD_ALL_COST_COMPONENTS_SUCCESS = type('[Case] Load All Cost Components Success');
+export const LOAD_ALL_COST_COMPONENTS_FAIL = type('[Case] Load All Cost Components Fail');
+
+export interface SearchCasePayload {
+ customerId: string;
+ fetchRequest: FetchRequest;
+}
+
+export interface CaseRoutePayload extends RoutePayload {
+ productId: string;
+ caseInstance: FimsCase;
+}
+
+export interface ExecuteCommandPayload extends RoutePayload {
+ productId: string;
+ caseId: string;
+ action: WorkflowAction;
+ command: CaseCommand;
+}
+
+export interface LoadAllCostComponentsPayload {
+ productId: string;
+ caseId: string;
+ action: WorkflowAction;
+}
+
+export class SearchAction implements Action {
+ readonly type = SEARCH;
+
+ constructor(public payload: SearchCasePayload) { }
+}
+
+export class SearchCompleteAction implements Action {
+ readonly type = SEARCH_COMPLETE;
+
+ constructor(public payload: SearchResult) { }
+}
+
+export class LoadAction implements Action {
+ readonly type = LOAD;
+
+ constructor(public payload: LoadResourcePayload) { }
+}
+
+export class SelectAction implements Action {
+ readonly type = SELECT;
+
+ constructor(public payload: SelectResourcePayload) { }
+}
+
+export class CreateCaseAction implements Action {
+ readonly type = CREATE;
+
+ constructor(public payload: CaseRoutePayload) { }
+}
+
+export class CreateCaseSuccessAction implements Action {
+ readonly type = CREATE_SUCCESS;
+
+ constructor(public payload: CreateResourceSuccessPayload) { }
+}
+
+export class CreateCaseFailAction implements Action {
+ readonly type = CREATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class UpdateCaseAction implements Action {
+ readonly type = UPDATE;
+
+ constructor(public payload: CaseRoutePayload) { }
+}
+
+export class UpdateCaseSuccessAction implements Action {
+ readonly type = UPDATE_SUCCESS;
+
+ constructor(public payload: UpdateResourceSuccessPayload) { }
+}
+
+export class UpdateCaseFailAction implements Action {
+ readonly type = UPDATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class LoadProductAction implements Action {
+ readonly type = LOAD_PRODUCT;
+
+ constructor(public payload: string) { }
+}
+
+export class LoadProductSuccessAction implements Action {
+ readonly type = LOAD_PRODUCT_SUCCESS;
+
+ constructor(public payload: Product) { }
+}
+
+export class LoadProductFailAction implements Action {
+ readonly type = LOAD_PRODUCT_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class UnloadProductAction implements Action {
+ readonly type = UNLOAD_PRODUCT;
+
+ constructor() { }
+}
+
+export class ResetCaseFormAction implements Action {
+ readonly type = RESET_FORM;
+
+ constructor() {}
+}
+
+export class ExecuteCommandAction implements Action {
+ readonly type = EXECUTE_COMMAND;
+
+ constructor(public payload: ExecuteCommandPayload) { }
+}
+
+export class ExecuteCommandSuccessAction implements Action {
+ readonly type = EXECUTE_COMMAND_SUCCESS;
+
+ constructor(public payload: ExecuteCommandPayload) { }
+}
+
+export class ExecuteCommandFailAction implements Action {
+ readonly type = EXECUTE_COMMAND_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export type Actions
+ = SearchAction
+ | SearchCompleteAction
+ | LoadAction
+ | SelectAction
+ | CreateCaseAction
+ | CreateCaseSuccessAction
+ | CreateCaseFailAction
+ | UpdateCaseAction
+ | UpdateCaseSuccessAction
+ | UpdateCaseFailAction
+ | LoadProductAction
+ | LoadProductSuccessAction
+ | LoadProductFailAction
+ | UnloadProductAction
+ | ResetCaseFormAction
+ | ExecuteCommandAction
+ | ExecuteCommandSuccessAction
+ | ExecuteCommandFailAction;
diff --git a/src/app/customers/cases/store/cases.reducer.ts b/src/app/customers/cases/store/cases.reducer.ts
new file mode 100644
index 0000000..2bca9c8
--- /dev/null
+++ b/src/app/customers/cases/store/cases.reducer.ts
@@ -0,0 +1,72 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ResourceState} from '../../../common/store/resource.reducer';
+import * as caseActions from './case.actions';
+import {CaseState} from '../../../services/portfolio/domain/case-state.model';
+
+export const initialState: ResourceState = {
+ ids: [],
+ entities: {},
+ loadedAt: {},
+ selectedId: null,
+};
+
+export function reducer(state = initialState, action: caseActions.Actions): ResourceState {
+
+ switch (action.type) {
+
+ case caseActions.EXECUTE_COMMAND_SUCCESS: {
+ const payload = action.payload;
+
+ const caseId = payload.caseId;
+ const commandAction: string = payload.action;
+
+ const caseInstance = state.entities[caseId];
+
+ let caseState: CaseState = null;
+
+ if (commandAction === 'OPEN') {
+ caseState = 'PENDING';
+ }else if (commandAction === 'APPROVE') {
+ caseState = 'APPROVED';
+ }else if (commandAction === 'DENY') {
+ caseState = 'CLOSED';
+ }else if (commandAction === 'CLOSE') {
+ caseState = 'CLOSED';
+ }else if (commandAction === 'DISBURSE') {
+ caseState = 'ACTIVE';
+ }
+
+ caseInstance.currentState = caseState;
+
+ return {
+ ids: [ ...state.ids ],
+ entities: Object.assign({}, state.entities, {
+ [caseInstance.identifier]: caseInstance
+ }),
+ loadedAt: state.loadedAt,
+ selectedId: state.selectedId
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
diff --git a/src/app/customers/cases/store/documents/document.actions.ts b/src/app/customers/cases/store/documents/document.actions.ts
new file mode 100644
index 0000000..d545e77
--- /dev/null
+++ b/src/app/customers/cases/store/documents/document.actions.ts
@@ -0,0 +1,274 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {type} from '../../../../store/util';
+import {RoutePayload} from '../../../../common/store/route-payload';
+import {CustomerDocument} from '../../../../services/customer/domain/customer-document.model';
+import {Action} from '@ngrx/store';
+import {
+ CreateResourceSuccessPayload, DeleteResourceSuccessPayload, LoadResourcePayload, SelectResourcePayload,
+ UpdateResourceSuccessPayload
+} from '../../../../common/store/resource.reducer';
+
+export const LOAD_ALL = type('[Case Document] Load All');
+export const LOAD_ALL_COMPLETE = type('[Case Document] Load All Complete');
+
+export const LOAD = type('[Case Document] Load');
+export const SELECT = type('[Case Document] Select');
+
+export const CREATE = type('[Case Document] Create');
+export const CREATE_SUCCESS = type('[Case Document] Create Success');
+export const CREATE_FAIL = type('[Case Document] Create Fail');
+
+export const UPDATE = type('[Case Document] Update');
+export const UPDATE_SUCCESS = type('[Case Document] Update Success');
+export const UPDATE_FAIL = type('[Case Document] Update Fail');
+
+export const DELETE = type('[Case Document] Delete');
+export const DELETE_SUCCESS = type('[Case Document] Delete Success');
+export const DELETE_FAIL = type('[Case Document] Delete Fail');
+
+export const LOCK = type('[Case Document] Lock');
+export const LOCK_SUCCESS = type('[Case Document] Lock Success');
+export const LOCK_FAIL = type('[Case Document] Lock Fail');
+
+export const LOAD_ALL_PAGES = type('[Case Document] Load All Pages');
+export const LOAD_ALL_PAGES_COMPLETE = type('[Case Document] Load All Pages Complete');
+
+export const UPLOAD_PAGE = type('[Case Document] Upload Page');
+export const UPLOAD_PAGE_SUCCESS = type('[Case Document] Upload Page Success');
+export const UPLOAD_PAGE_FAIL = type('[Case Document] Upload Page Fail');
+
+export const DELETE_PAGE = type('[Case Document] Delete Page');
+export const DELETE_PAGE_SUCCESS = type('[Case Document] Delete Page Success');
+export const DELETE_PAGE_FAIL = type('[Case Document] Delete Page Fail');
+
+export const RESET_PAGE_FORM = type('[Case Document] Reset Page Form');
+
+export interface LoadAllPayload {
+ customerId: string;
+ productId: string;
+ caseId: string;
+}
+
+export interface LoadAllPagesPayload {
+ customerId: string;
+ documentId: string;
+}
+
+export interface DocumentPayload extends RoutePayload {
+ productId: string;
+ caseId: string;
+ customerId: string;
+ document: CustomerDocument;
+}
+
+export interface DeletePagePayload {
+ customerId: string;
+ documentId: string;
+ pageNumber: number;
+}
+
+export interface UploadPagePayload extends RoutePayload {
+ customerId: string;
+ documentId: string;
+ pageNumber: number;
+ page: File;
+}
+
+export interface LockPayload {
+ customerId: string;
+ documentId: string;
+}
+
+export class LoadAllAction implements Action {
+ readonly type = LOAD_ALL;
+
+ constructor(public payload: LoadAllPayload) { }
+}
+
+export class LoadAllCompleteAction implements Action {
+ readonly type = LOAD_ALL_COMPLETE;
+
+ constructor(public payload: CustomerDocument[]) { }
+}
+
+export class LoadAction implements Action {
+ readonly type = LOAD;
+
+ constructor(public payload: LoadResourcePayload) { }
+}
+
+export class SelectAction implements Action {
+ readonly type = SELECT;
+
+ constructor(public payload: SelectResourcePayload) { }
+}
+
+export class CreateDocumentAction implements Action {
+ readonly type = CREATE;
+
+ constructor(public payload: DocumentPayload) { }
+}
+
+export class CreateDocumentSuccessAction implements Action {
+ readonly type = CREATE_SUCCESS;
+
+ constructor(public payload: CreateResourceSuccessPayload) { }
+}
+
+export class CreateDocumentFailAction implements Action {
+ readonly type = CREATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class UpdateDocumentAction implements Action {
+ readonly type = UPDATE;
+
+ constructor(public payload: DocumentPayload) { }
+}
+
+export class UpdateDocumentSuccessAction implements Action {
+ readonly type = UPDATE_SUCCESS;
+
+ constructor(public payload: UpdateResourceSuccessPayload) { }
+}
+
+export class UpdateDocumentFailAction implements Action {
+ readonly type = UPDATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class DeleteDocumentAction implements Action {
+ readonly type = DELETE;
+
+ constructor(public payload: DocumentPayload) { }
+}
+
+export class DeleteDocumentSuccessAction implements Action {
+ readonly type = DELETE_SUCCESS;
+
+ constructor(public payload: DeleteResourceSuccessPayload) { }
+}
+
+export class DeleteDocumentFailAction implements Action {
+ readonly type = DELETE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class LockDocumentAction implements Action {
+ readonly type = LOCK;
+
+ constructor(public payload: LockPayload) { }
+}
+
+export class LockDocumentSuccessAction implements Action {
+ readonly type = LOCK_SUCCESS;
+
+ constructor(public payload: LockPayload) { }
+}
+
+export class LockDocumentFailAction implements Action {
+ readonly type = LOCK_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class LoadAllPagesAction implements Action {
+ readonly type = LOAD_ALL_PAGES;
+
+ constructor(public payload: LoadAllPagesPayload) { }
+}
+
+export class LoadAllPagesCompleteAction implements Action {
+ readonly type = LOAD_ALL_PAGES_COMPLETE;
+
+ constructor(public payload: number[]) { }
+}
+
+export class UploadPageAction implements Action {
+ readonly type = UPLOAD_PAGE;
+
+ constructor(public payload: UploadPagePayload) { }
+}
+
+export class UploadPageSuccessAction implements Action {
+ readonly type = UPLOAD_PAGE_SUCCESS;
+
+ constructor(public payload: UploadPagePayload) { }
+}
+
+export class UploadPageFailAction implements Action {
+ readonly type = UPLOAD_PAGE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class DeletePageAction implements Action {
+ readonly type = DELETE_PAGE;
+
+ constructor(public payload: DeletePagePayload) { }
+}
+
+export class DeletePageSuccessAction implements Action {
+ readonly type = DELETE_PAGE_SUCCESS;
+
+ constructor(public payload: DeletePagePayload) { }
+}
+
+export class DeletePageFailAction implements Action {
+ readonly type = DELETE_PAGE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class ResetDocumentPageForm implements Action {
+ readonly type = RESET_PAGE_FORM;
+
+ constructor() {}
+}
+
+export type Actions
+ = LoadAllAction
+ | LoadAllCompleteAction
+ | LoadAction
+ | SelectAction
+ | CreateDocumentAction
+ | CreateDocumentSuccessAction
+ | CreateDocumentFailAction
+ | UpdateDocumentAction
+ | UpdateDocumentSuccessAction
+ | UpdateDocumentFailAction
+ | DeleteDocumentAction
+ | DeleteDocumentSuccessAction
+ | DeleteDocumentFailAction
+ | ResetDocumentPageForm
+ | LockDocumentAction
+ | LockDocumentSuccessAction
+ | LockDocumentFailAction
+ | LoadAllPagesAction
+ | LoadAllPagesCompleteAction
+ | UploadPageAction
+ | UploadPageSuccessAction
+ | UploadPageFailAction
+ | DeletePageAction
+ | DeletePageSuccessAction
+ | DeletePageFailAction;
diff --git a/src/app/customers/cases/store/documents/documents.reducer.ts b/src/app/customers/cases/store/documents/documents.reducer.ts
new file mode 100644
index 0000000..15f1f53
--- /dev/null
+++ b/src/app/customers/cases/store/documents/documents.reducer.ts
@@ -0,0 +1,77 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as documentActions from './document.actions';
+import {ResourceState} from '../../../../common/store/resource.reducer';
+import {CustomerDocument} from '../../../../services/customer/domain/customer-document.model';
+import {idsToHashWithCurrentTimestamp, resourcesToHash} from '../../../../common/store/reducer.helper';
+import {DeletePagePayload, LockPayload, UploadPagePayload} from './document.actions';
+
+export const initialState: ResourceState = {
+ ids: [],
+ entities: {},
+ loadedAt: {},
+ selectedId: null,
+};
+
+export function reducer(state = initialState, action: documentActions.Actions): ResourceState {
+
+ switch (action.type) {
+
+ case documentActions.LOAD_ALL: {
+ return initialState;
+ }
+
+ case documentActions.LOAD_ALL_COMPLETE: {
+ const documents: CustomerDocument[] = action.payload;
+
+ const ids = documents.map(document => document.identifier);
+
+ const entities = resourcesToHash(documents);
+
+ const loadedAt = idsToHashWithCurrentTimestamp(ids);
+
+ return {
+ ids: [ ...ids ],
+ entities: entities,
+ loadedAt: loadedAt,
+ selectedId: state.selectedId
+ };
+ }
+
+ case documentActions.LOCK_SUCCESS: {
+ const payload: LockPayload = action.payload;
+
+ const document = state.entities[payload.documentId];
+
+ const entities = Object.assign({}, state.entities, {
+ [document.identifier]: Object.assign({}, document, {
+ completed: true
+ })
+ });
+
+ return Object.assign({}, state, {
+ entities
+ })
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
diff --git a/src/app/customers/cases/store/documents/effects/notification.effects.ts b/src/app/customers/cases/store/documents/effects/notification.effects.ts
new file mode 100644
index 0000000..9db85f8
--- /dev/null
+++ b/src/app/customers/cases/store/documents/effects/notification.effects.ts
@@ -0,0 +1,87 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import * as documents from '../document.actions';
+import {Action} from '@ngrx/store';
+import {NotificationService, NotificationType} from '../../../../../services/notification/notification.service';
+
+@Injectable()
+export class CaseDocumentNotificationEffects {
+
+ @Effect({ dispatch: false })
+ createDocumentSuccess$: Observable<Action> = this.actions$
+ .ofType(documents.CREATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Document is going to be saved'
+ }));
+
+ @Effect({ dispatch: false })
+ deleteDocumentSuccess$: Observable<Action> = this.actions$
+ .ofType(documents.DELETE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Document is going to be deleted'
+ }));
+
+ @Effect({ dispatch: false })
+ uploadPageSuccess$: Observable<Action> = this.actions$
+ .ofType(documents.UPLOAD_PAGE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Page is going to be uploaded'
+ }));
+
+ @Effect({ dispatch: false })
+ uploadPageFail$: Observable<Action> = this.actions$
+ .ofType(documents.UPLOAD_PAGE_FAIL)
+ .do(() => this.notificationService.send({
+ type: NotificationType.ALERT,
+ message: 'Please choose a different page number'
+ }));
+
+ @Effect({ dispatch: false })
+ deletePageSuccess$: Observable<Action> = this.actions$
+ .ofType(documents.DELETE_PAGE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Page is going to be deleted'
+ }));
+
+ @Effect({ dispatch: false })
+ lockDocumentSuccess$: Observable<Action> = this.actions$
+ .ofType(documents.LOCK_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Document locked'
+ }));
+
+ @Effect({ dispatch: false })
+ lockDocumentFail$: Observable<Action> = this.actions$
+ .ofType(documents.LOCK_FAIL)
+ .do(() => this.notificationService.send({
+ type: NotificationType.ALERT,
+ title: 'Document could not be locked',
+ message: 'Please make sure all pages are uploaded and are in sequence'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+}
diff --git a/src/app/customers/cases/store/documents/effects/route.effects.ts b/src/app/customers/cases/store/documents/effects/route.effects.ts
new file mode 100644
index 0000000..3000bd8
--- /dev/null
+++ b/src/app/customers/cases/store/documents/effects/route.effects.ts
@@ -0,0 +1,53 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {Actions, Effect} from '@ngrx/effects';
+import {Action} from '@ngrx/store';
+import {Router} from '@angular/router';
+import * as documents from '../document.actions';
+import {
+ CreateDocumentSuccessAction, DeleteDocumentSuccessAction, UpdateDocumentSuccessAction,
+ UploadPageSuccessAction
+} from '../document.actions';
+
+@Injectable()
+export class CaseDocumentRouteEffects {
+
+ @Effect({ dispatch: false })
+ createUpdateSuccess$: Observable<Action> = this.actions$
+ .ofType(
+ documents.CREATE_SUCCESS,
+ documents.UPDATE_SUCCESS,
+ documents.UPLOAD_PAGE_SUCCESS
+ )
+ .do((action: CreateDocumentSuccessAction | UpdateDocumentSuccessAction | UploadPageSuccessAction) =>
+ this.router.navigate(['../'], { relativeTo: action.payload.activatedRoute} )
+ );
+
+ @Effect({ dispatch: false })
+ deleteSuccess$: Observable<Action> = this.actions$
+ .ofType(documents.DELETE_SUCCESS)
+ .do((action: DeleteDocumentSuccessAction) =>
+ this.router.navigate(['../../../../../../../../../../'], { relativeTo: action.payload.activatedRoute} )
+ );
+
+ constructor(private actions$: Actions, private router: Router) { }
+
+}
diff --git a/src/app/customers/cases/store/documents/effects/service.effects.ts b/src/app/customers/cases/store/documents/effects/service.effects.ts
new file mode 100644
index 0000000..cf34cf6
--- /dev/null
+++ b/src/app/customers/cases/store/documents/effects/service.effects.ts
@@ -0,0 +1,122 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as documents from '../document.actions';
+import {DeletePagePayload, DocumentPayload, LoadAllPagesPayload, LoadAllPayload, LockPayload, UploadPagePayload} from '../document.actions';
+import {of} from 'rxjs/observable/of';
+import {DocumentsService} from './services/documents.service';
+
+@Injectable()
+export class CaseDocumentApiEffects {
+
+ @Effect()
+ loadAllDocuments$: Observable<Action> = this.actions$
+ .ofType(documents.LOAD_ALL)
+ .map((action: documents.LoadAllAction) => action.payload)
+ .switchMap((payload: LoadAllPayload) =>
+ this.documentsService.getCustomerDocuments(payload.customerId, payload.productId, payload.caseId)
+ .map(customerDocuments => new documents.LoadAllCompleteAction(customerDocuments))
+ .catch(() => of(new documents.LoadAllCompleteAction([])))
+ );
+
+ @Effect()
+ createDocument$: Observable<Action> = this.actions$
+ .ofType(documents.CREATE)
+ .map((action: documents.CreateDocumentAction) => action.payload)
+ .mergeMap((payload: DocumentPayload) =>
+ this.documentsService.createDocument(payload.productId, payload.caseId, payload.customerId, payload.document)
+ .map(() => new documents.CreateDocumentSuccessAction({
+ resource: payload.document,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new documents.CreateDocumentFailAction(error)))
+ );
+
+ @Effect()
+ updateDocument$: Observable<Action> = this.actions$
+ .ofType(documents.UPDATE)
+ .map((action: documents.UpdateDocumentAction) => action.payload)
+ .mergeMap((payload: DocumentPayload) =>
+ this.documentsService.updateDocument(payload.customerId, payload.document)
+ .map(() => new documents.UpdateDocumentSuccessAction({
+ resource: payload.document,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new documents.UpdateDocumentFailAction(error)))
+ );
+
+ @Effect()
+ deleteDocument$: Observable<Action> = this.actions$
+ .ofType(documents.DELETE)
+ .map((action: documents.DeleteDocumentAction) => action.payload)
+ .mergeMap((payload: DocumentPayload) =>
+ this.documentsService.deleteDocument(payload.productId, payload.caseId, payload.customerId, payload.document)
+ .map(() => new documents.DeleteDocumentSuccessAction({
+ resource: payload.document,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new documents.DeleteDocumentFailAction(error)))
+ );
+
+ @Effect()
+ lockDocument$: Observable<Action> = this.actions$
+ .ofType(documents.LOCK)
+ .map((action: documents.LockDocumentAction) => action.payload)
+ .mergeMap((payload: LockPayload) =>
+ this.documentsService.lockDocument(payload.customerId, payload.documentId)
+ .map(() => new documents.LockDocumentSuccessAction(payload))
+ .catch((error) => of(new documents.LockDocumentFailAction(error)))
+ );
+
+ @Effect()
+ loadAllPages$: Observable<Action> = this.actions$
+ .ofType(documents.LOAD_ALL_PAGES)
+ .map((action: documents.LoadAllPagesAction) => action.payload)
+ .switchMap((payload: LoadAllPagesPayload) =>
+ this.documentsService.getDocumentPageNumbers(payload.customerId, payload.documentId)
+ .map(pageNumbers => new documents.LoadAllPagesCompleteAction(pageNumbers))
+ .catch(() => of(new documents.LoadAllPagesCompleteAction([])))
+ );
+
+ @Effect()
+ uploadDocumentPage$: Observable<Action> = this.actions$
+ .ofType(documents.UPLOAD_PAGE)
+ .map((action: documents.UploadPageAction) => action.payload)
+ .mergeMap((payload: UploadPagePayload) =>
+ this.documentsService.uploadPage(payload.customerId, payload.documentId, payload.pageNumber, payload.page)
+ .map(() => new documents.UploadPageSuccessAction(payload))
+ .catch((error) => of(new documents.UploadPageFailAction(error)))
+ );
+
+ @Effect()
+ deleteDocumentPage$: Observable<Action> = this.actions$
+ .ofType(documents.DELETE_PAGE)
+ .map((action: documents.DeletePageAction) => action.payload)
+ .mergeMap((payload: DeletePagePayload) =>
+ this.documentsService.deletePage(payload.customerId, payload.documentId, payload.pageNumber)
+ .map(() => new documents.DeletePageSuccessAction(payload))
+ .catch((error) => of(new documents.DeletePageFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private documentsService: DocumentsService) {}
+
+}
diff --git a/src/app/customers/cases/store/documents/effects/services/documents.service.ts b/src/app/customers/cases/store/documents/effects/services/documents.service.ts
new file mode 100644
index 0000000..f88f030
--- /dev/null
+++ b/src/app/customers/cases/store/documents/effects/services/documents.service.ts
@@ -0,0 +1,115 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {PortfolioService} from '../../../../../../services/portfolio/portfolio.service';
+import {CustomerService} from '../../../../../../services/customer/customer.service';
+import {Observable} from 'rxjs/Observable';
+import {CustomerDocument} from '../../../../../../services/customer/domain/customer-document.model';
+import {CaseCustomerDocuments} from '../../../../../../services/portfolio/domain/case-customer-documents.model';
+
+@Injectable()
+export class DocumentsService {
+
+ constructor(private customerService: CustomerService, private portfolioService: PortfolioService) {
+ }
+
+ public createDocument(productId: string, caseId: string, customerId: string, customerDocument: CustomerDocument): Observable<void> {
+ return this.getNextDocumentId(customerId, caseId)
+ .map((nextId: string) => Object.assign({}, customerDocument, {identifier: nextId}))
+ .mergeMap((document: CustomerDocument) => this.customerService.createDocument(customerId, document)
+ .mergeMap(() => this.portfolioService.getCaseDocuments(productId, caseId))
+ .map((documents: CaseCustomerDocuments) => this.addDocument(documents, customerId, document.identifier))
+ .mergeMap((documents: CaseCustomerDocuments) => this.portfolioService.changeCaseDocuments(productId, caseId, documents))
+ )
+ }
+
+ /**
+ * Fetches the next available document identifier from the customer service.
+ * The document identifier follows the pattern 'caseId_increment' e.g. 'myCase_12'
+ */
+ private getNextDocumentId(customerId: string, caseId: string): Observable<string> {
+ return this.customerService.getDocuments(customerId)
+ .map((documents: CustomerDocument[]) => documents.filter(document => document.identifier.startsWith(caseId)))
+ .map((documents: CustomerDocument[]) => documents.map(document =>
+ document.identifier.substr(caseId.length + 1, document.identifier.length))
+ )
+ .map((documentIds: string[]) => documentIds.map(id => parseInt(id, 10)))
+ .map((documentIds: number[]) => documentIds.length > 0 ? Math.max(...documentIds) + 1 : 1)
+ .map((nextId: number) => `${caseId}_${nextId}`);
+ }
+
+ public getCustomerDocuments(customerId: string, productId: string, caseId: string): Observable<CustomerDocument[]> {
+ return Observable.combineLatest(
+ this.portfolioService.getCaseDocuments(productId, caseId),
+ this.customerService.getDocuments(customerId),
+ (caseDocuments, customerDocuments) => ({
+ caseDocuments: caseDocuments.documents,
+ customerDocuments
+ })
+ ).map(result => result.customerDocuments.filter(document => {
+ const foundCaseDocument = result.caseDocuments.find(caseDocument => caseDocument.documentId === document.identifier);
+ return !!foundCaseDocument;
+ })
+ );
+ }
+
+ private addDocument(documents: CaseCustomerDocuments, customerId: string, documentId: string): CaseCustomerDocuments {
+ return Object.assign({}, documents, {
+ documents: documents.documents.concat({
+ customerId,
+ documentId
+ })
+ })
+ }
+
+ public updateDocument(customerId: string, customerDocument: CustomerDocument): Observable<void> {
+ return this.customerService.updateDocument(customerId, customerDocument);
+ }
+
+ public deleteDocument(productId: string, caseId: string, customerId: string, customerDocument: CustomerDocument): Observable<void> {
+ return this.customerService.deleteDocument(customerId, customerDocument)
+ .mergeMap(() => this.portfolioService.getCaseDocuments(productId, caseId))
+ .map((documents: CaseCustomerDocuments) => this.removeDocument(documents, customerDocument.identifier))
+ .mergeMap((documents: CaseCustomerDocuments) => this.portfolioService.changeCaseDocuments(productId, caseId, documents))
+ }
+
+ private removeDocument(documents: CaseCustomerDocuments, documentId: string): CaseCustomerDocuments {
+ return Object.assign({}, documents, {
+ documents: documents.documents.filter(document => document.documentId !== documentId)
+ })
+ }
+
+ public getDocumentPageNumbers(customerId: string, documentId: string): Observable<number[]> {
+ return this.customerService.getDocumentPageNumbers(customerId, documentId);
+ }
+
+ public uploadPage(customerId: string, documentId: string, pageNumber: number, file: File): Observable<void> {
+ return this.customerService.createDocumentPage(customerId, documentId, pageNumber, file);
+ }
+
+ public deletePage(customerId: string, documentId: string, pageNumber: number): Observable<void> {
+ return this.customerService.deleteDocumentPage(customerId, documentId, pageNumber);
+ }
+
+ public lockDocument(customerId: string, documentId: string): Observable<void> {
+ return this.customerService.completeDocument(customerId, documentId, true);
+ }
+
+
+}
diff --git a/src/app/customers/cases/store/documents/pageNumber.reducer.ts b/src/app/customers/cases/store/documents/pageNumber.reducer.ts
new file mode 100644
index 0000000..6c4d52c
--- /dev/null
+++ b/src/app/customers/cases/store/documents/pageNumber.reducer.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as documentActions from './document.actions';
+import {DeletePagePayload, UploadPagePayload} from './document.actions';
+
+export interface State {
+ pageNumbers: number[]
+}
+
+export const initialState: State = {
+ pageNumbers: [],
+};
+
+export function reducer(state = initialState, action: documentActions.Actions): State {
+
+ switch (action.type) {
+
+ case documentActions.LOAD_ALL_PAGES: {
+ return initialState;
+ }
+
+ case documentActions.LOAD_ALL_PAGES_COMPLETE: {
+ const pageNumbers: number[] = action.payload;
+
+ return {
+ pageNumbers
+ };
+ }
+
+ case documentActions.UPLOAD_PAGE_SUCCESS: {
+ const payload: UploadPagePayload = action.payload;
+
+ return {
+ pageNumbers: state.pageNumbers.concat(payload.pageNumber)
+ }
+ }
+
+ case documentActions.DELETE_PAGE_SUCCESS: {
+ const payload: DeletePagePayload = action.payload;
+
+ return {
+ pageNumbers: state.pageNumbers.filter(pageNumber => pageNumber !== payload.pageNumber)
+ }
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+export const getPageNumbers = (state: State) => state.pageNumbers;
diff --git a/src/app/customers/cases/store/effects/notification.effects.ts b/src/app/customers/cases/store/effects/notification.effects.ts
new file mode 100644
index 0000000..498e424
--- /dev/null
+++ b/src/app/customers/cases/store/effects/notification.effects.ts
@@ -0,0 +1,46 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as caseActions from '../case.actions';
+import {NotificationService, NotificationType} from '../../../../services/notification/notification.service';
+
+@Injectable()
+export class CaseNotificationEffects {
+
+ @Effect({ dispatch: false })
+ createCaseSuccess$: Observable<Action> = this.actions$
+ .ofType(caseActions.CREATE_SUCCESS, caseActions.UPDATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Case is going to be saved'
+ }));
+
+ @Effect({ dispatch: false })
+ executeCommandSuccess$: Observable<Action> = this.actions$
+ .ofType(caseActions.EXECUTE_COMMAND_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Case is going to be updated'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+}
diff --git a/src/app/customers/cases/store/effects/route.effects.ts b/src/app/customers/cases/store/effects/route.effects.ts
new file mode 100644
index 0000000..ad42d8b
--- /dev/null
+++ b/src/app/customers/cases/store/effects/route.effects.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Action} from '@ngrx/store';
+import {Observable} from 'rxjs/Observable';
+import {Actions, Effect} from '@ngrx/effects';
+import {Router} from '@angular/router';
+import * as caseActions from '../case.actions';
+import {Injectable} from '@angular/core';
+
+@Injectable()
+export class CaseRouteEffects {
+
+ @Effect({ dispatch: false })
+ createCaseSuccess$: Observable<Action> = this.actions$
+ .ofType(caseActions.CREATE_SUCCESS, caseActions.UPDATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute} ));
+
+ @Effect({ dispatch: false })
+ executeCommandSuccess$: Observable<Action> = this.actions$
+ .ofType(caseActions.EXECUTE_COMMAND_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../../../'], { relativeTo: payload.activatedRoute} ));
+
+ constructor(private actions$: Actions, private router: Router) { }
+
+}
diff --git a/src/app/customers/cases/store/effects/service.effects.ts b/src/app/customers/cases/store/effects/service.effects.ts
new file mode 100644
index 0000000..6edddb5
--- /dev/null
+++ b/src/app/customers/cases/store/effects/service.effects.ts
@@ -0,0 +1,98 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+import * as caseActions from '../case.actions';
+import {PortfolioService} from '../../../../services/portfolio/portfolio.service';
+import {Product} from '../../../../services/portfolio/domain/product.model';
+
+@Injectable()
+export class CaseApiEffects {
+
+ @Effect()
+ search$: Observable<Action> = this.actions$
+ .ofType(caseActions.SEARCH)
+ .debounceTime(300)
+ .map((action: caseActions.SearchAction) => action.payload)
+ .switchMap(payload => {
+ const nextSearch$ = this.actions$.ofType(caseActions.SEARCH).skip(1);
+
+ return this.portfolioService.getAllCasesForCustomer(payload.customerId, payload.fetchRequest)
+ .takeUntil(nextSearch$)
+ .map(fimsCasePage => new caseActions.SearchCompleteAction(fimsCasePage))
+ .catch(() => of(new caseActions.SearchCompleteAction({
+ totalElements: 0,
+ totalPages: 0,
+ elements: []
+ })));
+ });
+
+ @Effect()
+ createCase$: Observable<Action> = this.actions$
+ .ofType(caseActions.CREATE)
+ .map((action: caseActions.CreateCaseAction) => action.payload)
+ .mergeMap(payload =>
+ this.portfolioService.createCase(payload.productId, payload.caseInstance)
+ .map(() => new caseActions.CreateCaseSuccessAction({
+ resource: payload.caseInstance,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new caseActions.CreateCaseFailAction(error)))
+ );
+
+
+ @Effect()
+ updateCase$: Observable<Action> = this.actions$
+ .ofType(caseActions.UPDATE)
+ .map((action: caseActions.UpdateCaseAction) => action.payload)
+ .mergeMap(payload =>
+ this.portfolioService.changeCase(payload.productId, payload.caseInstance)
+ .map(() => new caseActions.UpdateCaseSuccessAction({
+ resource: payload.caseInstance,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new caseActions.UpdateCaseFailAction(error)))
+ );
+
+ @Effect()
+ loadProduct$: Observable<Action> = this.actions$
+ .ofType(caseActions.LOAD_PRODUCT)
+ .map((action: caseActions.LoadProductAction) => action.payload)
+ .mergeMap(productId =>
+ this.portfolioService.getProduct(productId)
+ .map((product: Product) => new caseActions.LoadProductSuccessAction(product))
+ .catch((error) => of(new caseActions.LoadProductFailAction(error)))
+ );
+
+ @Effect()
+ executeCommand$: Observable<Action> = this.actions$
+ .ofType(caseActions.EXECUTE_COMMAND)
+ .map((action: caseActions.ExecuteCommandAction) => action.payload)
+ .mergeMap(payload =>
+ this.portfolioService.executeCaseCommand(payload.productId, payload.caseId, payload.action, payload.command)
+ .map(() => new caseActions.ExecuteCommandSuccessAction(payload))
+ .catch((error) => of(new caseActions.ExecuteCommandFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private portfolioService: PortfolioService) { }
+
+}
diff --git a/src/app/customers/cases/store/form.reducer.ts b/src/app/customers/cases/store/form.reducer.ts
new file mode 100644
index 0000000..a647e76
--- /dev/null
+++ b/src/app/customers/cases/store/form.reducer.ts
@@ -0,0 +1,49 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as caseActions from './case.actions';
+import {Product} from '../../../services/portfolio/domain/product.model';
+import {FormState} from '../../../common/store/form.reducer';
+
+export interface State extends FormState {
+ product: Product;
+}
+
+export const initialState: State = {
+ error: null,
+ product: null
+};
+
+export function reducer(state = initialState, action: caseActions.Actions): State {
+ switch (action.type) {
+
+ case caseActions.LOAD_PRODUCT_SUCCESS: {
+ const product: Product = action.payload;
+ return {
+ error: state.error,
+ product: product
+ };
+ }
+
+ default:
+ return state;
+
+ }
+}
+
+export const getFormProduct = (state: State) => state.product;
diff --git a/src/app/customers/cases/store/index.ts b/src/app/customers/cases/store/index.ts
new file mode 100644
index 0000000..a3b34ca
--- /dev/null
+++ b/src/app/customers/cases/store/index.ts
@@ -0,0 +1,138 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as fromCustomer from '../../store';
+import * as fromCases from './cases.reducer';
+import * as fromCaseForm from './form.reducer';
+import * as fromCaseTasks from './tasks/tasks.reducer';
+import * as fromCasePayments from './payments/search.reducer';
+import * as fromCaseDocuments from './documents/documents.reducer'
+import * as fromCaseDocumentPages from './documents/pageNumber.reducer'
+
+import {ActionReducer, Store} from '@ngrx/store';
+import {createReducer} from '../../../store/index';
+import {createSelector} from 'reselect';
+import {
+ createResourceReducer,
+ getResourceAll,
+ getResourceEntities,
+ getResourceIds,
+ getResourceLoadedAt,
+ getResourceSelected,
+ getResourceSelectedId,
+ ResourceState
+} from '../../../common/store/resource.reducer';
+import {
+ createSearchReducer,
+ getSearchEntities,
+ getSearchTotalElements,
+ getSearchTotalPages,
+ SearchState
+} from '../../../common/store/search.reducer';
+import {createFormReducer, getFormError} from '../../../common/store/form.reducer';
+
+export interface State extends fromCustomer.State {
+ cases: ResourceState;
+ caseForm: fromCaseForm.State;
+ caseSearch: SearchState;
+ caseTasks: fromCaseTasks.State;
+ casePayments: fromCasePayments.State;
+ caseDocuments: ResourceState;
+ caseDocumentPages: fromCaseDocumentPages.State;
+}
+
+const reducers = {
+ cases: createResourceReducer('Case', fromCases.reducer),
+ caseForm: createFormReducer('Case', fromCaseForm.reducer),
+ caseSearch: createSearchReducer('Case'),
+ caseTasks: fromCaseTasks.reducer,
+ casePayments: fromCasePayments.reducer,
+ caseDocuments: createResourceReducer('Case Document', fromCaseDocuments.reducer),
+ caseDocumentPages: fromCaseDocumentPages.reducer
+};
+
+export const caseModuleReducer: ActionReducer<State> = createReducer(reducers);
+
+export class CasesStore extends Store<State> {}
+
+export function caseStoreFactory(appStore: Store<fromCustomer.State>) {
+ appStore.replaceReducer(caseModuleReducer);
+ return appStore;
+}
+
+export const getCaseSearchState = (state: State) => state.caseSearch;
+
+export const getSearchCases = createSelector(getCaseSearchState, getSearchEntities);
+export const getCaseSearchTotalElements = createSelector(getCaseSearchState, getSearchTotalElements);
+export const getCaseSearchTotalPages = createSelector(getCaseSearchState, getSearchTotalPages);
+
+export const getCaseSearchResults = createSelector(getSearchCases, getCaseSearchTotalPages, getCaseSearchTotalElements,
+ (cases, totalPages, totalElements) => {
+ return {
+ cases: cases,
+ totalPages: totalPages,
+ totalElements: totalElements
+ };
+});
+
+export const getCasesState = (state: State) => state.cases;
+
+export const getCaseEntities = createSelector(getCasesState, getResourceEntities);
+export const getCasesLoadedAt = createSelector(getCasesState, getResourceLoadedAt);
+export const getCaseIds = createSelector(getCasesState, getResourceIds);
+export const getSelectedCaseId = createSelector(getCasesState, getResourceSelectedId);
+export const getSelectedCase = createSelector(getCasesState, getResourceSelected);
+
+export const getCaseTasksState = (state: State) => state.caseTasks;
+
+export const getCaseCommands = createSelector(getCaseTasksState, fromCaseTasks.getCommands);
+
+
+export const getCasePaymentsSearchState = (state: State) => state.casePayments;
+
+export const getSearchCasePaymentPage = createSelector(getCasePaymentsSearchState, fromCasePayments.getPaymentPage);
+
+export const getCaseFormState = (state: State) => state.caseForm;
+export const getCaseFormError = createSelector(getCaseFormState, getFormError);
+
+export const getCaseFormProduct = createSelector(getCaseFormState, fromCaseForm.getFormProduct);
+
+export const getCaseSelection = createSelector(getSelectedCase, fromCustomer.getSelectedCustomer, (caseInstance, customer) => {
+ return {
+ customerId: customer.identifier,
+ productId: caseInstance.productIdentifier,
+ caseId: caseInstance.identifier
+ }
+});
+
+/**
+ * Case Document Selectors
+ */
+export const getCaseDocumentsState = (state: State) => state.caseDocuments;
+
+export const getAllCaseDocumentEntities = createSelector(getCaseDocumentsState, getResourceAll);
+
+export const getCaseDocumentLoadedAt = createSelector(getCaseDocumentsState, getResourceLoadedAt);
+export const getSelectedCaseDocument = createSelector(getCaseDocumentsState, getResourceSelected);
+
+/**
+ * Case Document Selectors
+ */
+export const getDocumentPagesState = (state: State) => state.caseDocumentPages;
+
+export const getAllDocumentPages = createSelector(getDocumentPagesState, fromCaseDocumentPages.getPageNumbers);
diff --git a/src/app/customers/cases/store/model/case-selection.model.ts b/src/app/customers/cases/store/model/case-selection.model.ts
new file mode 100644
index 0000000..c5bc084
--- /dev/null
+++ b/src/app/customers/cases/store/model/case-selection.model.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface CaseSelection {
+ customerId: string,
+ productId: string,
+ caseId: string,
+}
diff --git a/src/app/customers/cases/store/model/fims-command.model.ts b/src/app/customers/cases/store/model/fims-command.model.ts
new file mode 100644
index 0000000..95f229a
--- /dev/null
+++ b/src/app/customers/cases/store/model/fims-command.model.ts
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {CaseState} from '../../../../services/portfolio/domain/case-state.model';
+import {WorkflowAction} from '../../../../services/portfolio/domain/individuallending/workflow-action.model';
+import {FimsTaskInstance} from './fims-task-instance.model';
+
+export interface StatusCommand {
+ action: WorkflowAction;
+ preStates: CaseState[];
+ tasks: FimsTaskInstance[];
+ note?: string;
+}
diff --git a/src/app/customers/cases/store/model/fims-task-instance.model.ts b/src/app/customers/cases/store/model/fims-task-instance.model.ts
new file mode 100644
index 0000000..7bba822
--- /dev/null
+++ b/src/app/customers/cases/store/model/fims-task-instance.model.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {TaskDefinition} from '../../../../services/portfolio/domain/task-definition.model';
+
+export interface FimsTaskInstance {
+ taskDefinition: TaskDefinition;
+ comment: string;
+ executedOn: string;
+ executedBy: string;
+}
diff --git a/src/app/customers/cases/store/payments/effects/service.effects.ts b/src/app/customers/cases/store/payments/effects/service.effects.ts
new file mode 100644
index 0000000..143e6b1
--- /dev/null
+++ b/src/app/customers/cases/store/payments/effects/service.effects.ts
@@ -0,0 +1,51 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Actions, Effect} from '@ngrx/effects';
+import {Injectable} from '@angular/core';
+import {Action} from '@ngrx/store';
+import {Observable} from 'rxjs/Observable';
+import * as paymentActions from '../payment.actions';
+import {of} from 'rxjs/observable/of';
+import {PortfolioService} from '../../../../../services/portfolio/portfolio.service';
+
+@Injectable()
+export class CasePaymentsApiEffects {
+
+ @Effect()
+ search$: Observable<Action> = this.actions$
+ .ofType(paymentActions.SEARCH)
+ .debounceTime(300)
+ .map((action: paymentActions.SearchAction) => action.payload)
+ .switchMap(payload => {
+ const nextSearch$ = this.actions$.ofType(paymentActions.SEARCH).skip(1);
+
+ return this.portfolioService.getPaymentScheduleForCase(payload.productIdentifier, payload.caseIdentifier,
+ payload.initialDisbursalDate)
+ .takeUntil(nextSearch$)
+ .map(paymentsPage => new paymentActions.SearchCompleteAction(paymentsPage))
+ .catch(() => of(new paymentActions.SearchCompleteAction({
+ totalElements: 0,
+ totalPages: 0,
+ elements: [],
+ chargeNames: []
+ })));
+ });
+
+ constructor(private actions$: Actions, private portfolioService: PortfolioService) {}
+}
diff --git a/src/app/customers/cases/store/payments/payment.actions.ts b/src/app/customers/cases/store/payments/payment.actions.ts
new file mode 100644
index 0000000..370a1e0
--- /dev/null
+++ b/src/app/customers/cases/store/payments/payment.actions.ts
@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {type} from '../../../../store/util';
+import {PlannedPaymentPage} from '../../../../services/portfolio/domain/individuallending/planned-payment-page.model';
+import {Action} from '@ngrx/store';
+
+export const SEARCH = type('[Case Payments] Search');
+export const SEARCH_COMPLETE = type('[Case Payments] Search Complete');
+
+export interface SearchPaymentsPayload {
+ productIdentifier: string;
+ caseIdentifier: string;
+ initialDisbursalDate?: string;
+}
+
+export class SearchAction implements Action {
+ readonly type = SEARCH;
+
+ constructor(public payload: SearchPaymentsPayload) { }
+}
+
+export class SearchCompleteAction implements Action {
+ readonly type = SEARCH_COMPLETE;
+
+ constructor(public payload: PlannedPaymentPage) { }
+}
+
+export type Actions
+ = SearchAction
+ | SearchCompleteAction;
+
diff --git a/src/app/customers/cases/store/payments/search.reducer.ts b/src/app/customers/cases/store/payments/search.reducer.ts
new file mode 100644
index 0000000..511ede5
--- /dev/null
+++ b/src/app/customers/cases/store/payments/search.reducer.ts
@@ -0,0 +1,73 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as paymentActions from './payment.actions';
+import {SearchPaymentsPayload} from './payment.actions';
+import {PlannedPaymentPage} from '../../../../services/portfolio/domain/individuallending/planned-payment-page.model';
+
+export interface State {
+ paymentPage: PlannedPaymentPage;
+ loading: boolean;
+ initialDisbursalDate: string;
+}
+
+const initialState: State = {
+ paymentPage: {
+ chargeNames: [],
+ elements: [],
+ totalElements: 0,
+ totalPages: 0
+ },
+ loading: false,
+ initialDisbursalDate: null
+};
+
+export function reducer(state = initialState, action: paymentActions.Actions): State {
+
+ switch (action.type) {
+
+ case paymentActions.SEARCH: {
+ const payload: SearchPaymentsPayload = action.payload;
+
+ return Object.assign({}, state, {
+ paymentPage: initialState.paymentPage,
+ initialDisbursalDate: payload.initialDisbursalDate,
+ loading: true
+ });
+ }
+
+ case paymentActions.SEARCH_COMPLETE: {
+ const paymentsPage: PlannedPaymentPage = action.payload;
+
+ return {
+ paymentPage: paymentsPage,
+ loading: false,
+ initialDisbursalDate: state.initialDisbursalDate
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+
+export const getPaymentPage = (state: State) => state.paymentPage;
+
+export const getLoading = (state: State) => state.loading;
diff --git a/src/app/customers/cases/store/search.reducer.ts b/src/app/customers/cases/store/search.reducer.ts
new file mode 100644
index 0000000..c96d053
--- /dev/null
+++ b/src/app/customers/cases/store/search.reducer.ts
@@ -0,0 +1,80 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as caseActions from './case.actions';
+import {SearchCasePayload} from './case.actions';
+import {FetchRequest} from '../../../services/domain/paging/fetch-request.model';
+import {FimsCase} from '../../../services/portfolio/domain/fims-case.model';
+
+export interface State {
+ cases: FimsCase[];
+ totalPages: number;
+ totalElements: number;
+ loading: boolean;
+ fetchRequest: FetchRequest;
+}
+
+const initialState: State = {
+ cases: [],
+ totalPages: 0,
+ totalElements: 0,
+ loading: false,
+ fetchRequest: null
+};
+
+export function reducer(state = initialState, action: caseActions.Actions): State {
+
+ switch (action.type) {
+
+ case caseActions.SEARCH: {
+ const payload: SearchCasePayload = action.payload;
+
+ return Object.assign({}, state, {
+ fetchRequest: payload.fetchRequest,
+ loading: true
+ });
+ }
+
+ case caseActions.SEARCH_COMPLETE: {
+ const casePage = action.payload;
+
+ return {
+ cases: casePage.elements,
+ loading: false,
+ fetchRequest: state.fetchRequest,
+ totalElements: casePage.totalElements,
+ totalPages: casePage.totalPages
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+
+export const getCases = (state: State) => state.cases;
+
+export const getFetchRequest = (state: State) => state.fetchRequest;
+
+export const getLoading = (state: State) => state.loading;
+
+export const getTotalPages = (state: State) => state.totalPages;
+
+export const getTotalElements = (state: State) => state.totalElements;
diff --git a/src/app/customers/cases/store/tasks/effects/notification.effects.ts b/src/app/customers/cases/store/tasks/effects/notification.effects.ts
new file mode 100644
index 0000000..05107df
--- /dev/null
+++ b/src/app/customers/cases/store/tasks/effects/notification.effects.ts
@@ -0,0 +1,48 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {NotificationService, NotificationType} from '../../../../../services/notification/notification.service';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as taskActions from '../task.actions';
+
+@Injectable()
+export class CaseTasksNotificationEffects {
+
+ @Effect({dispatch: false})
+ createTaskSuccess$: Observable<Action> = this.actions$
+ .ofType(taskActions.EXECUTE_TASK_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Task executed successfully'
+ }));
+
+ @Effect({dispatch: false})
+ executeCommandSuccess$: Observable<Action> = this.actions$
+ .ofType(taskActions.EXECUTE_TASK_FAIL)
+ .do(() => this.notificationService.send({
+ type: NotificationType.ALERT,
+ message: 'Task execution failed'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {
+ }
+
+}
diff --git a/src/app/customers/cases/store/tasks/effects/service.effects.ts b/src/app/customers/cases/store/tasks/effects/service.effects.ts
new file mode 100644
index 0000000..1b9cca2
--- /dev/null
+++ b/src/app/customers/cases/store/tasks/effects/service.effects.ts
@@ -0,0 +1,69 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Actions, Effect} from '@ngrx/effects';
+import {Injectable} from '@angular/core';
+import {Action} from '@ngrx/store';
+import {Observable} from 'rxjs/Observable';
+import * as taskActions from '../task.actions';
+import {of} from 'rxjs/observable/of';
+import {PortfolioService} from '../../../../../services/portfolio/portfolio.service';
+import {TaskInstance} from '../../../../../services/portfolio/domain/task-instance.model';
+import {TaskDefinition} from '../../../../../services/portfolio/domain/task-definition.model';
+import {FimsTaskInstance} from '../../model/fims-task-instance.model';
+
+@Injectable()
+export class CaseTasksApiEffects {
+
+ @Effect()
+ loadAll$: Observable<Action> = this.actions$
+ .ofType(taskActions.LOAD_ALL)
+ .map((action: taskActions.LoadAllAction) => action.payload)
+ .switchMap(payload => {
+ return Observable.combineLatest(
+ this.portfolioService.findAllTaskDefinitionsForProduct(payload.productId),
+ this.portfolioService.findAllTasksForCase(payload.productId, payload.caseId, true),
+ (definitions, tasks) => ({
+ definitions,
+ tasks
+ })
+ ).map(result => this.mapTaskInstances(result.tasks, result.definitions))
+ .map(taskInstances => new taskActions.LoadAllCompleteAction(taskInstances))
+ .catch(() => of(new taskActions.LoadAllCompleteAction([])));
+ });
+
+ @Effect()
+ executeTask$: Observable<Action> = this.actions$
+ .ofType(taskActions.EXECUTE_TASK)
+ .map((action: taskActions.ExecuteTaskAction) => action.payload)
+ .mergeMap(payload => this.portfolioService.taskForCaseExecuted(payload.productIdentifier, payload.caseIdentifier,
+ payload.taskIdentifier, payload.executed)
+ .map(() => new taskActions.ExecuteTaskActionSuccess(payload))
+ .catch(error => of(new taskActions.ExecuteTaskActionFail(error))));
+
+ private mapTaskInstances(taskInstances: TaskInstance[], taskDefinitions: TaskDefinition[]): FimsTaskInstance[] {
+ return taskInstances.map(instance => ({
+ taskDefinition: taskDefinitions.find(definition => definition.identifier === instance.taskIdentifier),
+ comment: instance.comment,
+ executedOn: instance.executedOn,
+ executedBy: instance.executedBy
+ }));
+ }
+
+ constructor(private actions$: Actions, private portfolioService: PortfolioService) {}
+}
diff --git a/src/app/customers/cases/store/tasks/task.actions.ts b/src/app/customers/cases/store/tasks/task.actions.ts
new file mode 100644
index 0000000..45b55e3
--- /dev/null
+++ b/src/app/customers/cases/store/tasks/task.actions.ts
@@ -0,0 +1,80 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {type} from '../../../../store/util';
+import {Action} from '@ngrx/store';
+import {Error} from '../../../../services/domain/error.model';
+import {FimsTaskInstance} from '../model/fims-task-instance.model';
+
+export const LOAD_ALL = type('[Case Task] Load All');
+export const LOAD_ALL_COMPLETE = type('[Case Task] Load All Complete');
+
+export const EXECUTE_TASK = type('[Case Task] Execute');
+export const EXECUTE_TASK_SUCCESS = type('[Case Task] Execute Success');
+export const EXECUTE_TASK_FAIL = type('[Case Task] Execute Fail');
+
+export interface LoadAllTasksPayload {
+ caseId: string;
+ productId: string;
+}
+
+export interface ExecuteTaskPayload {
+ action: string;
+ productIdentifier: string;
+ caseIdentifier: string;
+ taskIdentifier: string;
+ executedBy: string;
+ executed: boolean;
+}
+
+export class LoadAllAction implements Action {
+ readonly type = LOAD_ALL;
+
+ constructor(public payload: LoadAllTasksPayload) { }
+}
+
+export class LoadAllCompleteAction implements Action {
+ readonly type = LOAD_ALL_COMPLETE;
+
+ constructor(public payload: FimsTaskInstance[]) { }
+}
+
+export class ExecuteTaskAction implements Action {
+ readonly type = EXECUTE_TASK;
+
+ constructor(public payload: ExecuteTaskPayload) {}
+}
+
+export class ExecuteTaskActionSuccess implements Action {
+ readonly type = EXECUTE_TASK_SUCCESS;
+
+ constructor(public payload: ExecuteTaskPayload) {}
+}
+
+export class ExecuteTaskActionFail implements Action {
+ readonly type = EXECUTE_TASK_FAIL;
+
+ constructor(public payload: Error) {}
+}
+
+export type Actions
+ = LoadAllAction
+ | LoadAllCompleteAction
+ | ExecuteTaskAction
+ | ExecuteTaskActionSuccess
+ | ExecuteTaskActionFail;
diff --git a/src/app/customers/cases/store/tasks/tasks.reducer.ts b/src/app/customers/cases/store/tasks/tasks.reducer.ts
new file mode 100644
index 0000000..6e4101a
--- /dev/null
+++ b/src/app/customers/cases/store/tasks/tasks.reducer.ts
@@ -0,0 +1,91 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as task from './task.actions';
+import {StatusCommand} from '../model/fims-command.model';
+
+export interface State {
+ commands: StatusCommand[];
+}
+
+export const initialState: State = {
+ commands: [
+ { action: 'OPEN', preStates: ['CREATED'], tasks: []},
+ { action: 'APPROVE', preStates: ['PENDING'], tasks: []},
+ { action: 'DENY', preStates: ['PENDING'], tasks: []},
+ { action: 'CLOSE', preStates: ['APPROVED', 'ACTIVE'], tasks: []},
+ { action: 'DISBURSE', preStates: ['APPROVED'], tasks: []}
+ ]
+};
+
+export function reducer(state = initialState, action: task.Actions): State {
+
+ switch (action.type) {
+
+ case task.LOAD_ALL: {
+ return initialState;
+ }
+
+ case task.LOAD_ALL_COMPLETE: {
+ const entities = action.payload;
+
+ const commands = state.commands.map(command => {
+ return Object.assign({}, command, {
+ tasks: entities.filter(instance => instance.taskDefinition.actions.indexOf(command.action) > -1)
+ });
+ });
+
+ return {
+ commands
+ };
+ }
+
+ case task.EXECUTE_TASK_SUCCESS: {
+ const payload = action.payload;
+
+ const commands = state.commands.map(command => {
+ if (command.action !== payload.action) {
+ return command;
+ }
+
+ return Object.assign({}, command, {
+ tasks: command.tasks.map(task => {
+ if (task.taskDefinition.identifier !== payload.taskIdentifier) {
+ return task;
+ }
+
+ return Object.assign({}, task, {
+ executedOn: payload.executed ? new Date().toISOString() : undefined,
+ executedBy: payload.executedBy
+ });
+ })
+ });
+ });
+
+ return {
+ commands
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+export const getCommands = (state: State) => state.commands;
diff --git a/src/app/customers/contact.helper.ts b/src/app/customers/contact.helper.ts
new file mode 100644
index 0000000..f72a1e7
--- /dev/null
+++ b/src/app/customers/contact.helper.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ContactDetail} from '../services/domain/contact/contact-detail.model';
+
+export function getContactDetailValueByType(contactDetails: ContactDetail[], type: string): string {
+ const items = contactDetails.filter(contact => contact.type === type);
+ return items.length ? items[0].value : '';
+}
diff --git a/src/app/customers/customFields/catalog-exists.guard.ts b/src/app/customers/customFields/catalog-exists.guard.ts
new file mode 100644
index 0000000..be567db
--- /dev/null
+++ b/src/app/customers/customFields/catalog-exists.guard.ts
@@ -0,0 +1,66 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import * as fromCustomers from '../store/index';
+import {CustomersStore} from '../store/index';
+import {CatalogService} from '../../services/catalog/catalog.service';
+import {ExistsGuardService} from '../../common/guards/exists-guard';
+import {Observable} from 'rxjs/Observable';
+import {LoadAction} from '../store/catalogs/catalog.actions';
+import {of} from 'rxjs/observable/of';
+
+@Injectable()
+export class CatalogExistsGuard implements CanActivate {
+
+ constructor(private store: CustomersStore,
+ private catalogService: CatalogService,
+ private existsGuardService: ExistsGuardService) {
+ }
+
+ hasCatalogInStore(): Observable<boolean> {
+ const timestamp$: Observable<number> = this.store.select(fromCustomers.getCustomerCatalogLoadedAt);
+
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ hasCatalogInApi(id: string): Observable<boolean> {
+ return this.catalogService.findCatalog(id, true)
+ .catch(() => {
+ return Observable.of(null);
+ })
+ .map(catalogEntity => new LoadAction(catalogEntity))
+ .do((action: LoadAction) => this.store.dispatch(action))
+ .map(catalog => !!catalog);
+ }
+
+ hasCatalog(id: string): Observable<boolean> {
+ return this.hasCatalogInStore()
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+ return this.hasCatalogInApi(id);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasCatalog('customers');
+ }
+}
diff --git a/src/app/customers/customFields/catalog.detail.component.html b/src/app/customers/customFields/catalog.detail.component.html
new file mode 100644
index 0000000..963fc23
--- /dev/null
+++ b/src/app/customers/customFields/catalog.detail.component.html
@@ -0,0 +1,63 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Custom fields' | translate }}" [navigateBackTo]="['../../../']">
+ <fims-layout-card-over-header-menu *ngIf="(catalog$ | async) as catalog">
+ <button mat-icon-button (click)="deleteCatalog(catalog)" title="{{'Delete this catalog' | translate}}">
+ <mat-icon>delete</mat-icon>
+ </button>
+ </fims-layout-card-over-header-menu>
+ <ng-container *ngIf="(catalog$ | async) as catalog; else elseBlock">
+ <div class="mat-content inset" flex>
+ <div layout="row">
+ <mat-list>
+ <mat-list-item>
+ <h3 matLine translate>Name</h3>
+ <p matLine>{{catalog.name}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Description</h3>
+ <p matLine>{{catalog.description}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Created by</h3>
+ <p matLine>{{catalog.createdBy}} - {{catalog.createdOn | date:'medium'}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Last modified by</h3>
+ <p matLine>{{catalog.lastModifiedBy}} - {{catalog.lastModifiedOn | date:'medium'}}</p>
+ </mat-list-item>
+ </mat-list>
+ </div>
+ <fims-data-table flex
+ [columns]="columns"
+ [data]="fieldData$ | async"
+ (onActionCellClick)="rowSelect($event)">
+ </fims-data-table>
+ </div>
+ </ng-container>
+ <ng-template #elseBlock>
+ <td-message label="{{'No custom fields found' | translate }}"
+ color="warn" icon="error">
+ <button td-message-actions mat-button [routerLink]="['edit']"
+ *hasPermission="{ id: 'catalog_catalogs', accessLevel: 'CHANGE'}" translate>CREATE CUSTOM FIELDS
+ </button>
+ </td-message>
+ </ng-template>
+</fims-layout-card-over>
+
+
diff --git a/src/app/customers/customFields/catalog.detail.component.ts b/src/app/customers/customFields/catalog.detail.component.ts
new file mode 100644
index 0000000..bc12679
--- /dev/null
+++ b/src/app/customers/customFields/catalog.detail.component.ts
@@ -0,0 +1,84 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component} from '@angular/core';
+import {Catalog} from '../../services/catalog/domain/catalog.model';
+import {Observable} from 'rxjs/Observable';
+import {CustomersStore} from '../store/index';
+import * as fromCustomers from '../store';
+import {TableData} from '../../common/data-table/data-table.component';
+import {dataTypes} from './domain/datatype-types.model';
+import {TdDialogService} from '@covalent/core';
+import {DELETE} from '../store/catalogs/catalog.actions';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Field} from '../../services/catalog/domain/field.model';
+
+@Component({
+ templateUrl: './catalog.detail.component.html'
+})
+export class CatalogDetailComponent {
+
+ catalog$: Observable<Catalog>;
+
+ fieldData$: Observable<TableData>;
+
+ columns: any[] = [
+ { name: 'identifier', label: 'Identifier' },
+ { name: 'dataType', label: 'Data type', format: value => dataTypes.find(type => type.type === value).label },
+ { name: 'label', label: 'Label' },
+ { name: 'hint', label: 'Hint' },
+ { name: 'description', label: 'Description' },
+ { name: 'mandatory', label: 'Mandatory' }
+ ];
+
+ constructor(private store: CustomersStore, private dialogService: TdDialogService, private route: ActivatedRoute,
+ private router: Router) {
+ this.catalog$ = store.select(fromCustomers.getCustomerCatalog)
+ .filter(catalog => !!catalog);
+
+ this.fieldData$ = this.catalog$
+ .map(catalog => ({
+ data: catalog.fields,
+ totalElements: catalog.fields.length,
+ totalPages: 1
+ }));
+ }
+
+ rowSelect(field: Field): void {
+ this.router.navigate(['field/detail', field.identifier], { relativeTo: this.route });
+ }
+
+ confirmDeletion(): Observable<boolean> {
+ return this.dialogService.openConfirm({
+ message: 'Do you want to delete this catalog?',
+ title: 'Confirm deletion',
+ acceptButton: 'DELETE CATALOG',
+ }).afterClosed();
+ }
+
+ deleteCatalog(catalog: Catalog): void {
+ this.confirmDeletion()
+ .filter(accept => accept)
+ .subscribe(() => {
+ this.store.dispatch({ type: DELETE, payload: {
+ catalog,
+ activatedRoute: this.route
+ } });
+ });
+ }
+}
diff --git a/src/app/customers/customFields/components/field.component.html b/src/app/customers/customFields/components/field.component.html
new file mode 100644
index 0000000..258d4c6
--- /dev/null
+++ b/src/app/customers/customFields/components/field.component.html
@@ -0,0 +1,53 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ng-container [formGroup]="form">
+ <div layout="row">
+ <fims-id-input [form]="form" controlName="identifier"></fims-id-input>
+ <fims-text-input [form]="form" controlName="label" placeholder="{{'Label' | translate}}"></fims-text-input>
+ </div>
+ <fims-text-input [form]="form" controlName="hint" placeholder="{{'Hint(Optional)' | translate}}"></fims-text-input>
+ <div layout="row">
+ <mat-form-field layout-margin flex>
+ <textarea matInput placeholder="{{'Description(Optional)' | translate}}" formControlName="description"></textarea>
+ </mat-form-field>
+ </div>
+ <mat-checkbox layout-margin formControlName="mandatory" translate>Mandatory?</mat-checkbox>
+ <mat-radio-group formControlName="dataType">
+ <mat-radio-button *ngFor="let type of dataTypeOptions" [value]="type.type" layout-margin>
+ {{type.label}}
+ </mat-radio-button>
+ </mat-radio-group>
+ <div layout="row">
+ <fims-text-input [hideIfDisabled]="true" type="number" [form]="form" controlName="length" placeholder="{{'Length' | translate}}"></fims-text-input>
+ <fims-text-input [hideIfDisabled]="true" type="number" [form]="form" controlName="precision" placeholder="{{'Max digits' | translate}}"></fims-text-input>
+ <fims-text-input [hideIfDisabled]="true" type="number" [form]="form" controlName="minValue" placeholder="{{'Min value' | translate}}"></fims-text-input>
+ <fims-text-input [hideIfDisabled]="true" type="number" [form]="form" controlName="maxValue" placeholder="{{'Max value' | translate}}"></fims-text-input>
+ </div>
+ <div layout-gt-xs="column" layout-margin formArrayName="options" *ngIf="options.enabled">
+ <h4 translate>Options</h4>
+ <div *ngFor="let option of options.controls; let i=index" [formGroupName]="i" layout="row">
+ <fims-text-input [form]="option" controlName="label" placeholder="{{'Label' | translate}}"></fims-text-input>
+ <fims-text-input type="number" [form]="option" controlName="value" placeholder="{{'Value' | translate}}"></fims-text-input>
+ <button mat-button (click)="removeOption(i)">{{'Remove option' | translate}}</button>
+ </div>
+ <p *ngIf="form.get('options').hasError('optionValueUnique')" class="tc-red-600" translate>
+ Values can't overlap with other values.
+ </p>
+ <button mat-button (click)="addOption()">{{'Add option' | translate}}</button>
+ </div>
+</ng-container>
diff --git a/src/app/customers/customFields/components/field.component.ts b/src/app/customers/customFields/components/field.component.ts
new file mode 100644
index 0000000..70a4e22
--- /dev/null
+++ b/src/app/customers/customFields/components/field.component.ts
@@ -0,0 +1,106 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {FormArray, FormGroup} from '@angular/forms';
+import {Component, EventEmitter, Input, OnChanges, Output, SimpleChanges} from '@angular/core';
+import {DataTypeOption, dataTypes} from '../domain/datatype-types.model';
+
+@Component({
+ selector: 'fims-custom-field-form',
+ templateUrl: './field.component.html'
+})
+export class FieldFormComponent implements OnChanges {
+
+ @Input() form: FormGroup;
+
+ @Input() editMode: boolean;
+
+ @Output('onAddOption') onAddOption = new EventEmitter<void>();
+
+ @Output('onRemoveOption') onRemoveOption = new EventEmitter<number>();
+
+ dataTypeOptions: DataTypeOption[] = dataTypes;
+
+ constructor() {
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes.form) {
+ this.form.get('dataType').valueChanges
+ .subscribe(dataType => this.toggleDataType());
+
+ this.toggleDataType();
+ }
+ }
+
+ private toggleDataType(): void {
+ const dataType = this.form.get('dataType').value;
+
+ const lengthControl = this.form.get('length');
+ const precisionControl = this.form.get('precision');
+ const minValueControl = this.form.get('minValue');
+ const maxValueControl = this.form.get('maxValue');
+ const optionsControl = this.form.get('options');
+
+ lengthControl.disable();
+ precisionControl.disable();
+ minValueControl.disable();
+ maxValueControl.disable();
+ optionsControl.disable();
+
+ switch (dataType) {
+ case 'TEXT': {
+ if (!this.editMode) {
+ lengthControl.enable();
+ }
+ break;
+ }
+
+ case 'NUMBER': {
+ if (!this.editMode) {
+ precisionControl.enable();
+ minValueControl.enable();
+ maxValueControl.enable();
+ }
+ break;
+ }
+
+ case 'SINGLE_SELECTION':
+ case 'MULTI_SELECTION': {
+ optionsControl.enable();
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+
+ addOption(): void {
+ this.onAddOption.emit();
+ }
+
+ removeOption(index: number): void {
+ this.onRemoveOption.emit(index);
+ }
+
+ get options(): FormArray {
+ return this.form.get('options') as FormArray;
+ }
+
+}
diff --git a/src/app/customers/customFields/components/value.component.html b/src/app/customers/customFields/components/value.component.html
new file mode 100644
index 0000000..5b3dc2c
--- /dev/null
+++ b/src/app/customers/customFields/components/value.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<mat-list-item *ngFor="let field of customCatalog?.fields" [ngSwitch]="field.dataType">
+ <h3 matLine translate>{{field.label}}</h3>
+ <p matLine *ngSwitchCase="'TEXT'">{{field.value}}</p>
+ <p matLine *ngSwitchCase="'NUMBER'">{{field.value | number}}</p>
+ <p matLine *ngSwitchCase="'DATE'">{{field.value | date:'medium'}}</p>
+ <p matLine *ngSwitchCase="'SINGLE_SELECTION'">{{field.value}}</p>
+ <p matLine *ngSwitchCase="'MULTI_SELECTION'">{{field.value}}</p>
+</mat-list-item>
diff --git a/src/app/customers/customFields/components/value.component.ts b/src/app/customers/customFields/components/value.component.ts
new file mode 100644
index 0000000..bd7bcaf
--- /dev/null
+++ b/src/app/customers/customFields/components/value.component.ts
@@ -0,0 +1,101 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input, OnChanges, SimpleChanges} from '@angular/core';
+import {Catalog} from '../../../services/catalog/domain/catalog.model';
+import {Value} from '../../../services/catalog/domain/value.model';
+import {Field, FieldDataType} from '../../../services/catalog/domain/field.model';
+import {Option} from '../../../services/catalog/domain/option.model';
+
+interface CustomCatalog {
+ label: string;
+ fields: CustomDetailField[];
+}
+
+interface CustomDetailField {
+ label: string;
+ value: string;
+ dataType: FieldDataType;
+}
+
+@Component({
+ selector: 'fims-customer-custom-values',
+ templateUrl: './value.component.html'
+})
+export class CustomerCustomValuesComponent implements OnChanges {
+
+ @Input() catalog: Catalog;
+
+ @Input() values: Value[];
+
+ customCatalog: CustomCatalog;
+
+ ngOnChanges(changes: SimpleChanges): void {
+ this.customCatalog = this.buildCustomCatalogs(this.values, this.catalog);
+ }
+
+ private buildCustomCatalogs(values: Value[], catalog: Catalog): CustomCatalog {
+ if (!values || !catalog || !catalog.fields) {
+ return;
+ }
+
+ const customCatalog: CustomCatalog = {
+ label: catalog.name,
+ fields: []
+ };
+
+ if (values) {
+ for (const value of values) {
+ const foundField: Field = catalog.fields.find((field: Field) => field.identifier === value.fieldIdentifier );
+
+ let valueString: string = value.value;
+
+ switch (foundField.dataType) {
+ case 'SINGLE_SELECTION': {
+ const foundOption = foundField.options.find((option: Option) => option.value === Number(valueString));
+ valueString = foundOption.label;
+ break;
+ }
+
+ case 'MULTI_SELECTION': {
+ const optionValues = valueString ? valueString.split(',').map(optionValue => Number(optionValue)) : [];
+ const foundOptions = foundField.options
+ .filter((option: Option) => optionValues.indexOf(option.value) > -1)
+ .map((option: Option) => option.label);
+ valueString = foundOptions.join(', ');
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ const customField: CustomDetailField = {
+ label: foundField.label,
+ value: valueString,
+ dataType: foundField.dataType
+ };
+
+ customCatalog.fields.push(customField);
+ }
+ }
+
+ return customCatalog;
+ };
+}
diff --git a/src/app/customers/customFields/domain/datatype-types.model.ts b/src/app/customers/customFields/domain/datatype-types.model.ts
new file mode 100644
index 0000000..69d3d6f
--- /dev/null
+++ b/src/app/customers/customFields/domain/datatype-types.model.ts
@@ -0,0 +1,32 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {FieldDataType} from '../../../services/catalog/domain/field.model';
+
+export interface DataTypeOption {
+ type: FieldDataType;
+ label: string;
+}
+
+export const dataTypes: DataTypeOption[] = [
+ { type: 'TEXT', label: 'Text' },
+ { type: 'NUMBER', label: 'Number' },
+ { type: 'DATE', label: 'Date' },
+ { type: 'SINGLE_SELECTION', label: 'Single selection' },
+ { type: 'MULTI_SELECTION', label: 'Multi selection' }
+];
diff --git a/src/app/customers/customFields/fields/field-exists.guard.ts b/src/app/customers/customFields/fields/field-exists.guard.ts
new file mode 100644
index 0000000..fe3de8f
--- /dev/null
+++ b/src/app/customers/customFields/fields/field-exists.guard.ts
@@ -0,0 +1,44 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import {Injectable} from '@angular/core';
+import {CustomersStore} from '../../store/index';
+import {ExistsGuardService} from '../../../common/guards/exists-guard';
+import {Observable} from 'rxjs/Observable';
+import * as fromCustomers from '../../store';
+
+@Injectable()
+export class FieldExistsGuard implements CanActivate {
+
+ constructor(private store: CustomersStore,
+ private existsGuardService: ExistsGuardService) {
+ }
+
+ hasFieldInCatalog(id: string): Observable<boolean> {
+ const getField$ = this.store.select(fromCustomers.getCustomerCatalog)
+ .map(catalog => catalog.fields.find(field => field.identifier === id))
+ .map(field => !!field);
+
+ return this.existsGuardService.routeTo404OnError(getField$);
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasFieldInCatalog(route.params['fieldId']);
+ }
+}
diff --git a/src/app/customers/customFields/fields/field.detail.component.html b/src/app/customers/customFields/fields/field.detail.component.html
new file mode 100644
index 0000000..87fd872
--- /dev/null
+++ b/src/app/customers/customFields/fields/field.detail.component.html
@@ -0,0 +1,73 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Custom field' | translate }}" [navigateBackTo]="['../../../../../../']" *ngIf="(field$ | async) as field">
+ <fims-layout-card-over-header-menu *ngIf="(catalog$ | async) as catalog">
+ <button mat-icon-button (click)="deleteField(catalog.identifier, field)" title="{{'Delete this field' | translate}}">
+ <mat-icon>delete</mat-icon>
+ </button>
+ </fims-layout-card-over-header-menu>
+ <div class="mat-content inset" flex>
+ <div layout="row">
+ <mat-list>
+ <mat-list-item>
+ <h3 matLine translate>Data type</h3>
+ <p matLine>{{field.dataType}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Label</h3>
+ <p matLine>{{field.label}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Hint</h3>
+ <p matLine>{{field.hint}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Description</h3>
+ <p matLine>{{field.description}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Mandatory</h3>
+ <p matLine>{{field.mandatory}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Length</h3>
+ <p matLine>{{field.length}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Max digits</h3>
+ <p matLine>{{field.precision}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Min value</h3>
+ <p matLine>{{field.minValue}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Max value</h3>
+ <p matLine>{{field.maxValue}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Created by</h3>
+ <p matLine>{{field.createdBy}} - {{field.createdOn | date:'medium'}}</p>
+ </mat-list-item>
+ </mat-list>
+ </div>
+ </div>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Edit field' | translate}}" icon="mode_edit" [link]="['edit']"
+ [permission]="{ id: 'catalog_catalogs', accessLevel: 'CHANGE'}">
+</fims-fab-button>
diff --git a/src/app/customers/customFields/fields/field.detail.component.ts b/src/app/customers/customFields/fields/field.detail.component.ts
new file mode 100644
index 0000000..ec13f20
--- /dev/null
+++ b/src/app/customers/customFields/fields/field.detail.component.ts
@@ -0,0 +1,62 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {DELETE_FIELD} from '../../store/catalogs/catalog.actions';
+import {Field} from '../../../services/catalog/domain/field.model';
+import {CustomersStore} from '../../store/index';
+import {TdDialogService} from '@covalent/core';
+import * as fromCustomers from '../../store';
+import {ActivatedRoute} from '@angular/router';
+import {Catalog} from '../../../services/catalog/domain/catalog.model';
+
+@Component({
+ templateUrl: './field.detail.component.html'
+})
+export class FieldDetailComponent {
+
+ catalog$: Observable<Catalog>;
+
+ field$: Observable<Field>;
+
+ constructor(private store: CustomersStore, private dialogService: TdDialogService, private route: ActivatedRoute) {
+ this.catalog$ = store.select(fromCustomers.getCustomerCatalog);
+ this.field$ = store.select(fromCustomers.getSelectedField);
+ }
+
+ confirmDeletion(): Observable<boolean> {
+ return this.dialogService.openConfirm({
+ message: 'Do you want to delete this field?',
+ title: 'Confirm deletion',
+ acceptButton: 'DELETE FIELD',
+ }).afterClosed();
+ }
+
+ deleteField(catalogIdentifier: string, field: Field): void {
+ this.confirmDeletion()
+ .filter(accept => accept)
+ .subscribe(() => {
+ this.store.dispatch({ type: DELETE_FIELD, payload: {
+ catalogIdentifier,
+ field,
+ activatedRoute: this.route
+ } });
+ });
+ }
+}
diff --git a/src/app/customers/customFields/fields/field.index.component.html b/src/app/customers/customFields/fields/field.index.component.html
new file mode 100644
index 0000000..ca721b3
--- /dev/null
+++ b/src/app/customers/customFields/fields/field.index.component.html
@@ -0,0 +1,18 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<router-outlet></router-outlet>
diff --git a/src/app/customers/customFields/fields/field.index.component.ts b/src/app/customers/customFields/fields/field.index.component.ts
new file mode 100644
index 0000000..c1f4b1b
--- /dev/null
+++ b/src/app/customers/customFields/fields/field.index.component.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {CustomersStore} from '../../store/index';
+import {ActivatedRoute} from '@angular/router';
+import {Subscription} from 'rxjs/Subscription';
+import {SelectFieldAction} from '../../store/catalogs/catalog.actions';
+
+@Component({
+ templateUrl: './field.index.component.html'
+})
+export class FieldIndexComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ constructor(private route: ActivatedRoute, private store: CustomersStore) {}
+
+ ngOnInit(): void {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectFieldAction(params['fieldId']))
+ .subscribe(this.store);
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ }
+}
diff --git a/src/app/customers/customFields/fields/form/edit.form.component.html b/src/app/customers/customFields/fields/form/edit.form.component.html
new file mode 100644
index 0000000..37e08f7
--- /dev/null
+++ b/src/app/customers/customFields/fields/form/edit.form.component.html
@@ -0,0 +1,24 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Change custom field' | translate}}" [navigateBackTo]="['../']" *ngIf="(catalog$ | async) as catalog">
+ <fims-catalog-custom-field-form
+ [field]="field$ | async"
+ (onSave)="onSave(catalog.identifier, $event)"
+ (onCancel)="onCancel()">
+ </fims-catalog-custom-field-form>
+</fims-layout-card-over>
diff --git a/src/app/customers/customFields/fields/form/edit.form.component.ts b/src/app/customers/customFields/fields/form/edit.form.component.ts
new file mode 100644
index 0000000..79cc7c1
--- /dev/null
+++ b/src/app/customers/customFields/fields/form/edit.form.component.ts
@@ -0,0 +1,56 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component} from '@angular/core';
+import * as fromCustomers from '../../../store/index';
+import {CustomersStore} from '../../../store/index';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Observable} from 'rxjs/Observable';
+import {Field} from '../../../../services/catalog/domain/field.model';
+import {UPDATE_FIELD} from '../../../store/catalogs/catalog.actions';
+import {Catalog} from '../../../../services/catalog/domain/catalog.model';
+
+@Component({
+ templateUrl: './edit.form.component.html'
+})
+export class EditCatalogFieldFormComponent {
+
+ catalog$: Observable<Catalog>;
+
+ field$: Observable<Field>;
+
+ constructor(private store: CustomersStore, private router: Router, private route: ActivatedRoute) {
+ this.catalog$ = store.select(fromCustomers.getCustomerCatalog);
+ this.field$ = store.select(fromCustomers.getSelectedField);
+ }
+
+ onSave(catalogIdentifier: string, field: Field): void {
+ this.store.dispatch({
+ type: UPDATE_FIELD,
+ payload: {
+ catalogIdentifier,
+ field,
+ activatedRoute: this.route
+ }
+ });
+ }
+
+ onCancel(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/customers/customFields/fields/form/form.component.html b/src/app/customers/customFields/fields/form/form.component.html
new file mode 100644
index 0000000..e3c0c12
--- /dev/null
+++ b/src/app/customers/customFields/fields/form/form.component.html
@@ -0,0 +1,36 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Update custom field' | translate}}" [state]="form.valid ? 'complete' : form.pristine ? 'none' : 'required'">
+ <fims-custom-field-form
+ [editMode]="true"
+ [form]="form"
+ (onAddOption)="addOption()"
+ (onRemoveOption)="removeOption($event)">
+ </fims-custom-field-form>
+ <ng-template td-step-actions>
+ <fims-form-final-action
+ [resourceName]="'CUSTOM FIELD'"
+ [editMode]="true"
+ [disabled]="!form.valid"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/customers/customFields/fields/form/form.component.ts b/src/app/customers/customFields/fields/form/form.component.ts
new file mode 100644
index 0000000..40f894c
--- /dev/null
+++ b/src/app/customers/customFields/fields/form/form.component.ts
@@ -0,0 +1,87 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild} from '@angular/core';
+import {FormGroup} from '@angular/forms';
+import {TdStepComponent} from '@covalent/core';
+import {Field} from '../../../../services/catalog/domain/field.model';
+import {Option} from '../../../../services/catalog/domain/option.model';
+import {FieldFormService} from '../../services/field-form.service';
+
+@Component({
+ selector: 'fims-catalog-custom-field-form',
+ templateUrl: './form.component.html'
+})
+export class CatalogFieldFormComponent implements OnInit, OnChanges {
+
+ form: FormGroup;
+
+ @ViewChild('detailsStep') detailsStep: TdStepComponent;
+
+ @Input('field') field: Field;
+
+ @Output('onSave') onSave = new EventEmitter<Field>();
+
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ constructor(private fieldFormService: FieldFormService) {
+ this.form = fieldFormService.buildForm();
+ }
+
+ ngOnInit(): void {
+ this.detailsStep.open();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes.field) {
+ this.fieldFormService.resetForm(this.form, this.field);
+
+ this.form.get('identifier').disable();
+ this.form.get('dataType').disable();
+ this.form.get('length').disable();
+ this.form.get('precision').disable();
+ this.form.get('minValue').disable();
+ this.form.get('maxValue').disable();
+ }
+ }
+
+ save(): void {
+ const field: Field = Object.assign({}, this.field, {
+ label: this.form.get('label').value,
+ hint: this.form.get('hint').value,
+ description: this.form.get('description').value,
+ mandatory: this.form.get('mandatory').value,
+ options: this.form.get('options').value
+ });
+
+ this.onSave.emit(field);
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+ addOption(option?: Option): void {
+ this.fieldFormService.addOption(this.form, option);
+ }
+
+ removeOption(index: number): void {
+ this.fieldFormService.removeOption(this.form, index);
+ }
+
+}
diff --git a/src/app/customers/customFields/form/create.form.component.html b/src/app/customers/customFields/form/create.form.component.html
new file mode 100644
index 0000000..d21bae9
--- /dev/null
+++ b/src/app/customers/customFields/form/create.form.component.html
@@ -0,0 +1,24 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Create custom fields' | translate}}" [navigateBackTo]="['../']">
+ <fims-customer-catalog-form
+ [catalog]="catalog"
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()">
+ </fims-customer-catalog-form>
+</fims-layout-card-over>
diff --git a/src/app/customers/customFields/form/create.form.component.ts b/src/app/customers/customFields/form/create.form.component.ts
new file mode 100644
index 0000000..ddc9d11
--- /dev/null
+++ b/src/app/customers/customFields/form/create.form.component.ts
@@ -0,0 +1,51 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component} from '@angular/core';
+import {CustomersStore} from '../../store/index';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Catalog} from '../../../services/catalog/domain/catalog.model';
+import {CREATE} from '../../store/catalogs/catalog.actions';
+
+@Component({
+ templateUrl: './create.form.component.html'
+})
+export class CreateCustomerCatalogFormComponent {
+
+ catalog: Catalog = {
+ identifier: 'customers',
+ name: '',
+ fields: []
+ };
+
+ constructor(private store: CustomersStore, private router: Router, private route: ActivatedRoute) {}
+
+ onSave(catalog: Catalog): void {
+ this.store.dispatch({
+ type: CREATE,
+ payload: {
+ catalog,
+ activatedRoute: this.route
+ }
+ });
+ }
+
+ onCancel(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/customers/customFields/form/form.component.html b/src/app/customers/customFields/form/form.component.html
new file mode 100644
index 0000000..9151d2f
--- /dev/null
+++ b/src/app/customers/customFields/form/form.component.html
@@ -0,0 +1,56 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Create custom fields' | translate}}" [state]="form.valid ? 'complete' : form.pristine ? 'none' : 'required'">
+ <form [formGroup]="form">
+ <fims-text-input [form]="form" controlName="name" placeholder="{{'Name' | translate}}"></fims-text-input>
+ <div layout="row">
+ <mat-form-field layout-margin flex>
+ <textarea matInput placeholder="{{'Description(Optional)' | translate}}" formControlName="description"></textarea>
+ </mat-form-field>
+ </div>
+ <div layout-gt-xs="column" layout-margin formArrayName="fields">
+ <h4 translate>Fields</h4>
+ <mat-card *ngFor="let field of fields; let i=index" [formGroupName]="i">
+ <mat-card-content>
+ <fims-custom-field-form
+ [form]="field"
+ (onAddOption)="addOption(field)"
+ (onRemoveOption)="removeOption(field, $event)">
+ </fims-custom-field-form>
+ </mat-card-content>
+ <mat-card-actions>
+ <button mat-button (click)="removeField(i)">{{'Remove field' | translate}}</button>
+ </mat-card-actions>
+ </mat-card>
+ <div layout="row">
+ <button mat-button (click)="addField()">{{'Add field' | translate}}</button>
+ </div>
+ </div>
+ </form>
+ <ng-template td-step-actions>
+ <fims-form-final-action
+ [resourceName]="'CUSTOM FIELDS'"
+ [editMode]="false"
+ [disabled]="!form.valid"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/customers/customFields/form/form.component.ts b/src/app/customers/customFields/form/form.component.ts
new file mode 100644
index 0000000..d4f195d
--- /dev/null
+++ b/src/app/customers/customFields/form/form.component.ts
@@ -0,0 +1,116 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild} from '@angular/core';
+import {AbstractControl, FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {TdStepComponent} from '@covalent/core';
+import {Catalog} from '../../../services/catalog/domain/catalog.model';
+import {Field} from '../../../services/catalog/domain/field.model';
+import {FieldFormService} from '../services/field-form.service';
+import {Option} from '../../../services/catalog/domain/option.model';
+
+@Component({
+ selector: 'fims-customer-catalog-form',
+ templateUrl: './form.component.html'
+})
+export class CustomerCatalogFormComponent implements OnInit, OnChanges {
+
+ form: FormGroup;
+
+ @ViewChild('detailsStep') detailsStep: TdStepComponent;
+
+ @Input('catalog') catalog: Catalog;
+
+ @Output('onSave') onSave = new EventEmitter<Catalog>();
+
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder, private fieldFormService: FieldFormService) {
+ this.form = this.formBuilder.group({
+ name: ['', [Validators.required, Validators.maxLength(256)]],
+ description: ['', [Validators.maxLength(4096)]],
+ fields: this.initFields([])
+ });
+ }
+
+ ngOnInit(): void {
+ this.detailsStep.open();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes.catalog) {
+ this.form.reset({
+ name: this.catalog.name,
+ description: this.catalog.description
+ });
+
+ this.catalog.fields.forEach(field => this.addField(field));
+ }
+ }
+
+ save(): void {
+ const catalog: Catalog = Object.assign({}, this.catalog, {
+ name: this.form.get('name').value,
+ description: this.form.get('description').value,
+ fields: this.form.get('fields').value
+ });
+
+ this.onSave.emit(catalog);
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+ private initFields(fields: Field[]): FormArray {
+ const formControls: FormGroup[] = [];
+ fields.forEach(allocation => formControls.push(this.initField(allocation)));
+ return this.formBuilder.array(formControls);
+ }
+
+ private initField(field?: Field): FormGroup {
+ const formGroup = this.fieldFormService.buildForm();
+
+ this.fieldFormService.resetForm(formGroup, field);
+
+ return formGroup;
+ }
+
+ addField(field?: Field): void {
+ const fields: FormArray = this.form.get('fields') as FormArray;
+ fields.push(this.initField(field));
+ }
+
+ removeField(index: number): void {
+ const fields: FormArray = this.form.get('fields') as FormArray;
+ fields.removeAt(index);
+ }
+
+ get fields(): AbstractControl[] {
+ const fields: FormArray = this.form.get('fields') as FormArray;
+ return fields.controls;
+ }
+
+ addOption(form: FormGroup, option?: Option): void {
+ this.fieldFormService.addOption(form, option);
+ }
+
+ removeOption(form: FormGroup, index: number): void {
+ this.fieldFormService.removeOption(form, index);
+ }
+}
diff --git a/src/app/customers/customFields/services/field-form.service.ts b/src/app/customers/customFields/services/field-form.service.ts
new file mode 100644
index 0000000..6fd0fd6
--- /dev/null
+++ b/src/app/customers/customFields/services/field-form.service.ts
@@ -0,0 +1,83 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {Injectable} from '@angular/core';
+import {Field} from '../../../services/catalog/domain/field.model';
+import {Option} from '../../../services/catalog/domain/option.model';
+import {FimsValidators} from '../../../common/validator/validators';
+import {optionValueUnique} from './option-value-unique.validator';
+
+@Injectable()
+export class FieldFormService {
+
+ constructor(private formBuilder: FormBuilder) {
+ }
+
+ buildForm(): FormGroup {
+ return this.formBuilder.group({
+ identifier: ['', [Validators.required]],
+ dataType: ['', [Validators.required]],
+ label: ['', [Validators.required, Validators.maxLength(256)]],
+ hint: ['', [Validators.maxLength(512)]],
+ description: ['', [Validators.maxLength(4096)]],
+ mandatory: [''],
+ length: ['', [FimsValidators.minValue(1), FimsValidators.maxScale(0)]],
+ precision: ['', [FimsValidators.minValue(0), FimsValidators.maxScale(0)]],
+ minValue: ['', [FimsValidators.minValue(0)]],
+ maxValue: ['', [FimsValidators.minValue(1)]],
+ options: this.formBuilder.array([], optionValueUnique)
+ });
+ }
+
+ resetForm(form: FormGroup, field: Field): void {
+ form.reset({
+ identifier: field ? field.identifier : '',
+ dataType: field ? field.dataType : 'TEXT',
+ label: field ? field.label : '',
+ hint: field ? field.hint : '',
+ description: field ? field.description : '',
+ mandatory: field ? field.mandatory : false,
+ length: field ? field.length : 1,
+ precision: field ? field.precision : 0,
+ minValue: field ? field.minValue : 0,
+ maxValue: field ? field.maxValue : 1
+ });
+
+ if (field && field.options) {
+ field.options.forEach(option => this.addOption(form, option));
+ }
+ }
+
+ initOption(option?: Option): FormGroup {
+ return this.formBuilder.group({
+ label: [option ? option.label : '', [Validators.required, Validators.maxLength(256)]],
+ value: [option ? option.value : '', [Validators.required, FimsValidators.minValue(0)]]
+ });
+ }
+
+ addOption(form: FormGroup, option?: Option): void {
+ const options: FormArray = form.get('options') as FormArray;
+ options.push(this.initOption(option));
+ }
+
+ removeOption(form: FormGroup, index: number): void {
+ const options: FormArray = form.get('options') as FormArray;
+ options.removeAt(index);
+ }
+}
diff --git a/src/app/customers/customFields/services/option-value-unique.validator.ts b/src/app/customers/customFields/services/option-value-unique.validator.ts
new file mode 100644
index 0000000..0fc3185
--- /dev/null
+++ b/src/app/customers/customFields/services/option-value-unique.validator.ts
@@ -0,0 +1,38 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {FormArray, FormGroup, ValidationErrors} from '@angular/forms';
+
+export function optionValueUnique(array: FormArray): ValidationErrors | null {
+ const options: FormGroup[] = array.controls as FormGroup[];
+
+ const values = options
+ .map(optionGroup => parseInt(optionGroup.get('value').value, 10));
+
+ const set = new Set();
+
+ values.forEach(number => set.add(number));
+
+ if (set.size !== values.length) {
+ return {
+ optionValueUnique: true
+ };
+ }
+
+ return null;
+}
diff --git a/src/app/customers/customer-exists.guard.ts b/src/app/customers/customer-exists.guard.ts
new file mode 100644
index 0000000..cec8fc7
--- /dev/null
+++ b/src/app/customers/customer-exists.guard.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import {Injectable} from '@angular/core';
+import * as fromCustomers from './store';
+import {Observable} from 'rxjs/Observable';
+import {LoadAction} from './store/customer.actions';
+import {of} from 'rxjs/observable/of';
+import {CustomerService} from '../services/customer/customer.service';
+import {CustomersStore} from './store/index';
+import {ExistsGuardService} from '../common/guards/exists-guard';
+
+@Injectable()
+export class CustomerExistsGuard implements CanActivate {
+
+ constructor(private store: CustomersStore,
+ private customerService: CustomerService,
+ private existsGuardService: ExistsGuardService) {
+ }
+
+ hasCustomerInStore(id: string): Observable<boolean> {
+ const timestamp$: Observable<number> = this.store.select(fromCustomers.getCustomerLoadedAt)
+ .map(loadedAt => loadedAt[id]);
+
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ hasCustomerInApi(id: string): Observable<boolean> {
+ const getCustomer$: Observable<any> = this.customerService.getCustomer(id)
+ .map(customerEntity => new LoadAction({
+ resource: customerEntity
+ }))
+ .do((action: LoadAction) => this.store.dispatch(action))
+ .map(customer => !!customer);
+
+ return this.existsGuardService.routeTo404OnError(getCustomer$);
+ }
+
+ hasCustomer(id: string): Observable<boolean> {
+ return this.hasCustomerInStore(id)
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+ return this.hasCustomerInApi(id);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasCustomer(route.params['id']);
+ }
+}
diff --git a/src/app/customers/customer.component.html b/src/app/customers/customer.component.html
new file mode 100644
index 0000000..7cd20a6
--- /dev/null
+++ b/src/app/customers/customer.component.html
@@ -0,0 +1,49 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Manage members' | translate}}">
+ <fims-layout-card-over-header-menu>
+ <td-search-box #searchBox placeholder="{{'Search' | translate}}" (search)="search($event)" [alwaysVisible]="false"></td-search-box>
+ </fims-layout-card-over-header-menu>
+ <fims-two-column-layout>
+ <mat-nav-list left>
+ <h3 mat-subheader translate>Management</h3>
+ <a mat-list-item [routerLink]="['tasks']" *hasPermission="{ id: 'customer_tasks', accessLevel: 'READ' }">
+ <mat-icon matListAvatar>playlist_add_check</mat-icon>
+ <h3 matLine translate>Tasks</h3>
+ <p matLine translate>Manage tasks</p>
+ </a>
+ <a mat-list-item [routerLink]="['catalog/detail']" *hasPermission="{ id: 'catalog_catalogs', accessLevel: 'READ' }">
+ <mat-icon matListAvatar>format_list_numbered</mat-icon>
+ <h3 matLine translate>Custom fields</h3>
+ <p matLine translate>Manage custom fields</p>
+ </a>
+ </mat-nav-list>
+ <fims-data-table
+ right
+ (onFetch)="fetchCustomers($event)"
+ (onActionCellClick)="rowSelect($event)"
+ [columns]="columns"
+ [data]="customerData$ | async"
+ [loading]="loading$ | async"
+ [sortable]="true"
+ [pageable]="true">
+ </fims-data-table>
+ </fims-two-column-layout>
+
+</fims-layout-card-over>
+<fims-fab-button title="{{'Create new member ' | translate}}" icon="add" [link]="['create']" [permission]="{ id: 'customer_customers', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/customers/customer.component.ts b/src/app/customers/customer.component.ts
new file mode 100644
index 0000000..5c52edb
--- /dev/null
+++ b/src/app/customers/customer.component.ts
@@ -0,0 +1,88 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import {ActivatedRoute, Params, Router} from '@angular/router';
+import {FetchRequest} from '../services/domain/paging/fetch-request.model';
+import {TableData, TableFetchRequest} from '../common/data-table/data-table.component';
+import {Customer} from '../services/customer/domain/customer.model';
+import {Observable} from 'rxjs/Observable';
+import * as fromRoot from '../store';
+import {SEARCH} from '../store/customer/customer.actions';
+import {CustomersStore} from './store/index';
+
+@Component({
+ templateUrl: './customer.component.html'
+})
+export class CustomerComponent implements OnInit {
+
+ customerData$: Observable<TableData>;
+
+ loading$: Observable<boolean>;
+
+ columns: any[] = [
+ { name: 'identifier', label: 'Id' },
+ { name: 'givenName', label: 'First Name' },
+ { name: 'surname', label: 'Last Name' },
+ { name: 'currentState', label: 'Current status' }
+ ];
+
+ private searchTerm: string;
+
+ private lastFetchRequest: FetchRequest = {};
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: CustomersStore) {}
+
+ ngOnInit(): void {
+ this.customerData$ = this.store.select(fromRoot.getCustomerSearchResults)
+ .map(customerPage => ({
+ data: customerPage.customers,
+ totalElements: customerPage.totalElements,
+ totalPages: customerPage.totalPages
+ }));
+
+ this.loading$ = this.store.select(fromRoot.getCustomerSearchLoading);
+
+ this.route.queryParams.subscribe((params: Params) => {
+ this.search(params['term']);
+ });
+ }
+
+ search(searchTerm: string): void {
+ this.searchTerm = searchTerm;
+ this.fetchCustomers();
+ }
+
+ rowSelect(customer: Customer): void {
+ this.router.navigate(['detail', customer.identifier], { relativeTo: this.route });
+ }
+
+ fetchCustomers(fetchRequest?: TableFetchRequest): void {
+ if (fetchRequest) {
+ this.lastFetchRequest = fetchRequest;
+ }
+
+ this.lastFetchRequest.searchTerm = this.searchTerm;
+
+ this.store.dispatch({ type: SEARCH, payload: this.lastFetchRequest });
+ }
+
+ goToTasks(): void {
+ this.router.navigate(['tasks'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/customers/customer.module.ts b/src/app/customers/customer.module.ts
new file mode 100644
index 0000000..0b58c5f
--- /dev/null
+++ b/src/app/customers/customer.module.ts
@@ -0,0 +1,190 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {RouterModule} from '@angular/router';
+import {CustomerRoutes} from './customer.routing';
+import {NgModule} from '@angular/core';
+import {CustomerComponent} from './customer.component';
+import {CustomerFormComponent} from './form/form.component';
+import {CreateCustomerFormComponent} from './form/create/create.form.component';
+import {FimsSharedModule} from '../common/common.module';
+import {CustomerDetailComponent} from './detail/customer.detail.component';
+import {CustomerDetailFormComponent} from './form/detail/detail.component';
+import {CustomerOfficesComponent} from './form/offices/offices.component';
+import {CustomerEmployeesComponent} from './form/employees/employees.component';
+import {CustomerContactFormComponent} from './form/contact/contact.component';
+import {EditCustomerFormComponent} from './form/edit/edit.form.component';
+import {CustomerCustomFieldsComponent} from './form/customFields/custom-fields.component';
+import {CustomerStatusComponent} from './detail/status/status.component';
+import {CustomerActivityComponent} from './detail/activity/activity.component';
+import {CustomerIndexComponent} from './detail/customer.index.component';
+import {CustomerExistsGuard} from './customer-exists.guard';
+import {CustomersStore, customerStoreFactory} from './store/index';
+import {Store} from '@ngrx/store';
+import {CustomerNotificationEffects} from './store/effects/notification.effects';
+import {CustomerRouteEffects} from './store/effects/route.effects';
+import {EffectsModule} from '@ngrx/effects';
+import {CustomerApiEffects} from './store/effects/service.effects';
+import {CustomerCommandApiEffects} from './store/commands/effects/service.effects';
+import {CustomerTasksNotificationEffects} from './store/customerTasks/effects/notification.effects';
+import {CustomerTasksApiEffects} from './store/customerTasks/effects/service.effects';
+import {CustomerTasksRouteEffects} from './store/customerTasks/effects/route.effects';
+import {CustomerPortraitComponent} from './detail/portrait/portrait.component';
+import {TranslateModule} from '@ngx-translate/core';
+import {CommonModule} from '@angular/common';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {
+ MatButtonModule,
+ MatCardModule,
+ MatCheckboxModule,
+ MatIconModule,
+ MatInputModule,
+ MatListModule,
+ MatOptionModule,
+ MatRadioModule,
+ MatSelectModule,
+ MatToolbarModule
+} from '@angular/material';
+import {CovalentChipsModule, CovalentFileModule, CovalentMessageModule, CovalentSearchModule, CovalentStepsModule} from '@covalent/core';
+import {TaskListComponent} from './tasks/task.list.component';
+import {TasksApiEffects} from './store/tasks/effects/service.effects';
+import {TasksRouteEffects} from './store/tasks/effects/route.effects';
+import {TasksNotificationEffects} from './store/tasks/effects/notification.effects';
+import {TaskCreateFormComponent} from './tasks/form/create.form.component';
+import {TaskEditFormComponent} from './tasks/form/edit.form.component';
+import {TaskFormComponent} from './tasks/form/form.component';
+import {TaskExistsGuard} from './tasks/task-exists.guard';
+import {TaskIndexComponent} from './tasks/task.index.component';
+import {TaskDetailComponent} from './tasks/task.detail.component';
+import {CustomerTaskComponent} from './detail/status/customer-task.component';
+import {CustomerPayrollFormComponent} from './detail/payroll/form/form.component';
+import {CustomerPayrollDetailComponent} from './detail/payroll/payroll.detail.component';
+import {CreateCustomerPayrollFormComponent} from './detail/payroll/form/create.form.component';
+import {PayrollExistsGuard} from './detail/payroll/payroll-exists.guard';
+import {CustomerPayrollApiEffects} from './store/payroll/effects/service.effects';
+import {CustomerPayrollRouteEffects} from './store/payroll/effects/route.effects';
+import {CustomerPayrollNotificationEffects} from './store/payroll/effects/notification.effects';
+import {CatalogExistsGuard} from './customFields/catalog-exists.guard';
+import {CreateCustomerCatalogFormComponent} from './customFields/form/create.form.component';
+import {CatalogDetailComponent} from './customFields/catalog.detail.component';
+import {CustomerCatalogFormComponent} from './customFields/form/form.component';
+import {FieldFormComponent} from './customFields/components/field.component';
+import {CatalogApiEffects} from './store/catalogs/effects/service.effects';
+import {CatalogRouteEffects} from './store/catalogs/effects/route.effects';
+import {CatalogNotificationEffects} from './store/catalogs/effects/notification.effects';
+import {FieldFormService} from './customFields/services/field-form.service';
+import {EditCatalogFieldFormComponent} from './customFields/fields/form/edit.form.component';
+import {FieldDetailComponent} from './customFields/fields/field.detail.component';
+import {FieldIndexComponent} from './customFields/fields/field.index.component';
+import {CatalogFieldFormComponent} from './customFields/fields/form/form.component';
+import {FieldExistsGuard} from './customFields/fields/field-exists.guard';
+import {CustomerCustomValuesComponent} from './customFields/components/value.component';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(CustomerRoutes),
+ FimsSharedModule,
+ TranslateModule,
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ MatCardModule,
+ MatIconModule,
+ MatListModule,
+ MatToolbarModule,
+ MatInputModule,
+ MatButtonModule,
+ MatRadioModule,
+ MatCheckboxModule,
+ MatOptionModule,
+ MatSelectModule,
+ CovalentSearchModule,
+ CovalentStepsModule,
+ CovalentFileModule,
+ CovalentMessageModule,
+ CovalentChipsModule,
+
+ EffectsModule.run(CustomerApiEffects),
+ EffectsModule.run(CustomerRouteEffects),
+ EffectsModule.run(CustomerNotificationEffects),
+
+ EffectsModule.run(TasksApiEffects),
+ EffectsModule.run(TasksRouteEffects),
+ EffectsModule.run(TasksNotificationEffects),
+
+ EffectsModule.run(CustomerTasksApiEffects),
+ EffectsModule.run(CustomerTasksRouteEffects),
+ EffectsModule.run(CustomerTasksNotificationEffects),
+ EffectsModule.run(CustomerCommandApiEffects),
+
+ EffectsModule.run(CustomerPayrollApiEffects),
+ EffectsModule.run(CustomerPayrollRouteEffects),
+ EffectsModule.run(CustomerPayrollNotificationEffects),
+
+ EffectsModule.run(CatalogApiEffects),
+ EffectsModule.run(CatalogRouteEffects),
+ EffectsModule.run(CatalogNotificationEffects),
+ ],
+ declarations: [
+ CustomerComponent,
+ CustomerDetailFormComponent,
+ CustomerContactFormComponent,
+ CustomerCustomFieldsComponent,
+ CustomerOfficesComponent,
+ CustomerEmployeesComponent,
+ CustomerFormComponent,
+ CreateCustomerFormComponent,
+ EditCustomerFormComponent,
+ CustomerIndexComponent,
+ CustomerDetailComponent,
+ CustomerStatusComponent,
+ CustomerActivityComponent,
+ CustomerPortraitComponent,
+ TaskListComponent,
+ TaskIndexComponent,
+ TaskCreateFormComponent,
+ TaskEditFormComponent,
+ TaskFormComponent,
+ TaskDetailComponent,
+ CustomerTaskComponent,
+
+ CustomerPayrollDetailComponent,
+ CreateCustomerPayrollFormComponent,
+ CustomerPayrollFormComponent,
+
+ CatalogDetailComponent,
+ CreateCustomerCatalogFormComponent,
+ CustomerCatalogFormComponent,
+ CatalogFieldFormComponent,
+ FieldFormComponent,
+ EditCatalogFieldFormComponent,
+ FieldIndexComponent,
+ FieldDetailComponent,
+ CustomerCustomValuesComponent
+ ],
+ providers: [
+ FieldFormService,
+ CustomerExistsGuard,
+ TaskExistsGuard,
+ PayrollExistsGuard,
+ CatalogExistsGuard,
+ FieldExistsGuard,
+ { provide: CustomersStore, useFactory: customerStoreFactory, deps: [Store]}
+ ]
+})
+export class CustomerModule {}
diff --git a/src/app/customers/customer.routing.ts b/src/app/customers/customer.routing.ts
new file mode 100644
index 0000000..a6b936d
--- /dev/null
+++ b/src/app/customers/customer.routing.ts
@@ -0,0 +1,192 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Routes} from '@angular/router';
+import {CustomerComponent} from './customer.component';
+import {CreateCustomerFormComponent} from './form/create/create.form.component';
+import {CustomerDetailComponent} from './detail/customer.detail.component';
+import {EditCustomerFormComponent} from './form/edit/edit.form.component';
+import {CustomerActivityComponent} from './detail/activity/activity.component';
+import {CustomerStatusComponent} from './detail/status/status.component';
+import {CustomerIndexComponent} from './detail/customer.index.component';
+import {CustomerExistsGuard} from './customer-exists.guard';
+import {CustomerPortraitComponent} from './detail/portrait/portrait.component';
+import {TaskListComponent} from './tasks/task.list.component';
+import {TaskExistsGuard} from './tasks/task-exists.guard';
+import {TaskEditFormComponent} from './tasks/form/edit.form.component';
+import {TaskCreateFormComponent} from './tasks/form/create.form.component';
+import {TaskIndexComponent} from './tasks/task.index.component';
+import {TaskDetailComponent} from './tasks/task.detail.component';
+import {PayrollExistsGuard} from './detail/payroll/payroll-exists.guard';
+import {CustomerPayrollDetailComponent} from './detail/payroll/payroll.detail.component';
+import {CreateCustomerPayrollFormComponent} from './detail/payroll/form/create.form.component';
+import {CatalogDetailComponent} from './customFields/catalog.detail.component';
+import {CatalogExistsGuard} from './customFields/catalog-exists.guard';
+import {CreateCustomerCatalogFormComponent} from './customFields/form/create.form.component';
+import {FieldIndexComponent} from './customFields/fields/field.index.component';
+import {FieldExistsGuard} from './customFields/fields/field-exists.guard';
+import {FieldDetailComponent} from './customFields/fields/field.detail.component';
+import {EditCatalogFieldFormComponent} from './customFields/fields/form/edit.form.component';
+
+export const CustomerRoutes: Routes = [
+ {
+ path: '',
+ component: CustomerComponent,
+ data: {title: 'Manage Customers', hasPermission: {id: 'customer_customers', accessLevel: 'READ'}},
+ canActivate: [ CatalogExistsGuard ]
+ },
+ {
+ path: 'create',
+ component: CreateCustomerFormComponent,
+ data: {title: 'Create Customer', hasPermission: { id: 'customer_customers', accessLevel: 'CHANGE' }}
+ },
+ {
+ path: 'detail/:id/edit',
+ component: EditCustomerFormComponent,
+ data: {title: 'Edit Customer', hasPermission: { id: 'customer_customers', accessLevel: 'CHANGE' }},
+ canActivate: [ CustomerExistsGuard ]
+ },
+ {
+ path: 'detail/:id',
+ component: CustomerIndexComponent,
+ data: {
+ hasPermission: { id: 'customer_customers', accessLevel: 'READ' }
+ },
+ canActivate: [ CustomerExistsGuard ],
+ children: [
+ {
+ path: '',
+ component: CustomerDetailComponent,
+ data: {title: 'View Customer'}
+ },
+ {
+ path: 'tasks',
+ component: CustomerStatusComponent,
+ data: {title: 'Manage Customer Tasks'},
+ },
+ {
+ path: 'activities',
+ component: CustomerActivityComponent,
+ data: {title: 'Manage Customer Tasks'}
+ },
+ {
+ path: 'portrait',
+ component: CustomerPortraitComponent,
+ data: {
+ title: 'Upload portrait',
+ hasPermission: { id: 'customer_portrait', accessLevel: 'READ' }
+ }
+ },
+ {path: 'identifications', loadChildren: './detail/identityCard/identity-card.module#IdentityCardModule'},
+ {path: 'loans', loadChildren: './cases/case.module#CaseModule'},
+ {path: 'deposits', loadChildren: './deposits/deposits.module#DepositsModule'},
+ {
+ path: 'payroll',
+ canActivate: [ PayrollExistsGuard ],
+ data: {
+ hasPermission: { id: 'payroll_configuration', accessLevel: 'READ' }
+ },
+ children: [
+ {
+ path: '',
+ component: CustomerPayrollDetailComponent
+ },
+ {
+ path: 'edit',
+ component: CreateCustomerPayrollFormComponent,
+ data: {
+ hasPermission: { id: 'payroll_configuration', accessLevel: 'CHANGE' }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ path: 'tasks',
+ component: TaskListComponent,
+ data: {
+ hasPermission: { id: 'customer_tasks', accessLevel: 'READ' }
+ }
+ },
+ {
+ path: 'tasks/detail/:id',
+ canActivate: [ TaskExistsGuard ],
+ component: TaskIndexComponent,
+ data: {
+ hasPermission: { id: 'customer_tasks', accessLevel: 'READ' }
+ },
+ children: [
+ {
+ path: '',
+ component: TaskDetailComponent
+ },
+ {
+ path: 'edit',
+ component: TaskEditFormComponent,
+ data: {
+ hasPermission: { id: 'customer_tasks', accessLevel: 'CHANGE' }
+ }
+ }
+ ]
+ },
+ {
+ path: 'tasks/create',
+ component: TaskCreateFormComponent,
+ data: {
+ hasPermission: { id: 'customer_tasks', accessLevel: 'CHANGE' }
+ }
+ },
+ {
+ path: 'catalog/detail',
+ data: {
+ hasPermission: { id: 'catalog_catalogs', accessLevel: 'READ' }
+ },
+ children: [
+ {
+ path: '',
+ component: CatalogDetailComponent
+ },
+ {
+ path: 'edit',
+ component: CreateCustomerCatalogFormComponent,
+ data: {
+ hasPermission: { id: 'catalog_catalogs', accessLevel: 'CHANGE' }
+ }
+ },
+ {
+ path: 'field/detail/:fieldId',
+ component: FieldIndexComponent,
+ canActivate: [ FieldExistsGuard ],
+ children: [
+ {
+ path: '',
+ component: FieldDetailComponent
+ },
+ {
+ path: 'edit',
+ component: EditCatalogFieldFormComponent,
+ data: {
+ hasPermission: { id: 'catalog_catalogs', accessLevel: 'CHANGE' }
+ }
+ }
+ ]
+ }
+ ]
+ }
+];
diff --git a/src/app/customers/deposits/deposit-instance-exists.guard.ts b/src/app/customers/deposits/deposit-instance-exists.guard.ts
new file mode 100644
index 0000000..20bc2d7
--- /dev/null
+++ b/src/app/customers/deposits/deposit-instance-exists.guard.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import {Injectable} from '@angular/core';
+import * as fromDeposits from './store';
+import {Observable} from 'rxjs/Observable';
+import {of} from 'rxjs/observable/of';
+import {DepositsStore} from './store/index';
+import {DepositAccountService} from '../../services/depositAccount/deposit-account.service';
+import {ExistsGuardService} from '../../common/guards/exists-guard';
+import {LoadAction} from './store/deposit.actions';
+
+@Injectable()
+export class DepositInstanceExistsGuard implements CanActivate {
+
+ constructor(private store: DepositsStore,
+ private depositService: DepositAccountService,
+ private existsGuardService: ExistsGuardService) {}
+
+ hasProductInstanceInStore(id: string): Observable<boolean> {
+ const timestamp$ = this.store.select(fromDeposits.getDepositsLoadedAt)
+ .map(loadedAt => loadedAt[id]);
+
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ hasProductInstanceInApi(id: string): Observable<boolean> {
+ const getProductInstance$ = this.depositService.findProductInstance(id)
+ .map(productInstance => new LoadAction({
+ resource: productInstance
+ }))
+ .do((action: LoadAction) => this.store.dispatch(action))
+ .map(productInstance => !!productInstance);
+
+ return this.existsGuardService.routeTo404OnError(getProductInstance$);
+ }
+
+ hasProductInstance(id: string): Observable<boolean> {
+ return this.hasProductInstanceInStore(id)
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+
+ return this.hasProductInstanceInApi(id);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasProductInstance(route.params['id']);
+ }
+}
diff --git a/src/app/customers/deposits/deposits.list.component.html b/src/app/customers/deposits/deposits.list.component.html
new file mode 100644
index 0000000..b5cd64f
--- /dev/null
+++ b/src/app/customers/deposits/deposits.list.component.html
@@ -0,0 +1,26 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Manage member deposit accounts' | translate}}" [navigateBackTo]="['../../../../']">
+ <fims-data-table flex
+ (onFetch)="fetchProductInstances($event)"
+ (onActionCellClick)="rowSelect($event)"
+ [columns]="columns"
+ [data]="productInstancesData$ | async">
+ </fims-data-table>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Create new deposit account for member ' | translate}}" icon="add" [link]="['create']" [permission]="{ id: 'deposit_instances', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/customers/deposits/deposits.list.component.ts b/src/app/customers/deposits/deposits.list.component.ts
new file mode 100644
index 0000000..0f96fb1
--- /dev/null
+++ b/src/app/customers/deposits/deposits.list.component.ts
@@ -0,0 +1,95 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {Customer} from '../../services/customer/domain/customer.model';
+import {Subscription} from 'rxjs/Subscription';
+import {TableData} from '../../common/data-table/data-table.component';
+import {ActivatedRoute, Router} from '@angular/router';
+import * as fromDeposits from './store/index';
+import {DepositsStore} from './store/index';
+import {FetchRequest} from '../../services/domain/paging/fetch-request.model';
+import {SEARCH} from './store/deposit.actions';
+import {ProductInstance} from '../../services/depositAccount/domain/instance/product-instance.model';
+import * as fromCustomers from '../store';
+import {DatePipe} from '@angular/common';
+
+
+@Component({
+ templateUrl: './deposits.list.component.html',
+ providers: [DatePipe]
+})
+export class DepositsListComponent implements OnInit, OnDestroy {
+
+ private customerSubscription: Subscription;
+
+ private customer: Customer;
+
+ productInstancesData$: Observable<TableData>;
+
+ columns: any[] = [
+ { name: 'productIdentifier', label: 'Deposit product' },
+ { name: 'accountIdentifier', label: 'Account identifier' },
+ { name: 'balance', label: 'Balance', numeric: true, format: v => v.toFixed(2) },
+ { name: 'state', label: 'State' },
+ {
+ name: 'openedOn', label: 'Opened on', format: (v: any) => {
+ return this.datePipe.transform(v, 'shortDate');
+ }
+ },
+ {
+ name: 'lastTransactionDate', label: 'Last transaction', format: (v: any) => {
+ return this.datePipe.transform(v, 'short');
+ }
+ }
+ ];
+
+ constructor(private router: Router, private route: ActivatedRoute, private depositsStore: DepositsStore, private datePipe: DatePipe) {}
+
+ ngOnInit(): void {
+ this.productInstancesData$ = this.depositsStore.select(fromDeposits.getDepositSearchResults)
+ .map(depositsPage => ({
+ totalElements: depositsPage.totalElements,
+ totalPages: depositsPage.totalPages,
+ data: depositsPage.deposits
+ }));
+
+ this.customerSubscription = this.depositsStore.select(fromCustomers.getSelectedCustomer)
+ .filter(customer => !!customer)
+ .subscribe(customer => {
+ this.customer = customer;
+ this.fetchProductInstances();
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.customerSubscription.unsubscribe();
+ }
+
+ fetchProductInstances(fetchRequest?: FetchRequest): void {
+ this.depositsStore.dispatch({ type: SEARCH, payload: {
+ customerId: this.customer.identifier,
+ fetchRequest: fetchRequest
+ }});
+ }
+
+ rowSelect(productInstance: ProductInstance): void {
+ this.router.navigate(['detail', productInstance.accountIdentifier], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/customers/deposits/deposits.module.ts b/src/app/customers/deposits/deposits.module.ts
new file mode 100644
index 0000000..d5d24fa
--- /dev/null
+++ b/src/app/customers/deposits/deposits.module.ts
@@ -0,0 +1,93 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {RouterModule} from '@angular/router';
+import {NgModule} from '@angular/core';
+import {FimsSharedModule} from '../../common/common.module';
+import {TranslateModule} from '@ngx-translate/core';
+import {CommonModule} from '@angular/common';
+import {ReactiveFormsModule} from '@angular/forms';
+import {
+ MatButtonModule,
+ MatCardModule,
+ MatIconModule,
+ MatInputModule,
+ MatListModule,
+ MatOptionModule,
+ MatRadioModule,
+ MatSelectModule,
+ MatToolbarModule
+} from '@angular/material';
+import {CovalentChipsModule, CovalentCommonModule, CovalentStepsModule} from '@covalent/core';
+import {DepositCreateComponent} from './form/create.component';
+import {DepositFormComponent} from './form/form.component';
+import {DepositsListComponent} from './deposits.list.component';
+import {Store} from '@ngrx/store';
+import {DepositsStore, depositsStoreFactory} from './store/index';
+import {DepositRoutes} from './deposits.routes';
+import {EffectsModule} from '@ngrx/effects';
+import {DepositProductInstanceApiEffects} from './store/effects/service.effects';
+import {DepositProductInstanceRouteEffects} from './store/effects/route.effects';
+import {DepositProductInstanceNotificationEffects} from './store/effects/notification.effects';
+import {DepositIndexComponent} from './detail/deposit.index.component';
+import {DepositDetailComponent} from './detail/deposit.detail.component';
+import {DepositInstanceExistsGuard} from './deposit-instance-exists.guard';
+import {DepositEditComponent} from './form/edit.component';
+import {IssueChequesFormComponent} from './detail/cheques/form.component';
+import {IssueChequeComponent} from './detail/cheques/cheques.component';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(DepositRoutes),
+ FimsSharedModule,
+ TranslateModule,
+ CommonModule,
+ ReactiveFormsModule,
+ MatIconModule,
+ MatListModule,
+ MatToolbarModule,
+ MatInputModule,
+ MatButtonModule,
+ MatOptionModule,
+ MatSelectModule,
+ MatRadioModule,
+ MatCardModule,
+ CovalentCommonModule,
+ CovalentStepsModule,
+ CovalentChipsModule,
+
+ EffectsModule.run(DepositProductInstanceApiEffects),
+ EffectsModule.run(DepositProductInstanceRouteEffects),
+ EffectsModule.run(DepositProductInstanceNotificationEffects),
+ ],
+ declarations: [
+ DepositsListComponent,
+ DepositFormComponent,
+ DepositIndexComponent,
+ DepositCreateComponent,
+ DepositEditComponent,
+ DepositDetailComponent,
+ IssueChequeComponent,
+ IssueChequesFormComponent,
+ ],
+ providers: [
+ DepositInstanceExistsGuard,
+ { provide: DepositsStore, useFactory: depositsStoreFactory, deps: [Store] }
+ ]
+})
+export class DepositsModule {}
diff --git a/src/app/customers/deposits/deposits.routes.ts b/src/app/customers/deposits/deposits.routes.ts
new file mode 100644
index 0000000..bc1bdd8
--- /dev/null
+++ b/src/app/customers/deposits/deposits.routes.ts
@@ -0,0 +1,71 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {DepositsListComponent} from './deposits.list.component';
+import {Routes} from '@angular/router';
+import {DepositCreateComponent} from './form/create.component';
+import {DepositIndexComponent} from './detail/deposit.index.component';
+import {DepositDetailComponent} from './detail/deposit.detail.component';
+import {DepositInstanceExistsGuard} from './deposit-instance-exists.guard';
+import {DepositEditComponent} from './form/edit.component';
+import {IssueChequeComponent} from './detail/cheques/cheques.component';
+
+export const DepositRoutes: Routes = [
+ {
+ path: '',
+ component: DepositsListComponent,
+ data: {
+ hasPermission: {id: 'deposit_instances', accessLevel: 'READ'}
+ }
+ },
+ {
+ path: 'detail/:id',
+ component: DepositIndexComponent,
+ canActivate: [DepositInstanceExistsGuard],
+ data: {
+ hasPermission: {id: 'deposit_instances', accessLevel: 'READ'}
+ },
+ children: [
+ {
+ path: '',
+ component: DepositDetailComponent
+ },
+ {
+ path: 'edit',
+ component: DepositEditComponent,
+ data: {
+ hasPermission: {id: 'deposit_instances', accessLevel: 'CHANGE'}
+ }
+ },
+ {
+ path: 'cheques',
+ component: IssueChequeComponent,
+ data: {
+ hasPermission: {id: 'cheque_management', accessLevel: 'CHANGE'}
+ }
+ }
+ ]
+ },
+ {
+ path: 'create',
+ component: DepositCreateComponent,
+ data: {
+ hasPermission: {id: 'deposit_instances', accessLevel: 'CHANGE'}
+ }
+ }
+];
diff --git a/src/app/customers/deposits/detail/cheques/cheques.component.html b/src/app/customers/deposits/detail/cheques/cheques.component.html
new file mode 100644
index 0000000..a03cb2e
--- /dev/null
+++ b/src/app/customers/deposits/detail/cheques/cheques.component.html
@@ -0,0 +1,24 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Issue cheques' | translate}}">
+ <fims-issue-cheque-form
+ [accountIdentifier]="(depositInstance$ | async).accountIdentifier"
+ (onSave)="issueCheques($event)"
+ (onCancel)="cancel()">
+ </fims-issue-cheque-form>
+</fims-layout-card-over>
diff --git a/src/app/customers/deposits/detail/cheques/cheques.component.ts b/src/app/customers/deposits/detail/cheques/cheques.component.ts
new file mode 100644
index 0000000..34fea7e
--- /dev/null
+++ b/src/app/customers/deposits/detail/cheques/cheques.component.ts
@@ -0,0 +1,55 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import * as fromDeposits from '../../store/index';
+import {DepositsStore} from '../../store/index';
+import {Observable} from 'rxjs/Observable';
+import {ProductInstance} from '../../../../services/depositAccount/domain/instance/product-instance.model';
+import {IssuingCount} from '../../../../services/cheque/domain/issuing-count.model';
+import {ISSUE_CHEQUES} from '../../store/deposit.actions';
+import {ActivatedRoute, Router} from '@angular/router';
+
+@Component({
+ templateUrl: './cheques.component.html'
+})
+export class IssueChequeComponent implements OnInit {
+
+ depositInstance$: Observable<ProductInstance>;
+
+ constructor(private store: DepositsStore, private router: Router, private route: ActivatedRoute) {}
+
+ ngOnInit(): void {
+ this.depositInstance$ = this.store.select(fromDeposits.getSelectedDepositInstance);
+ }
+
+ issueCheques(issuingCount: IssuingCount): void {
+ this.store.dispatch({
+ type: ISSUE_CHEQUES,
+ payload: {
+ issuingCount,
+ activatedRoute: this.route
+ }
+ });
+ }
+
+ cancel(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+
+}
diff --git a/src/app/customers/deposits/detail/cheques/form.component.html b/src/app/customers/deposits/detail/cheques/form.component.html
new file mode 100644
index 0000000..ca62023
--- /dev/null
+++ b/src/app/customers/deposits/detail/cheques/form.component.html
@@ -0,0 +1,30 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Issue cheques' | translate}}" [state]="form.valid ? 'complete' : form.pristine ? 'none' : 'required'">
+ <form [formGroup]="form" layout="column">
+ <fims-text-input type="number" [form]="form" controlName="start" placeholder="{{'Start at(optional)' | translate}}"></fims-text-input>
+ <fims-text-input type="number" [form]="form" controlName="amount" placeholder="{{'Cheque amount' | translate}}"></fims-text-input>
+ </form>
+ <ng-template td-step-actions>
+ <button mat-raised-button color="primary" (click)="save()" [disabled]="form.invalid">{{'ISSUE CHEQUES' | translate}}</button>
+ <span flex></span>
+ <button mat-button (click)="cancel()">{{'CANCEL' | translate}}</button>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/customers/deposits/detail/cheques/form.component.ts b/src/app/customers/deposits/detail/cheques/form.component.ts
new file mode 100644
index 0000000..86edbc0
--- /dev/null
+++ b/src/app/customers/deposits/detail/cheques/form.component.ts
@@ -0,0 +1,66 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
+import {TdStepComponent} from '@covalent/core';
+import {IssuingCount} from '../../../../services/cheque/domain/issuing-count.model';
+import {FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {FimsValidators} from '../../../../common/validator/validators';
+
+@Component({
+ selector: 'fims-issue-cheque-form',
+ templateUrl: './form.component.html'
+})
+export class IssueChequesFormComponent implements OnInit {
+
+ form: FormGroup;
+
+ @ViewChild('detailsStep') detailsStep: TdStepComponent;
+
+ @Input('accountIdentifier') accountIdentifier: string;
+
+ @Output('onSave') onSave = new EventEmitter<IssuingCount>();
+
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder) {}
+
+ ngOnInit(): void {
+ this.form = this.formBuilder.group({
+ start: [1, [FimsValidators.minValue(1)]],
+ amount: [1, [Validators.required, FimsValidators.minValue(1)]]
+ });
+
+ this.detailsStep.open();
+ }
+
+ save(): void {
+ const issuingCount: IssuingCount = {
+ start: this.form.get('start').value,
+ amount: this.form.get('amount').value,
+ accountIdentifier: this.accountIdentifier
+ };
+
+ this.onSave.emit(issuingCount);
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+}
diff --git a/src/app/customers/deposits/detail/deposit.detail.component.html b/src/app/customers/deposits/detail/deposit.detail.component.html
new file mode 100644
index 0000000..5e01fe9
--- /dev/null
+++ b/src/app/customers/deposits/detail/deposit.detail.component.html
@@ -0,0 +1,60 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Deposit account' | translate}}" [navigateBackTo]="['../../../../../../']">
+ <fims-layout-card-over-header-menu>
+ <button mat-icon-button (click)="issueCheques()" title="{{'Issue cheques' | translate}}"
+ *hasPermission="{ id: 'cheque_management', accessLevel: 'CHANGE'}">
+ <mat-icon>import_contacts</mat-icon>
+ </button>
+ </fims-layout-card-over-header-menu>
+ <div class="mat-content inset" flex>
+ <div layout="row">
+ <mat-list *ngIf="(depositInstance$ | async) as instance">
+ <h3 mat-subheader translate>Current status</h3>
+ <fims-state-display [state]="instance.state"></fims-state-display>
+ <h3 mat-subheader translate>Details</h3>
+ <mat-list-item>
+ <h3 matLine translate>Account</h3>
+ <p matLine>
+ <a [routerLink]="['/accounting/accounts/detail', instance.accountIdentifier, 'entries']">
+ {{instance.alternativeAccountNumber ? instance.alternativeAccountNumber : instance.accountNumber}}
+ </a>
+ </p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Balance</h3>
+ <p matLine>{{instance.balance | number:'1.2-2'}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Beneficiaries</h3>
+ <p matLine>{{instance.beneficiaries?.join(', ')}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Opened on</h3>
+ <p matLine>{{instance.openedOn | date:'shortDate'}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Last transaction</h3>
+ <p matLine>{{instance.lastTransactionDate | date:'medium'}}</p>
+ </mat-list-item>
+ </mat-list>
+ </div>
+ </div>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Edit deposit account' | translate}}" icon="mode_edit" [link]="['edit']"
+ [permission]="{ id: 'deposit_instances', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/customers/deposits/detail/deposit.detail.component.ts b/src/app/customers/deposits/detail/deposit.detail.component.ts
new file mode 100644
index 0000000..fdb3928
--- /dev/null
+++ b/src/app/customers/deposits/detail/deposit.detail.component.ts
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import {ProductInstance} from '../../../services/depositAccount/domain/instance/product-instance.model';
+import {Observable} from 'rxjs/Observable';
+import * as fromDeposits from '../store/index';
+import {DepositsStore} from '../store/index';
+import {ActivatedRoute, Router} from '@angular/router';
+
+@Component({
+ templateUrl: './deposit.detail.component.html'
+})
+export class DepositDetailComponent implements OnInit {
+
+ depositInstance$: Observable<ProductInstance>;
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: DepositsStore) {}
+
+ ngOnInit(): void {
+ this.depositInstance$ = this.store.select(fromDeposits.getSelectedDepositInstance);
+ }
+
+ issueCheques(): void {
+ this.router.navigate(['cheques'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/customers/deposits/detail/deposit.index.component.html b/src/app/customers/deposits/detail/deposit.index.component.html
new file mode 100644
index 0000000..ca721b3
--- /dev/null
+++ b/src/app/customers/deposits/detail/deposit.index.component.html
@@ -0,0 +1,18 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<router-outlet></router-outlet>
diff --git a/src/app/customers/deposits/detail/deposit.index.component.ts b/src/app/customers/deposits/detail/deposit.index.component.ts
new file mode 100644
index 0000000..abfab7d
--- /dev/null
+++ b/src/app/customers/deposits/detail/deposit.index.component.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {Subscription} from 'rxjs/Subscription';
+import {ActivatedRoute} from '@angular/router';
+import {DepositsStore} from '../store/index';
+import {SelectAction} from '../store/deposit.actions';
+
+@Component({
+ templateUrl: './deposit.index.component.html'
+})
+export class DepositIndexComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ constructor(private route: ActivatedRoute, private store: DepositsStore) {}
+
+ ngOnInit(): void {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectAction(params['id']))
+ .subscribe(this.store);
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ }
+}
diff --git a/src/app/customers/deposits/form/create.component.html b/src/app/customers/deposits/form/create.component.html
new file mode 100644
index 0000000..f4cfc51
--- /dev/null
+++ b/src/app/customers/deposits/form/create.component.html
@@ -0,0 +1,27 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Create new deposit account for member ' | translate}}">
+ <fims-deposit-form-component #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [customerId]="(customer$ | async).identifier"
+ [productInstance]="productInstance"
+ [productDefinitions]="productDefinitions$ | async"
+ [editMode]="false">
+ </fims-deposit-form-component>
+</fims-layout-card-over>
diff --git a/src/app/customers/deposits/form/create.component.ts b/src/app/customers/deposits/form/create.component.ts
new file mode 100644
index 0000000..16d579d
--- /dev/null
+++ b/src/app/customers/deposits/form/create.component.ts
@@ -0,0 +1,72 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {DepositFormComponent} from './form.component';
+import {Customer} from '../../../services/customer/domain/customer.model';
+import {ProductInstance} from '../../../services/depositAccount/domain/instance/product-instance.model';
+import {ActivatedRoute, Router} from '@angular/router';
+import {DepositsStore} from '../store/index';
+import {CREATE} from '../store/deposit.actions';
+import * as fromCustomers from '../../store/index';
+import {DepositAccountService} from '../../../services/depositAccount/deposit-account.service';
+import {Observable} from 'rxjs/Observable';
+import {ProductDefinition} from '../../../services/depositAccount/domain/definition/product-definition.model';
+
+@Component({
+ templateUrl: './create.component.html'
+})
+export class DepositCreateComponent implements OnInit {
+
+ @ViewChild('form') formComponent: DepositFormComponent;
+
+ customer$: Observable<Customer>;
+
+ productInstance: ProductInstance = {
+ customerIdentifier: '',
+ productIdentifier: ''
+ };
+
+ productDefinitions$: Observable<ProductDefinition[]>;
+
+ constructor(private router: Router, private route: ActivatedRoute, private depositsStore: DepositsStore,
+ private depositService: DepositAccountService) {}
+
+ ngOnInit(): void {
+ this.customer$ = this.depositsStore.select(fromCustomers.getSelectedCustomer)
+ .filter(customer => !!customer);
+
+ this.productDefinitions$ = this.depositService.fetchProductDefinitions()
+ .map(productDefinitions => productDefinitions.filter(definition => definition.active));
+ }
+
+ onSave(productInstance: ProductInstance): void {
+ this.depositsStore.dispatch({ type: CREATE, payload: {
+ productInstance: productInstance,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel(): void {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/customers/deposits/form/edit.component.html b/src/app/customers/deposits/form/edit.component.html
new file mode 100644
index 0000000..2f22669
--- /dev/null
+++ b/src/app/customers/deposits/form/edit.component.html
@@ -0,0 +1,27 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Edit deposit account for member ' | translate}}">
+ <fims-deposit-form-component #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [customerId]="(customer$ | async).identifier"
+ [productInstance]="productInstance$ | async"
+ [productDefinitions]="[]"
+ [editMode]="true">
+ </fims-deposit-form-component>
+</fims-layout-card-over>
diff --git a/src/app/customers/deposits/form/edit.component.ts b/src/app/customers/deposits/form/edit.component.ts
new file mode 100644
index 0000000..3ea86b5
--- /dev/null
+++ b/src/app/customers/deposits/form/edit.component.ts
@@ -0,0 +1,62 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {DepositFormComponent} from './form.component';
+import {Customer} from '../../../services/customer/domain/customer.model';
+import {ProductInstance} from '../../../services/depositAccount/domain/instance/product-instance.model';
+import {ActivatedRoute, Router} from '@angular/router';
+import * as fromDeposits from '../store/index';
+import {DepositsStore} from '../store/index';
+import * as fromCustomers from '../../store/index';
+import {Observable} from 'rxjs/Observable';
+import {UPDATE} from '../store/deposit.actions';
+
+@Component({
+ templateUrl: './edit.component.html'
+})
+export class DepositEditComponent implements OnInit {
+
+ @ViewChild('form') formComponent: DepositFormComponent;
+
+ customer$: Observable<Customer>;
+
+ productInstance$: Observable<ProductInstance>;
+
+ constructor(private router: Router, private route: ActivatedRoute, private depositsStore: DepositsStore) {}
+
+ ngOnInit(): void {
+ this.customer$ = this.depositsStore.select(fromCustomers.getSelectedCustomer);
+ this.productInstance$ = this.depositsStore.select(fromDeposits.getSelectedDepositInstance);
+ }
+
+ onSave(productInstance: ProductInstance): void {
+ this.depositsStore.dispatch({ type: UPDATE, payload: {
+ productInstance,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel(): void {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/customers/deposits/form/form.component.html b/src/app/customers/deposits/form/form.component.html
new file mode 100644
index 0000000..1879d9b
--- /dev/null
+++ b/src/app/customers/deposits/form/form.component.html
@@ -0,0 +1,51 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Assign product' | translate}}" [state]="detailForm.valid ? 'complete' : detailForm.pristine ? 'none' : 'required'">
+ <form [formGroup]="detailForm" layout="column">
+ <mat-form-field layout-margin *ngIf="!editMode">
+ <mat-select formControlName="productIdentifier" placeholder="{{ 'Select product' | translate }}">
+ <mat-option *ngFor="let definition of productDefinitions" [value]="definition.identifier">
+ {{definition.name}}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ <div layout="row">
+ <td-chips [items]="filteredCustomers | async"
+ [debounce]="500"
+ formControlName="beneficiaries"
+ placeholder="{{'Search beneficiary' | translate }}"
+ (inputChange)="filterAsync($event)"
+ requireMatch>
+ <ng-template td-chip let-chip="chip">
+ <div class="tc-grey-100 bgc-teal-700" td-chip-avatar>{{chip.substring(0, 1).toUpperCase()}}</div> {{chip}}
+ </ng-template>
+ </td-chips>
+ </div>
+ </form>
+ <ng-template td-step-actions>
+ <fims-form-final-action
+ [resourceName]="'DEPOSIT ACCOUNT'"
+ [editMode]="editMode"
+ [disabled]="!isValid"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/customers/deposits/form/form.component.ts b/src/app/customers/deposits/form/form.component.ts
new file mode 100644
index 0000000..68c2399
--- /dev/null
+++ b/src/app/customers/deposits/form/form.component.ts
@@ -0,0 +1,86 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
+import {FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {TdStepComponent} from '@covalent/core';
+import {ProductInstance} from '../../../services/depositAccount/domain/instance/product-instance.model';
+import {ProductDefinition} from '../../../services/depositAccount/domain/definition/product-definition.model';
+import {Observable} from 'rxjs/Observable';
+import {CustomerService} from '../../../services/customer/customer.service';
+
+@Component({
+ selector: 'fims-deposit-form-component',
+ templateUrl: './form.component.html'
+})
+export class DepositFormComponent implements OnInit {
+
+ filteredCustomers: Observable<string[]>;
+
+ detailForm: FormGroup;
+
+ @ViewChild('detailsStep') detailsStep: TdStepComponent;
+
+ @Input('editMode') editMode: boolean;
+
+ @Input('customerId') customerId: string;
+
+ @Input('productDefinitions') productDefinitions: ProductDefinition[];
+
+ @Input('productInstance') productInstance: ProductInstance;
+
+ @Output('onSave') onSave = new EventEmitter<ProductInstance>();
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder, private customerService: CustomerService) {}
+
+ ngOnInit(): void {
+ this.detailForm = this.formBuilder.group({
+ productIdentifier: [this.productInstance.productIdentifier, [Validators.required]],
+ beneficiaries: [this.productInstance.beneficiaries ? this.productInstance.beneficiaries : []]
+ });
+
+ this.detailsStep.open();
+ }
+
+ get isValid(): boolean {
+ return this.detailForm.valid;
+ }
+
+ save(): void {
+ const productInstance: ProductInstance = {
+ productIdentifier: this.detailForm.get('productIdentifier').value,
+ beneficiaries: this.detailForm.get('beneficiaries').value,
+ customerIdentifier: this.customerId,
+ accountIdentifier: this.productInstance.accountIdentifier,
+ state: this.productInstance.state
+ };
+
+ this.onSave.emit(productInstance);
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+ filterAsync(searchTerm: string): void {
+ this.filteredCustomers = this.customerService.fetchCustomers({
+ searchTerm
+ }).map(customerPage => customerPage.customers.map(customer => customer.identifier));
+ }
+}
diff --git a/src/app/customers/deposits/store/deposit.actions.ts b/src/app/customers/deposits/store/deposit.actions.ts
new file mode 100644
index 0000000..69fde58
--- /dev/null
+++ b/src/app/customers/deposits/store/deposit.actions.ts
@@ -0,0 +1,156 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {type} from '../../../store/util';
+import {FetchRequest} from '../../../services/domain/paging/fetch-request.model';
+import {RoutePayload} from '../../../common/store/route-payload';
+import {ProductInstance} from '../../../services/depositAccount/domain/instance/product-instance.model';
+import {
+ CreateResourceSuccessPayload,
+ LoadResourcePayload,
+ SelectResourcePayload,
+ UpdateResourceSuccessPayload
+} from '../../../common/store/resource.reducer';
+import {Action} from '@ngrx/store';
+import {SearchResult} from '../../../common/store/search.reducer';
+import {IssuingCount} from '../../../services/cheque/domain/issuing-count.model';
+
+export const SEARCH = type('[Deposit] Search');
+export const SEARCH_COMPLETE = type('[Deposit] Search Complete');
+
+export const LOAD = type('[Deposit] Load');
+export const SELECT = type('[Deposit] Select');
+
+export const CREATE = type('[Deposit] Create');
+export const CREATE_SUCCESS = type('[Deposit] Create Success');
+export const CREATE_FAIL = type('[Deposit] Create Fail');
+
+export const UPDATE = type('[Deposit] Update');
+export const UPDATE_SUCCESS = type('[Deposit] Update Success');
+export const UPDATE_FAIL = type('[Deposit] Update Fail');
+
+export const ISSUE_CHEQUES = type('[Deposit] Issue Cheques');
+export const ISSUE_CHEQUES_SUCCESS = type('[Deposit] Issue Cheques Success');
+export const ISSUE_CHEQUES_FAIL = type('[Deposit] Issue Cheques Fail');
+
+
+export interface SearchProductInstancePayload {
+ customerId: string;
+ fetchRequest: FetchRequest;
+}
+
+export interface DepositRoutePayload extends RoutePayload {
+ productInstance: ProductInstance;
+}
+
+export interface IssueChequePayload extends RoutePayload {
+ issuingCount: IssuingCount;
+}
+
+export class SearchAction implements Action {
+ readonly type = SEARCH;
+
+ constructor(public payload: SearchProductInstancePayload) { }
+}
+
+export class SearchCompleteAction implements Action {
+ readonly type = SEARCH_COMPLETE;
+
+ constructor(public payload: SearchResult) { }
+}
+
+export class LoadAction implements Action {
+ readonly type = LOAD;
+
+ constructor(public payload: LoadResourcePayload) { }
+}
+
+export class SelectAction implements Action {
+ readonly type = SELECT;
+
+ constructor(public payload: SelectResourcePayload) { }
+}
+
+export class CreateProductInstanceAction implements Action {
+ readonly type = CREATE;
+
+ constructor(public payload: DepositRoutePayload) { }
+}
+
+export class CreateProductInstanceSuccessAction implements Action {
+ readonly type = CREATE_SUCCESS;
+
+ constructor(public payload: CreateResourceSuccessPayload) { }
+}
+
+export class CreateProductInstanceFailAction implements Action {
+ readonly type = CREATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class UpdateProductInstanceAction implements Action {
+ readonly type = UPDATE;
+
+ constructor(public payload: DepositRoutePayload) { }
+}
+
+export class UpdateProductInstanceSuccessAction implements Action {
+ readonly type = UPDATE_SUCCESS;
+
+ constructor(public payload: UpdateResourceSuccessPayload) { }
+}
+
+export class UpdateProductInstanceFailAction implements Action {
+ readonly type = UPDATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class IssueChequesAction implements Action {
+ readonly type = ISSUE_CHEQUES;
+
+ constructor(public payload: IssueChequePayload) { }
+}
+
+export class IssueChequesSuccessAction implements Action {
+ readonly type = ISSUE_CHEQUES_SUCCESS;
+
+ constructor(public payload: IssueChequePayload) { }
+}
+
+export class IssueChequesFailAction implements Action {
+ readonly type = ISSUE_CHEQUES_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export type Actions
+ = SearchAction
+ | SearchCompleteAction
+ | LoadAction
+ | SelectAction
+ | CreateProductInstanceAction
+ | CreateProductInstanceSuccessAction
+ | CreateProductInstanceFailAction
+ | UpdateProductInstanceAction
+ | UpdateProductInstanceSuccessAction
+ | UpdateProductInstanceFailAction
+ | IssueChequesAction
+ | IssueChequesSuccessAction
+ | IssueChequesFailAction;
diff --git a/src/app/customers/deposits/store/effects/notification.effects.ts b/src/app/customers/deposits/store/effects/notification.effects.ts
new file mode 100644
index 0000000..4f58a38
--- /dev/null
+++ b/src/app/customers/deposits/store/effects/notification.effects.ts
@@ -0,0 +1,54 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {NotificationService, NotificationType} from '../../../../services/notification/notification.service';
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as instanceActions from '../deposit.actions';
+
+@Injectable()
+export class DepositProductInstanceNotificationEffects {
+
+ @Effect({dispatch: false})
+ createProductInstanceSuccess$: Observable<Action> = this.actions$
+ .ofType(instanceActions.CREATE_SUCCESS, instanceActions.UPDATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Deposit account is going to be saved'
+ }));
+
+ @Effect({dispatch: false})
+ issueChequesSuccess$: Observable<Action> = this.actions$
+ .ofType(instanceActions.ISSUE_CHEQUES_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Cheques are going to be issued'
+ }));
+
+ @Effect({dispatch: false})
+ issueChequesFail$: Observable<Action> = this.actions$
+ .ofType(instanceActions.ISSUE_CHEQUES_FAIL)
+ .do(() => this.notificationService.send({
+ type: NotificationType.ALERT,
+ message: 'There was an issue issuing cheques'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+}
diff --git a/src/app/customers/deposits/store/effects/route.effects.ts b/src/app/customers/deposits/store/effects/route.effects.ts
new file mode 100644
index 0000000..b447699
--- /dev/null
+++ b/src/app/customers/deposits/store/effects/route.effects.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Router} from '@angular/router';
+import {Observable} from 'rxjs/Observable';
+import * as instanceActions from '../deposit.actions';
+import {Action} from '@ngrx/store';
+
+@Injectable()
+export class DepositProductInstanceRouteEffects {
+
+ @Effect({ dispatch: false })
+ createProductInstanceSuccess$: Observable<Action> = this.actions$
+ .ofType(instanceActions.CREATE_SUCCESS, instanceActions.UPDATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute }));
+
+ @Effect({ dispatch: false })
+ issueChequesSuccess$: Observable<Action> = this.actions$
+ .ofType(instanceActions.ISSUE_CHEQUES_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute }));
+
+ constructor(private actions$: Actions, private router: Router) { }
+
+}
diff --git a/src/app/customers/deposits/store/effects/service.effects.ts b/src/app/customers/deposits/store/effects/service.effects.ts
new file mode 100644
index 0000000..f331d99
--- /dev/null
+++ b/src/app/customers/deposits/store/effects/service.effects.ts
@@ -0,0 +1,87 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {of} from 'rxjs/observable/of';
+import {Action} from '@ngrx/store';
+import {Observable} from 'rxjs/Observable';
+import {Actions, Effect} from '@ngrx/effects';
+import {emptySearchResult} from '../../../../common/store/search.reducer';
+import {DepositAccountService} from '../../../../services/depositAccount/deposit-account.service';
+import {Injectable} from '@angular/core';
+import * as instanceActions from '../deposit.actions';
+import {ChequeService} from '../../../../services/cheque/cheque.service';
+
+@Injectable()
+export class DepositProductInstanceApiEffects {
+
+ @Effect()
+ search$: Observable<Action> = this.actions$
+ .ofType(instanceActions.SEARCH)
+ .debounceTime(300)
+ .map(action => action.payload)
+ .switchMap(payload => {
+ const nextSearch$ = this.actions$.ofType(instanceActions.SEARCH).skip(1);
+
+ return this.depositService.fetchProductInstances(payload.customerId)
+ .takeUntil(nextSearch$)
+ .map(productInstances => new instanceActions.SearchCompleteAction({
+ elements: productInstances,
+ totalElements: productInstances.length,
+ totalPages: 1
+ }))
+ .catch(() => of(new instanceActions.SearchCompleteAction(emptySearchResult())));
+ });
+
+ @Effect()
+ createProduct$: Observable<Action> = this.actions$
+ .ofType(instanceActions.CREATE)
+ .map((action: instanceActions.CreateProductInstanceAction) => action.payload)
+ .mergeMap(payload =>
+ this.depositService.createProductInstance(payload.productInstance)
+ .map(() => new instanceActions.CreateProductInstanceSuccessAction({
+ resource: payload.productInstance,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new instanceActions.CreateProductInstanceFailAction(error)))
+ );
+
+ @Effect()
+ updateProduct$: Observable<Action> = this.actions$
+ .ofType(instanceActions.UPDATE)
+ .map((action: instanceActions.UpdateProductInstanceAction) => action.payload)
+ .mergeMap(payload =>
+ this.depositService.updateProductInstance(payload.productInstance)
+ .map(() => new instanceActions.UpdateProductInstanceSuccessAction({
+ resource: payload.productInstance,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new instanceActions.UpdateProductInstanceFailAction(error)))
+ );
+
+ @Effect()
+ issueCheques$: Observable<Action> = this.actions$
+ .ofType(instanceActions.ISSUE_CHEQUES)
+ .map((action: instanceActions.IssueChequesAction) => action.payload)
+ .mergeMap(payload =>
+ this.chequeService.issue(payload.issuingCount)
+ .map(() => new instanceActions.IssueChequesSuccessAction(payload))
+ .catch((error) => of(new instanceActions.IssueChequesFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private depositService: DepositAccountService, private chequeService: ChequeService) { }
+}
diff --git a/src/app/customers/deposits/store/index.ts b/src/app/customers/deposits/store/index.ts
new file mode 100644
index 0000000..3de47e7
--- /dev/null
+++ b/src/app/customers/deposits/store/index.ts
@@ -0,0 +1,69 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {createResourceReducer, getResourceLoadedAt, getResourceSelected, ResourceState} from '../../../common/store/resource.reducer';
+import * as fromCustomer from '../../store';
+import {ActionReducer, Store} from '@ngrx/store';
+import {createReducer} from '../../../store/index';
+import {
+ createSearchReducer,
+ getSearchEntities,
+ getSearchTotalElements,
+ getSearchTotalPages,
+ SearchState
+} from '../../../common/store/search.reducer';
+import {createSelector} from 'reselect';
+
+export interface State extends fromCustomer.State {
+ deposits: ResourceState;
+ depositSearch: SearchState;
+}
+
+const reducers = {
+ deposits: createResourceReducer('Deposit', undefined, 'accountIdentifier'),
+ depositSearch: createSearchReducer('Deposit')
+};
+
+export const depositModuleReducer: ActionReducer<State> = createReducer(reducers);
+
+export class DepositsStore extends Store<State> {}
+
+export function depositsStoreFactory(appStore: Store<fromCustomer.State>) {
+ appStore.replaceReducer(depositModuleReducer);
+ return appStore;
+}
+
+export const getDepositSearchState = (state: State) => state.depositSearch;
+
+export const getSearchDeposits = createSelector(getDepositSearchState, getSearchEntities);
+export const getDepositSearchTotalElements = createSelector(getDepositSearchState, getSearchTotalElements);
+export const getDepositSearchTotalPages = createSelector(getDepositSearchState, getSearchTotalPages);
+
+export const getDepositSearchResults = createSelector(getSearchDeposits, getDepositSearchTotalPages, getDepositSearchTotalElements,
+ (deposits, totalPages, totalElements) => {
+ return {
+ deposits,
+ totalPages,
+ totalElements
+ };
+ });
+
+export const getDepositsState = (state: State) => state.deposits;
+
+export const getDepositsLoadedAt = createSelector(getDepositsState, getResourceLoadedAt);
+export const getSelectedDepositInstance = createSelector(getDepositsState, getResourceSelected);
diff --git a/src/app/customers/detail/activity/activity.component.html b/src/app/customers/detail/activity/activity.component.html
new file mode 100644
index 0000000..ae2f48d
--- /dev/null
+++ b/src/app/customers/detail/activity/activity.component.html
@@ -0,0 +1,26 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Activities' | translate}}" [navigateBackTo]="['../']">
+ <mat-list>
+ <h3 mat-subheader translate>Latest activity</h3>
+ <ng-template let-item let-last="last" ngFor [ngForOf]="commands">
+ <fims-command-display [command]="item"></fims-command-display>
+ <mat-divider *ngIf="!last" mat-inset></mat-divider>
+ </ng-template>
+ </mat-list>
+</fims-layout-card-over>
diff --git a/src/app/customers/detail/activity/activity.component.ts b/src/app/customers/detail/activity/activity.component.ts
new file mode 100644
index 0000000..04ad93d
--- /dev/null
+++ b/src/app/customers/detail/activity/activity.component.ts
@@ -0,0 +1,51 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {Command} from '../../../services/customer/domain/command.model';
+import {CustomersStore} from '../../store/index';
+import {LOAD_ALL} from '../../store/commands/commands.actions';
+import * as fromCustomers from '../../store';
+import {Subscription} from 'rxjs/Subscription';
+
+@Component({
+ templateUrl: './activity.component.html'
+})
+export class CustomerActivityComponent implements OnInit, OnDestroy {
+
+ private commandsSubscription: Subscription;
+
+ private customerSubscription: Subscription;
+
+ commands: Command[];
+
+ constructor(private store: CustomersStore) {}
+
+ ngOnInit(): void {
+ this.customerSubscription = this.store.select(fromCustomers.getSelectedCustomer)
+ .subscribe(customer => this.store.dispatch({ type: LOAD_ALL, payload: customer.identifier }));
+
+ this.commandsSubscription = this.store.select(fromCustomers.getAllCustomerCommands)
+ .subscribe(commands => this.commands = commands);
+ }
+
+ ngOnDestroy(): void {
+ this.commandsSubscription.unsubscribe();
+ this.customerSubscription.unsubscribe();
+ }
+}
diff --git a/src/app/customers/detail/customer.detail.component.html b/src/app/customers/detail/customer.detail.component.html
new file mode 100644
index 0000000..5c60582
--- /dev/null
+++ b/src/app/customers/detail/customer.detail.component.html
@@ -0,0 +1,111 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{customer.givenName}} {{customer.surname}}" [navigateBackTo]="['/customers']">
+ <fims-layout-card-over-header-menu>
+ <td-search-box #searchBox placeholder="{{'Search' | translate}}" (search)="searchCustomer($event)" [alwaysVisible]="false"></td-search-box>
+ </fims-layout-card-over-header-menu>
+ <td-message *ngIf="!isCustomerActive" label="{{'Member not active' | translate }}"
+ sublabel="{{'You can activate the member under tasks' | translate }}"
+ color="warn" icon="error">
+ <button td-message-actions mat-button (click)="goToTasks()"
+ *hasPermission="{ id: 'customer_customers', accessLevel: 'CHANGE'}" translate>GO TO TASKS
+ </button>
+ </td-message>
+ <fims-two-column-layout>
+ <ng-container left>
+ <fims-portrait (onClick)="changePortrait()" [tooltip]="'Change portrait'" [blob]="portrait"></fims-portrait>
+ <mat-nav-list>
+ <h3 mat-subheader translate>Financial products</h3>
+ <a mat-list-item [routerLink]="['loans']" *hasPermission="{ id: 'portfolio_cases', accessLevel: 'READ'}">
+ <mat-icon matListAvatar>credit_card</mat-icon>
+ <h3 matLine translate>Loan accounts</h3>
+ <p matLine translate>Manage loan accounts</p>
+ </a>
+ <a mat-list-item [routerLink]="['deposits']" *hasPermission="{ id: 'deposit_instances', accessLevel: 'READ'}">
+ <mat-icon matListAvatar>attach_money</mat-icon>
+ <h3 matLine translate>Deposit accounts</h3>
+ <p matLine translate>Manage deposit accounts</p>
+ </a>
+ <mat-divider></mat-divider>
+ <h3 mat-subheader translate>Management</h3>
+ <a mat-list-item [routerLink]="['identifications']" *hasPermission="{ id: 'customer_identifications', accessLevel: 'READ'}">
+ <mat-icon matListAvatar>perm_identity</mat-icon>
+ <h3 matLine translate>Identification cards</h3>
+ <p matLine translate>View identification cards</p>
+ </a>
+ <a mat-list-item [routerLink]="['tasks']">
+ <mat-icon matListAvatar>playlist_add_check</mat-icon>
+ <h3 matLine translate>Tasks</h3>
+ <p matLine translate>Change the status of the member </p>
+ </a>
+ <a mat-list-item [routerLink]="['activities']">
+ <mat-icon matListAvatar>event</mat-icon>
+ <h3 matLine translate>Activities</h3>
+ <p matLine translate>Recent activities</p>
+ </a>
+ <a mat-list-item [routerLink]="['payroll']" *hasPermission="{ id: 'payroll_configuration', accessLevel: 'READ'}">
+ <mat-icon matListAvatar>receipt</mat-icon>
+ <h3 matLine translate>Payroll</h3>
+ <p matLine translate>Manage payroll distributions</p>
+ </a>
+ </mat-nav-list>
+ </ng-container>
+ <mat-list right>
+ <h3 mat-subheader translate>Current status</h3>
+ <fims-state-display [state]="customer.currentState"></fims-state-display>
+ <h3 mat-subheader translate>Address</h3>
+ <mat-list-item>
+ <mat-icon matListAvatar>location_on</mat-icon>
+ <h3 matLine>{{customer.address?.street}}, {{customer.address?.city}}, {{customer.address?.postalCode}},
+ {{customer.address?.country}}</h3>
+ </mat-list-item>
+ <h3 mat-subheader translate>Contact information</h3>
+ <mat-list-item [ngSwitch]="detail.type" *ngFor="let detail of customer.contactDetails">
+ <mat-icon *ngSwitchCase="'EMAIL'" matListAvatar>email</mat-icon>
+ <mat-icon *ngSwitchCase="'PHONE'" matListAvatar>phone</mat-icon>
+ <mat-icon *ngSwitchCase="'MOBILE'" matListAvatar>smartphone</mat-icon>
+ <h3 matLine>{{detail.value}}</h3>
+ </mat-list-item>
+ <mat-list-item *ngIf="!customer.contactDetails?.length">
+ <h3 matLine translate>No contact details available</h3>
+ </mat-list-item>
+ <h3 mat-subheader translate>Birthday</h3>
+ <mat-list-item>
+ <mat-icon matListAvatar>cake</mat-icon>
+ <h3 matLine>{{customer.dateOfBirth | displayFimsDate}}</h3>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Application date</h3>
+ <h3 matLine>{{customer.applicationDate | date:'shortDate'}}</h3>
+ </mat-list-item>
+ <fims-customer-custom-values
+ [catalog]="catalog$ | async"
+ [values]="customer.customValues">
+ </fims-customer-custom-values>
+ <mat-list-item>
+ <h3 matLine translate>Created by</h3>
+ <p matLine>{{customer.createdBy}} - {{customer.createdOn | date:'medium'}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Last modified by</h3>
+ <p matLine>{{customer.lastModifiedBy}} - {{customer.lastModifiedOn | date:'medium'}}</p>
+ </mat-list-item>
+ </mat-list>
+ </fims-two-column-layout>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Edit member ' | translate}}" icon="mode_edit" [link]="['edit']" [permission]="{ id: 'customer_customers', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/customers/detail/customer.detail.component.scss b/src/app/customers/detail/customer.detail.component.scss
new file mode 100644
index 0000000..aaaf060
--- /dev/null
+++ b/src/app/customers/detail/customer.detail.component.scss
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+.header-button-right {
+ float: right
+}
diff --git a/src/app/customers/detail/customer.detail.component.ts b/src/app/customers/detail/customer.detail.component.ts
new file mode 100644
index 0000000..7cdaa9a
--- /dev/null
+++ b/src/app/customers/detail/customer.detail.component.ts
@@ -0,0 +1,83 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ActivatedRoute, Router} from '@angular/router';
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {Customer} from '../../services/customer/domain/customer.model';
+import {Catalog} from '../../services/catalog/domain/catalog.model';
+import * as fromCustomers from '../store';
+import {Subscription} from 'rxjs/Subscription';
+import {CustomersStore} from '../store/index';
+import {CustomerService} from '../../services/customer/customer.service';
+import {Observable} from 'rxjs/Observable';
+
+
+interface CustomDetailField {
+ label: string;
+ value: string;
+}
+
+@Component({
+ templateUrl: './customer.detail.component.html',
+ styleUrls: ['./customer.detail.component.scss']
+})
+export class CustomerDetailComponent implements OnInit, OnDestroy {
+
+ portrait: Blob;
+
+ private customerSubscription: Subscription;
+
+ customer: Customer;
+
+ catalog$: Observable<Catalog>;
+
+ isCustomerActive: boolean;
+
+ constructor(private route: ActivatedRoute, private router: Router, private store: CustomersStore,
+ private customerService: CustomerService) {}
+
+ ngOnInit(): void {
+ this.customerSubscription = this.store.select(fromCustomers.getSelectedCustomer)
+ .filter(customer => !!customer)
+ .do(customer => this.customer = customer)
+ .do(customer => this.isCustomerActive = customer.currentState === 'ACTIVE')
+ .flatMap(customer => this.customerService.getPortrait(customer.identifier))
+ .subscribe(portrait => this.portrait = portrait);
+
+ this.catalog$ = this.store.select(fromCustomers.getCustomerCatalog);
+ }
+
+ ngOnDestroy(): void {
+ this.customerSubscription.unsubscribe();
+ }
+
+ searchCustomer(term): void {
+ if (term) {
+ this.router.navigate(['../../../'], { queryParams: { term: term }, relativeTo: this.route });
+ }
+ }
+
+ changePortrait(): void {
+ this.router.navigate(['portrait'], { relativeTo: this.route });
+ }
+
+ goToTasks(): void {
+ this.router.navigate(['tasks'], { relativeTo: this.route });
+ }
+
+}
diff --git a/src/app/customers/detail/customer.index.component.html b/src/app/customers/detail/customer.index.component.html
new file mode 100644
index 0000000..ca721b3
--- /dev/null
+++ b/src/app/customers/detail/customer.index.component.html
@@ -0,0 +1,18 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<router-outlet></router-outlet>
diff --git a/src/app/customers/detail/customer.index.component.ts b/src/app/customers/detail/customer.index.component.ts
new file mode 100644
index 0000000..1e7d89d
--- /dev/null
+++ b/src/app/customers/detail/customer.index.component.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {SelectAction} from '../store/customer.actions';
+import {CustomersStore} from '../store/index';
+import {Subscription} from 'rxjs/Subscription';
+import {ActivatedRoute} from '@angular/router';
+
+@Component({
+ templateUrl: './customer.index.component.html'
+})
+export class CustomerIndexComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ constructor(private route: ActivatedRoute, private customersStore: CustomersStore) {}
+
+ ngOnInit(): void {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectAction(params['id']))
+ .subscribe(this.customersStore);
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ }
+}
diff --git a/src/app/customers/detail/identityCard/form/create.form.component.html b/src/app/customers/detail/identityCard/form/create.form.component.html
new file mode 100644
index 0000000..2a6cf3c
--- /dev/null
+++ b/src/app/customers/detail/identityCard/form/create.form.component.html
@@ -0,0 +1,24 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Create new identification card' | translate}}">
+ <fims-identity-card-form #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [identificationCard]="identificationCard">
+ </fims-identity-card-form>
+</fims-layout-card-over>
diff --git a/src/app/customers/detail/identityCard/form/create.form.component.ts b/src/app/customers/detail/identityCard/form/create.form.component.ts
new file mode 100644
index 0000000..7cdfb8a
--- /dev/null
+++ b/src/app/customers/detail/identityCard/form/create.form.component.ts
@@ -0,0 +1,85 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {Subscription} from 'rxjs/Subscription';
+import {IdentificationCard} from '../../../../services/customer/domain/identification-card.model';
+import {IdentityCardFormComponent} from './identity-card-form.component';
+import {ActivatedRoute, Router} from '@angular/router';
+import * as fromCustomers from '../../../store/index';
+import {CustomersStore} from '../../../store/index';
+import {CREATE, RESET_FORM} from '../../../store/identityCards/identity-cards.actions';
+import {Error} from '../../../../services/domain/error.model';
+import {Customer} from '../../../../services/customer/domain/customer.model';
+
+@Component({
+ templateUrl: './create.form.component.html'
+})
+export class CreateCustomerIdentificationCardFormComponent implements OnInit, OnDestroy {
+
+ private formStateSubscription: Subscription;
+
+ private customerSubscription: Subscription;
+
+ private customer: Customer;
+
+ @ViewChild('form') formComponent: IdentityCardFormComponent;
+
+ identificationCard: IdentificationCard = {
+ type: '',
+ number: '',
+ expirationDate: null
+ };
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: CustomersStore) {}
+
+ ngOnInit() {
+ this.customerSubscription = this.store.select(fromCustomers.getSelectedCustomer)
+ .subscribe(customer => this.customer = customer);
+
+ this.formStateSubscription = this.store.select(fromCustomers.getCustomerIdentificationCardFormError)
+ .filter((error: Error) => !!error)
+ .subscribe((error: Error) => {
+ this.formComponent.showNumberValidationError();
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.formStateSubscription.unsubscribe();
+ this.customerSubscription.unsubscribe();
+
+ this.store.dispatch({ type: RESET_FORM });
+ }
+
+ onSave(identificationCard: IdentificationCard) {
+ const customerId = this.customer.identifier;
+ this.store.dispatch({ type: CREATE, payload: {
+ customerId,
+ identificationCard,
+ activatedRoute: this.route
+ } });
+ }
+
+ onCancel() {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/customers/detail/identityCard/form/edit.form.component.html b/src/app/customers/detail/identityCard/form/edit.form.component.html
new file mode 100644
index 0000000..978b4e8
--- /dev/null
+++ b/src/app/customers/detail/identityCard/form/edit.form.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Edit identification card' | translate}}">
+ <fims-identity-card-form #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [identificationCard]="identificationCard$ | async"
+ [editMode]="true">
+ </fims-identity-card-form>
+</fims-layout-card-over>
diff --git a/src/app/customers/detail/identityCard/form/edit.form.component.ts b/src/app/customers/detail/identityCard/form/edit.form.component.ts
new file mode 100644
index 0000000..bc773d6
--- /dev/null
+++ b/src/app/customers/detail/identityCard/form/edit.form.component.ts
@@ -0,0 +1,70 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {Subscription} from 'rxjs/Subscription';
+import {IdentificationCard} from '../../../../services/customer/domain/identification-card.model';
+import {Observable} from 'rxjs/Observable';
+import * as fromCustomers from '../../../store/index';
+import {CustomersStore} from '../../../store/index';
+import {ActivatedRoute, Router} from '@angular/router';
+import {UPDATE} from '../../../store/identityCards/identity-cards.actions';
+import {Customer} from '../../../../services/customer/domain/customer.model';
+
+@Component({
+ templateUrl: './edit.form.component.html'
+})
+export class EditCustomerIdentificationCardFormComponent implements OnInit, OnDestroy {
+
+ private customerSubscription: Subscription;
+
+ private customer: Customer;
+
+ identificationCard$: Observable<IdentificationCard>;
+
+ constructor(private router: Router, private route: ActivatedRoute, private customersStore: CustomersStore) {}
+
+ ngOnInit() {
+ this.customerSubscription = this.customersStore.select(fromCustomers.getSelectedCustomer)
+ .subscribe(customer => this.customer = customer);
+
+ this.identificationCard$ = this.customersStore.select(fromCustomers.getSelectedIdentificationCard);
+ }
+
+ ngOnDestroy(): void {
+ this.customerSubscription.unsubscribe();
+ }
+
+ onSave(identificationCard: IdentificationCard) {
+ const customerId = this.customer.identifier;
+
+ this.customersStore.dispatch({ type: UPDATE, payload: {
+ customerId,
+ identificationCard,
+ activatedRoute: this.route
+ } });
+ }
+
+ onCancel() {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/customers/detail/identityCard/form/identity-card-form.component.html b/src/app/customers/detail/identityCard/form/identity-card-form.component.html
new file mode 100644
index 0000000..4b40a76
--- /dev/null
+++ b/src/app/customers/detail/identityCard/form/identity-card-form.component.html
@@ -0,0 +1,38 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Identification card' | translate}}" [state]="form.valid ? 'complete' : form.pristine ? 'none' : 'required'">
+ <form [formGroup]="form" layout="column">
+ <fims-id-input [form]="form" controlName="number" [placeholder]="'Number'" [readonly]="editMode"></fims-id-input>
+ <fims-text-input [form]="form" controlName="type" placeholder="{{'Type' | translate}}"></fims-text-input>
+ <fims-date-input [form]="form" controlName="expirationDate" placeholder="{{'Expiration date' | translate}}"></fims-date-input>
+ <fims-text-input [form]="form" controlName="issuer" placeholder="{{'Issuer' | translate}}"></fims-text-input>
+ </form>
+ </td-step>
+ <td-step label="{{'Final step' | translate}}" [state]="'complete'">
+ <ng-template td-step-summary>
+ <fims-form-final-action
+ [resourceName]="'IDENTIFICATION CARD'"
+ [editMode]="editMode"
+ [disabled]="!form.valid"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/customers/detail/identityCard/form/identity-card-form.component.spec.ts b/src/app/customers/detail/identityCard/form/identity-card-form.component.spec.ts
new file mode 100644
index 0000000..042f2bc
--- /dev/null
+++ b/src/app/customers/detail/identityCard/form/identity-card-form.component.spec.ts
@@ -0,0 +1,131 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {IdentityCardFormComponent} from './identity-card-form.component';
+import {MatButtonModule, MatCardModule, MatIconModule, MatInputModule} from '@angular/material';
+import {CovalentFileModule, CovalentStepsModule} from '@covalent/core';
+import {ReactiveFormsModule} from '@angular/forms';
+import {setValueByCssSelector} from '../../../../common/testing/input-fields';
+import {By} from '@angular/platform-browser';
+import {Component, DebugElement} from '@angular/core';
+import {IdentificationCard} from '../../../../services/customer/domain/identification-card.model';
+import {TranslateModule} from '@ngx-translate/core';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {dateAsISOString, toFimsDate} from '../../../../services/domain/date.converter';
+import {FimsSharedModule} from '../../../../common/common.module';
+
+describe('Test identity card form component', () => {
+
+ let fixture: ComponentFixture<TestComponent>;
+
+ let formComponent: TestComponent;
+
+ let oneDayAhead: string;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [
+ TestComponent,
+ IdentityCardFormComponent
+ ],
+ imports: [
+ TranslateModule.forRoot(),
+ FimsSharedModule,
+ ReactiveFormsModule,
+ MatInputModule,
+ MatIconModule,
+ MatButtonModule,
+ MatCardModule,
+ CovalentStepsModule,
+ CovalentFileModule,
+ NoopAnimationsModule
+ ]
+ });
+
+ fixture = TestBed.createComponent(TestComponent);
+ formComponent = fixture.componentInstance;
+
+ fixture.detectChanges();
+ }));
+
+ beforeEach(() => {
+ const today = new Date();
+ today.setDate(today.getDate() + 1);
+ oneDayAhead = dateAsISOString(today);
+ });
+
+ function setValidValues(): void {
+ setValueByCssSelector(fixture, '#number', 'test');
+ setValueByCssSelector(fixture, '#type', 'test');
+ setValueByCssSelector(fixture, '#expirationDate', oneDayAhead);
+ setValueByCssSelector(fixture, '#issuer', 'test');
+ }
+
+ it('should disable/enable save button', () => {
+ const button: DebugElement = fixture.debugElement.query(By.css('button[color="primary"]'));
+
+ expect(button.properties['disabled']).toBeTruthy('Button should be disabled');
+
+ setValidValues();
+
+ fixture.detectChanges();
+
+ expect(button.properties['disabled']).toBeFalsy('Button should be enabled');
+ });
+
+ it('should set the correct form values', () => {
+ setValidValues();
+
+ fixture.detectChanges();
+
+ const button: DebugElement = fixture.debugElement.query(By.css('button[color="primary"]'));
+
+ button.nativeElement.click();
+
+ const expectedResult: IdentificationCard = {
+ type: 'test',
+ issuer: 'test',
+ expirationDate: toFimsDate(oneDayAhead),
+ number: 'test'
+ };
+
+ fixture.detectChanges();
+
+ expect(expectedResult).toEqual(fixture.componentInstance.output);
+ });
+
+});
+
+@Component({
+ template: '<fims-identity-card-form [identificationCard]="input" (onSave)="onSave($event)"></fims-identity-card-form>'
+})
+class TestComponent {
+
+ input: IdentificationCard = {
+ type: '',
+ number: '',
+ expirationDate: null
+ };
+
+ output: IdentificationCard;
+
+ onSave(identificationCard: IdentificationCard): void {
+ this.output = identificationCard;
+ }
+}
diff --git a/src/app/customers/detail/identityCard/form/identity-card-form.component.ts b/src/app/customers/detail/identityCard/form/identity-card-form.component.ts
new file mode 100644
index 0000000..02359a6
--- /dev/null
+++ b/src/app/customers/detail/identityCard/form/identity-card-form.component.ts
@@ -0,0 +1,101 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
+import {FormComponent} from '../../../../common/forms/form.component';
+import {FormBuilder, Validators} from '@angular/forms';
+import {IdentificationCard} from '../../../../services/customer/domain/identification-card.model';
+import {ExpirationDate} from '../../../../services/customer/domain/expiration-date.model';
+import {FimsValidators} from '../../../../common/validator/validators';
+import {TdStepComponent} from '@covalent/core';
+
+@Component({
+ selector: 'fims-identity-card-form',
+ templateUrl: './identity-card-form.component.html'
+})
+export class IdentityCardFormComponent extends FormComponent<IdentificationCard> implements OnInit {
+
+ @ViewChild('detailsStep') step: TdStepComponent;
+
+ @Input() identificationCard: IdentificationCard;
+
+ @Input() editMode = false;
+
+ @Output('onSave') onSave = new EventEmitter<IdentificationCard>();
+
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder) {
+ super();
+ }
+
+ ngOnInit(): void {
+ this.form = this.formBuilder.group({
+ number: [this.identificationCard.number, [Validators.required, Validators.minLength(3), Validators.maxLength(32),
+ FimsValidators.urlSafe]],
+ type: [this.identificationCard.type, [Validators.required, Validators.maxLength(128)]],
+ expirationDate: [this.formatDate(this.identificationCard.expirationDate), [Validators.required, FimsValidators.afterToday]],
+ issuer: [this.identificationCard.issuer, [Validators.required, Validators.maxLength(256)]]
+ });
+
+ this.step.open();
+ }
+
+ showNumberValidationError(): void {
+ this.setError('number', 'unique', true);
+ }
+
+ private formatDate(expirationDate: ExpirationDate): string {
+ if (!expirationDate) {
+ return '';
+ }
+ return `${expirationDate.year}-${this.addZero(expirationDate.month)}-${this.addZero(expirationDate.day)}`;
+ }
+
+ private addZero(value: number): string {
+ return ('0' + value).slice(-2);
+ }
+
+ get formData(): IdentificationCard {
+ // Not needed
+ return;
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+ save(): void {
+ const expirationDate: string = this.form.get('expirationDate').value;
+ const chunks: string[] = expirationDate.split('-');
+
+ const identificationCard: IdentificationCard = {
+ type: this.form.get('type').value,
+ number: this.form.get('number').value,
+ expirationDate: {
+ day: Number(chunks[2]),
+ month: Number(chunks[1]),
+ year: Number(chunks[0])
+ },
+ issuer: this.form.get('issuer').value
+ };
+
+ this.onSave.emit(identificationCard);
+ }
+
+}
diff --git a/src/app/customers/detail/identityCard/identity-card-exists.guard.ts b/src/app/customers/detail/identityCard/identity-card-exists.guard.ts
new file mode 100644
index 0000000..3e80d26
--- /dev/null
+++ b/src/app/customers/detail/identityCard/identity-card-exists.guard.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import {Injectable} from '@angular/core';
+import * as fromCustomers from '../../store';
+import {Observable} from 'rxjs/Observable';
+import {of} from 'rxjs/observable/of';
+import {CustomersStore} from '../../store/index';
+import {CustomerService} from '../../../services/customer/customer.service';
+import {ExistsGuardService} from '../../../common/guards/exists-guard';
+import {LoadAction} from '../../store/identityCards/identity-cards.actions';
+
+@Injectable()
+export class IdentityCardExistsGuard implements CanActivate {
+
+ constructor(private store: CustomersStore,
+ private customerService: CustomerService,
+ private existsGuardService: ExistsGuardService) {
+ }
+
+ hasIdentificationCardInStore(number: string): Observable<boolean> {
+ const timestamp$: Observable<number> = this.store.select(fromCustomers.getIdentificationCardLoadedAt)
+ .map(loadedAt => loadedAt[number]);
+
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ hasIdentificationCardInApi(customerId: string, number: string): Observable<boolean> {
+ const getIdentificationCard: Observable<any> = this.customerService.getIdentificationCard(customerId, number)
+ .map(identificationCardEntity => new LoadAction({
+ resource: identificationCardEntity
+ }))
+ .do((action: LoadAction) => this.store.dispatch(action))
+ .map(identificationCard => !!identificationCard);
+
+ return this.existsGuardService.routeTo404OnError(getIdentificationCard);
+ }
+
+ hasIdentificationCard(customerId: string, number: string): Observable<boolean> {
+ return this.hasIdentificationCardInStore(number)
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+ return this.hasIdentificationCardInApi(customerId, number);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasIdentificationCard(route.parent.params['id'], route.params['number']);
+ }
+}
diff --git a/src/app/customers/detail/identityCard/identity-card.detail.component.html b/src/app/customers/detail/identityCard/identity-card.detail.component.html
new file mode 100644
index 0000000..96627d0
--- /dev/null
+++ b/src/app/customers/detail/identityCard/identity-card.detail.component.html
@@ -0,0 +1,52 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{ 'Identification card' | translate }}" [navigateBackTo]="['../../../../../../']">
+ <fims-layout-card-over-header-menu layout="row" layout-align="end center">
+ <a mat-icon-button title="{{'Edit identification card' | translate}}" [routerLink]="['edit']" *hasPermission="{ id: 'customer_identifications', accessLevel: 'CHANGE' }">
+ <mat-icon>mode_edit</mat-icon>
+ </a>
+ <button mat-icon-button (click)="deleteIdentificationCard()" title="{{'Delete this identification card' | translate}}" *hasPermission="{ id: 'customer_identifications', accessLevel: 'DELETE' }"><mat-icon>delete</mat-icon></button>
+ </fims-layout-card-over-header-menu>
+ <div class="mat-content inset" flex>
+ <mat-list>
+ <mat-list-item>
+ <h3 matLine translate>Number</h3>
+ <p matLine>{{identificationCard?.number}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Type</h3>
+ <p matLine>{{identificationCard?.type}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Issuer</h3>
+ <p matLine>{{identificationCard?.issuer}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Expiration date</h3>
+ <p matLine>{{identificationCard?.expirationDate | displayFimsDate}}</p>
+ </mat-list-item>
+ </mat-list>
+ <fims-identification-card-scan-list
+ [scans]="scans$ | async"
+ (onView)="viewScan($event)"
+ (onUpload)="uploadScan($event)"
+ (onDelete)="deleteScan($event)">
+ </fims-identification-card-scan-list>
+ </div>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Add identification card scan' | translate}}" icon="add_a_photo" [link]="['addScan']" [permission]="{ id: 'customer_identifications', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/customers/detail/identityCard/identity-card.detail.component.ts b/src/app/customers/detail/identityCard/identity-card.detail.component.ts
new file mode 100644
index 0000000..52d1bc9
--- /dev/null
+++ b/src/app/customers/detail/identityCard/identity-card.detail.component.ts
@@ -0,0 +1,150 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {Subscription} from 'rxjs/Subscription';
+import * as fromCustomers from '../../store/index';
+import {CustomersStore} from '../../store/index';
+import {Customer} from '../../../services/customer/domain/customer.model';
+import {DELETE} from '../../store/identityCards/identity-cards.actions';
+import {CREATE, DELETE as DELETE_SCAN, LoadAllAction} from '../../store/identityCards/scans/scans.actions';
+import {ActivatedRoute} from '@angular/router';
+import {IdentificationCard} from '../../../services/customer/domain/identification-card.model';
+import {Observable} from 'rxjs/Observable';
+import {TranslateService} from '@ngx-translate/core';
+import {TdDialogService} from '@covalent/core';
+import {IdentificationCardScan} from '../../../services/customer/domain/identification-card-scan.model';
+import {UploadIdentificationCardScanEvent} from './scans/scan.list.component';
+import {ImageComponent} from '../../../common/image/image.component';
+import {CustomerService} from '../../../services/customer/customer.service';
+
+@Component({
+ templateUrl: './identity-card.detail.component.html'
+})
+export class CustomerIdentityCardDetailComponent implements OnInit, OnDestroy {
+
+ private actionSubscription: Subscription;
+
+ private customer: Customer;
+
+ identificationCard: IdentificationCard;
+
+ scans$: Observable<IdentificationCardScan[]>;
+
+ constructor(private route: ActivatedRoute, private customersStore: CustomersStore, private translate: TranslateService,
+ private dialogService: TdDialogService, private customerService: CustomerService) {}
+
+ ngOnInit(): void {
+ this.scans$ = this.customersStore.select(fromCustomers.getAllIdentificationCardScanEntities);
+
+ this.actionSubscription = Observable.combineLatest(
+ this.customersStore.select(fromCustomers.getSelectedIdentificationCard)
+ .filter(identificationCard => !!identificationCard),
+ this.customersStore.select(fromCustomers.getSelectedCustomer),
+ (identificationCard, customer) => ({
+ identificationCard,
+ customer
+ }))
+ .do(result => this.customer = result.customer)
+ .do(result => this.identificationCard = result.identificationCard)
+ .map(result => new LoadAllAction({
+ customerIdentifier: result.customer.identifier,
+ identificationCardNumber: result.identificationCard.number
+ }))
+ .subscribe(this.customersStore);
+ }
+
+ ngOnDestroy(): void {
+ this.actionSubscription.unsubscribe();
+ }
+
+ confirmDeletion(): Observable<boolean> {
+ const message = 'Do you want to delete this identification card?';
+ const title = 'Confirm deletion';
+ const button = 'DELETE IDENTIFICATION CARD';
+
+ return this.translate.get([title, message, button])
+ .flatMap(result =>
+ this.dialogService.openConfirm({
+ message: result[message],
+ title: result[title],
+ acceptButton: result[button]
+ }).afterClosed()
+ );
+ }
+
+ deleteIdentificationCard(): void {
+ this.confirmDeletion()
+ .filter(accept => accept)
+ .subscribe(() => {
+ this.customersStore.dispatch({ type: DELETE, payload: {
+ customerId: this.customer.identifier,
+ identificationCard: this.identificationCard,
+ activatedRoute: this.route
+ }});
+ });
+ }
+
+ viewScan(identifier: string): void {
+ this.customerService.getIdentificationCardScanImage(this.customer.identifier, this.identificationCard.number, identifier)
+ .subscribe(blob => {
+ this.dialogService.open(ImageComponent, {
+ data: blob
+ });
+ });
+ }
+
+ uploadScan(event: UploadIdentificationCardScanEvent): void {
+ this.customersStore.dispatch({
+ type: CREATE,
+ payload: {
+ customerIdentifier: this.customer.identifier,
+ identificationCardNumber: this.identificationCard.number,
+ scan: event.scan,
+ file: event.file
+ }
+ });
+ }
+
+ confirmScanDeletion(): Observable<boolean> {
+ const message = 'Do you want to delete this scan?';
+ const title = 'Confirm deletion';
+ const button = 'DELETE SCAN';
+
+ return this.translate.get([title, message, button])
+ .flatMap(result =>
+ this.dialogService.openConfirm({
+ message: result[message],
+ title: result[title],
+ acceptButton: result[button]
+ }).afterClosed()
+ );
+ }
+
+ deleteScan(scan: IdentificationCardScan): void {
+ this.confirmScanDeletion()
+ .filter(accept => accept)
+ .subscribe(() => {
+ this.customersStore.dispatch({ type: DELETE_SCAN, payload: {
+ customerIdentifier: this.customer.identifier,
+ identificationCardNumber: this.identificationCard.number,
+ scan
+ }});
+ });
+ }
+}
diff --git a/src/app/customers/detail/identityCard/identity-card.index.component.html b/src/app/customers/detail/identityCard/identity-card.index.component.html
new file mode 100644
index 0000000..ca721b3
--- /dev/null
+++ b/src/app/customers/detail/identityCard/identity-card.index.component.html
@@ -0,0 +1,18 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<router-outlet></router-outlet>
diff --git a/src/app/customers/detail/identityCard/identity-card.index.component.ts b/src/app/customers/detail/identityCard/identity-card.index.component.ts
new file mode 100644
index 0000000..7380648
--- /dev/null
+++ b/src/app/customers/detail/identityCard/identity-card.index.component.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {Subscription} from 'rxjs/Subscription';
+import {ActivatedRoute} from '@angular/router';
+import {CustomersStore} from '../../store/index';
+import {SelectAction} from '../../store/identityCards/identity-cards.actions';
+
+@Component({
+ templateUrl: './identity-card.index.component.html'
+})
+export class CustomerIdentityCardIndexComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ constructor(private route: ActivatedRoute, private customersStore: CustomersStore) {}
+
+ ngOnInit(): void {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectAction(params['number']))
+ .subscribe(this.customersStore);
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ }
+}
diff --git a/src/app/customers/detail/identityCard/identity-card.list.component.html b/src/app/customers/detail/identityCard/identity-card.list.component.html
new file mode 100644
index 0000000..ad263cf
--- /dev/null
+++ b/src/app/customers/detail/identityCard/identity-card.list.component.html
@@ -0,0 +1,27 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Manage identification cards' | translate}}" [navigateBackTo]="['../../../../']">
+ <fims-data-table
+ (onActionCellClick)="rowSelect($event)"
+ [columns]="columns"
+ [data]="identityCardData$ | async"
+ [sortable]="false"
+ [pageable]="false">
+ </fims-data-table>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Create new identication card' | translate}}" icon="add" [link]="['create']" [permission]="{ id: 'customer_identifications', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/customers/detail/identityCard/identity-card.list.component.ts b/src/app/customers/detail/identityCard/identity-card.list.component.ts
new file mode 100644
index 0000000..69f7f26
--- /dev/null
+++ b/src/app/customers/detail/identityCard/identity-card.list.component.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {Subscription} from 'rxjs/Subscription';
+import * as fromCustomers from '../../store/index';
+import {CustomersStore} from '../../store/index';
+import {Observable} from 'rxjs/Observable';
+import {TableData} from '../../../common/data-table/data-table.component';
+import {LOAD_ALL} from '../../store/identityCards/identity-cards.actions';
+import {IdentificationCard} from '../../../services/customer/domain/identification-card.model';
+import {ActivatedRoute, Router} from '@angular/router';
+
+@Component({
+ templateUrl: './identity-card.list.component.html'
+})
+export class CustomerIdentityCardListComponent implements OnInit, OnDestroy {
+
+ private customerSubscription: Subscription;
+
+ identityCardData$: Observable<TableData>;
+
+ columns: any[] = [
+ { name: 'number', label: 'Number' },
+ { name: 'type', label: 'Type' },
+ { name: 'issuer', label: 'Issuer' }
+ ];
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: CustomersStore) {}
+
+ ngOnInit(): void {
+ this.identityCardData$ = this.store.select(fromCustomers.getAllCustomerIdentificationCardEntities)
+ .map(entities => ({
+ data: entities,
+ totalElements: entities.length,
+ totalPages: 1
+ }));
+
+ this.customerSubscription = this.store.select(fromCustomers.getSelectedCustomer)
+ .subscribe(customer => {
+ this.store.dispatch({ type: LOAD_ALL, payload: customer.identifier});
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.customerSubscription.unsubscribe();
+ }
+
+ rowSelect(identificationCard: IdentificationCard): void {
+ this.router.navigate(['detail', identificationCard.number], { relativeTo: this.route });
+ }
+
+}
diff --git a/src/app/customers/detail/identityCard/identity-card.module.ts b/src/app/customers/detail/identityCard/identity-card.module.ts
new file mode 100644
index 0000000..82e7937
--- /dev/null
+++ b/src/app/customers/detail/identityCard/identity-card.module.ts
@@ -0,0 +1,87 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {FimsSharedModule} from '../../../common/common.module';
+import {IdentityCardRoutes} from './identity-card.routing';
+import {RouterModule} from '@angular/router';
+import {NgModule} from '@angular/core';
+import {IdentityCardExistsGuard} from './identity-card-exists.guard';
+import {CustomerIdentityCardListComponent} from './identity-card.list.component';
+import {CreateCustomerIdentificationCardFormComponent} from './form/create.form.component';
+import {CustomerIdentityCardIndexComponent} from './identity-card.index.component';
+import {CustomerIdentityCardDetailComponent} from './identity-card.detail.component';
+import {EditCustomerIdentificationCardFormComponent} from './form/edit.form.component';
+import {CustomerIdentificationCardNotificationEffects} from '../../store/identityCards/effects/notification.effects';
+import {EffectsModule} from '@ngrx/effects';
+import {CustomerIdentificationCardRouteEffects} from '../../store/identityCards/effects/route.effects';
+import {CustomerIdentificationCardApiEffects} from '../../store/identityCards/effects/service.effects';
+import {TranslateModule} from '@ngx-translate/core';
+import {CommonModule} from '@angular/common';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {MatButtonModule, MatDatepickerModule, MatIconModule, MatInputModule, MatListModule, MatToolbarModule} from '@angular/material';
+import {CovalentFileModule, CovalentStepsModule} from '@covalent/core';
+import {IdentityCardFormComponent} from './form/identity-card-form.component';
+import {CustomerIdentityCardScanListComponent} from './scans/scan.list.component';
+import {CustomerIdentificationCardScanApiEffects} from '../../store/identityCards/scans/effects/service.effects';
+import {CustomerIdentificationCardScanNotificationEffects} from '../../store/identityCards/scans/effects/notification.effects';
+import {CreateIdentificationCardScanComponent} from './scans/form/create.form.component';
+import {IdentificationCardScanComponent} from './scans/form/scan.form.component';
+import {CustomerIdentificationCardScanRouteEffects} from '../../store/identityCards/scans/effects/route.effects';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(IdentityCardRoutes),
+ FimsSharedModule,
+ TranslateModule,
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ MatIconModule,
+ MatListModule,
+ MatToolbarModule,
+ MatInputModule,
+ MatButtonModule,
+ MatDatepickerModule,
+ CovalentStepsModule,
+ CovalentFileModule,
+
+ EffectsModule.run(CustomerIdentificationCardApiEffects),
+ EffectsModule.run(CustomerIdentificationCardRouteEffects),
+ EffectsModule.run(CustomerIdentificationCardNotificationEffects),
+
+ EffectsModule.run(CustomerIdentificationCardScanApiEffects),
+ EffectsModule.run(CustomerIdentificationCardScanRouteEffects),
+ EffectsModule.run(CustomerIdentificationCardScanNotificationEffects)
+ ],
+ declarations: [
+ CustomerIdentityCardListComponent,
+ CreateCustomerIdentificationCardFormComponent,
+ CustomerIdentityCardIndexComponent,
+ CustomerIdentityCardDetailComponent,
+ EditCustomerIdentificationCardFormComponent,
+ IdentityCardFormComponent,
+ CustomerIdentityCardScanListComponent,
+ CreateIdentificationCardScanComponent,
+ IdentificationCardScanComponent
+ ],
+ providers: [
+ IdentityCardExistsGuard
+ ]
+})
+export class IdentityCardModule {}
+
diff --git a/src/app/customers/detail/identityCard/identity-card.routing.ts b/src/app/customers/detail/identityCard/identity-card.routing.ts
new file mode 100644
index 0000000..13f7267
--- /dev/null
+++ b/src/app/customers/detail/identityCard/identity-card.routing.ts
@@ -0,0 +1,78 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Routes} from '@angular/router';
+import {CustomerIdentityCardListComponent} from './identity-card.list.component';
+import {CreateCustomerIdentificationCardFormComponent} from './form/create.form.component';
+import {CustomerIdentityCardIndexComponent} from './identity-card.index.component';
+import {IdentityCardExistsGuard} from './identity-card-exists.guard';
+import {CustomerIdentityCardDetailComponent} from './identity-card.detail.component';
+import {EditCustomerIdentificationCardFormComponent} from './form/edit.form.component';
+import {CreateIdentificationCardScanComponent} from './scans/form/create.form.component';
+
+export const IdentityCardRoutes: Routes = [
+ {
+ path: '',
+ component: CustomerIdentityCardListComponent,
+ data: {
+ title: 'Manage identification cards',
+ hasPermission: { id: 'customer_identifications', accessLevel: 'READ' }
+ }
+ },
+ {
+ path: 'create',
+ component: CreateCustomerIdentificationCardFormComponent,
+ data: {
+ title: 'Create identification card',
+ hasPermission: { id: 'customer_identifications', accessLevel: 'CHANGE' }
+ },
+ },
+ {
+ path: 'detail/:number',
+ component: CustomerIdentityCardIndexComponent,
+ canActivate: [ IdentityCardExistsGuard ],
+ children: [
+ {
+ path: '',
+ component: CustomerIdentityCardDetailComponent,
+ data: {
+ title: 'Identification Card',
+ hasPermission: { id: 'customer_identifications', accessLevel: 'READ' }
+ }
+ },
+ {
+ path: 'edit',
+ component: EditCustomerIdentificationCardFormComponent,
+ data: {
+ title: 'Edit identification card',
+ hasPermission: {id: 'customer_identifications', accessLevel: 'CHANGE'}
+ },
+ },
+ {
+ path: 'addScan',
+ component: CreateIdentificationCardScanComponent,
+ data: {
+ title: 'Add identification card scan',
+ hasPermission: {id: 'customer_identifications', accessLevel: 'CHANGE'}
+ }
+ }
+ ]
+ }
+];
+
+
diff --git a/src/app/customers/detail/identityCard/scans/form/create.form.component.html b/src/app/customers/detail/identityCard/scans/form/create.form.component.html
new file mode 100644
index 0000000..cc47284
--- /dev/null
+++ b/src/app/customers/detail/identityCard/scans/form/create.form.component.html
@@ -0,0 +1,24 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Upload new identification card scan' | translate}}">
+ <fims-identity-card-scan-form #form
+ [error]="error$ | async"
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()">
+ </fims-identity-card-scan-form>
+</fims-layout-card-over>
diff --git a/src/app/customers/detail/identityCard/scans/form/create.form.component.ts b/src/app/customers/detail/identityCard/scans/form/create.form.component.ts
new file mode 100644
index 0000000..d49151e
--- /dev/null
+++ b/src/app/customers/detail/identityCard/scans/form/create.form.component.ts
@@ -0,0 +1,90 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {IdentificationCardScan} from '../../../../../services/customer/domain/identification-card-scan.model';
+import * as fromCustomers from '../../../../store/index';
+import {CustomersStore} from '../../../../store/index';
+import {IdentificationCardScanComponent, IdentityCardScanFormData} from './scan.form.component';
+import {CREATE, RESET_FORM} from '../../../../store/identityCards/scans/scans.actions';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Observable} from 'rxjs/Observable';
+import {Error} from '../../../../../services/domain/error.model';
+import {Subscription} from 'rxjs/Subscription';
+import {IdentificationCard} from '../../../../../services/customer/domain/identification-card.model';
+import {Customer} from '../../../../../services/customer/domain/customer.model';
+
+@Component({
+ templateUrl: './create.form.component.html'
+})
+export class CreateIdentificationCardScanComponent implements OnInit, OnDestroy {
+
+ private customerSubscription: Subscription;
+
+ private identificationCardSubscription: Subscription;
+
+ customer: Customer;
+
+ identificationCard: IdentificationCard;
+
+ error$: Observable<Error>;
+
+ @ViewChild('form') formComponent: IdentificationCardScanComponent;
+
+ constructor(private router: Router, private route: ActivatedRoute, private customersStore: CustomersStore) {}
+
+ ngOnInit(): void {
+ this.error$ = this.customersStore.select(fromCustomers.getCustomerIdentificationCardScanFormError);
+
+ this.customerSubscription = this.customersStore.select(fromCustomers.getSelectedCustomer)
+ .subscribe(customer => this.customer = customer);
+
+ this.identificationCardSubscription = this.customersStore.select(fromCustomers.getSelectedIdentificationCard)
+ .subscribe(identificationCard => this.identificationCard = identificationCard);
+ }
+
+ ngOnDestroy(): void {
+ this.customerSubscription.unsubscribe();
+ this.identificationCardSubscription.unsubscribe();
+
+ this.customersStore.dispatch({ type: RESET_FORM });
+ }
+
+ onSave(formData: IdentityCardScanFormData): void {
+ const scan: IdentificationCardScan = {
+ identifier: formData.identifier,
+ description: formData.description
+ };
+
+ this.customersStore.dispatch({
+ type: CREATE,
+ payload: {
+ customerIdentifier: this.customer.identifier,
+ identificationCardNumber: this.identificationCard.number,
+ scan,
+ file: formData.file,
+ activatedRoute: this.route
+ }
+ });
+ }
+
+ onCancel(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+
+}
diff --git a/src/app/customers/detail/identityCard/scans/form/scan.form.component.html b/src/app/customers/detail/identityCard/scans/form/scan.form.component.html
new file mode 100644
index 0000000..e5aaa84
--- /dev/null
+++ b/src/app/customers/detail/identityCard/scans/form/scan.form.component.html
@@ -0,0 +1,59 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Upload new identification card scan' | translate}}"
+ [state]="form.valid ? 'complete' : form.pristine ? 'none' : 'required'">
+ <form [formGroup]="form" layout="column">
+ <fims-id-input [form]="form" controlName="identifier" [readonly]="editMode"></fims-id-input>
+ <fims-text-input [form]="form" controlName="description" placeholder="{{'Description' | translate}}"></fims-text-input>
+ <div layout="row">
+ <mat-form-field tdFileDrop
+ [disabled]="true"
+ flex layout-margin>
+ <input matInput
+ placeholder="{{'Selected file' | translate }}"
+ [value]="form.get('file').value ? form.get('file').value.name : ''"
+ [disabled]="true"
+ readonly/>
+ <mat-hint class="tc-red-500" *ngIf="form.get('file').hasError('maxFileSize')">
+ {{ 'Max file size' | translate:{ value: form.get('file').getError('maxFileSize')['value']} }}
+ </mat-hint>
+ </mat-form-field>
+ <button mat-icon-button *ngIf="form.get('file').value" (click)="fileInput.clear()"
+ (keyup.enter)="fileInput.clear()">
+ <mat-icon>cancel</mat-icon>
+ </button>
+ <td-file-input class="push-left-sm push-right-sm" #fileInput formControlName="file" accept=".jpg,.png">
+ <mat-icon>folder</mat-icon>
+ <span class="text-upper" translate>Browse...</span>
+ </td-file-input>
+ </div>
+ </form>
+ </td-step>
+ <td-step label="{{'Final step' | translate}}" [state]="'complete'">
+ <ng-template td-step-summary>
+ <fims-form-final-action
+ [resourceName]="'IDENTIFICATION CARD SCAN'"
+ [editMode]="editMode"
+ [disabled]="!form.valid"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/customers/detail/identityCard/scans/form/scan.form.component.ts b/src/app/customers/detail/identityCard/scans/form/scan.form.component.ts
new file mode 100644
index 0000000..ddb6b38
--- /dev/null
+++ b/src/app/customers/detail/identityCard/scans/form/scan.form.component.ts
@@ -0,0 +1,76 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
+import {FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {Error} from '../../../../../services/domain/error.model';
+import {TdStepComponent} from '@covalent/core';
+import {FimsValidators} from '../../../../../common/validator/validators';
+
+export interface IdentityCardScanFormData {
+ identifier: string;
+ description: string;
+ file: File;
+}
+
+@Component({
+ selector: 'fims-identity-card-scan-form',
+ templateUrl: './scan.form.component.html'
+})
+export class IdentificationCardScanComponent implements OnInit {
+
+ @ViewChild('detailsStep') detailsStep: TdStepComponent;
+
+ form: FormGroup;
+
+ @Input() editMode: boolean;
+
+ @Input() set error(error: Error) {
+ if (!error) {
+ return;
+ }
+
+ this.form.get('identifier').setErrors({
+ 'unique': true
+ });
+ }
+
+ @Output() onSave = new EventEmitter<IdentityCardScanFormData>();
+
+ @Output() onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder) {}
+
+ ngOnInit() {
+ this.form = this.formBuilder.group({
+ identifier: ['', [Validators.required, Validators.minLength(3), Validators.maxLength(32), FimsValidators.urlSafe]],
+ description: ['', [Validators.required, Validators.maxLength(1024)]],
+ file: ['', [Validators.required, FimsValidators.maxFileSize(512)]]
+ });
+
+ this.detailsStep.open();
+ }
+
+ save(): void {
+ this.onSave.emit(this.form.getRawValue());
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+}
diff --git a/src/app/customers/detail/identityCard/scans/scan.list.component.html b/src/app/customers/detail/identityCard/scans/scan.list.component.html
new file mode 100644
index 0000000..8125d1c
--- /dev/null
+++ b/src/app/customers/detail/identityCard/scans/scan.list.component.html
@@ -0,0 +1,30 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<mat-list>
+ <h3 mat-subheader translate *ngIf="scans && scans.length > 0" translate>Scans uploaded</h3>
+ <mat-list-item *ngFor="let scan of scans">
+ <mat-icon mat-list-icon>photo_camera</mat-icon>
+ <h4 matLine>{{scan.description}}</h4>
+ <button mat-button color="warn" (click)="deleteScan(scan)" *hasPermission="{ id: 'customer_identifications', accessLevel: 'DELETE' }" translate>
+ Delete
+ </button>
+ <button mat-raised-button (click)="viewScan(scan.identifier)" translate>
+ View
+ </button>
+ </mat-list-item>
+</mat-list>
diff --git a/src/app/customers/detail/identityCard/scans/scan.list.component.ts b/src/app/customers/detail/identityCard/scans/scan.list.component.ts
new file mode 100644
index 0000000..aaaaff4
--- /dev/null
+++ b/src/app/customers/detail/identityCard/scans/scan.list.component.ts
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, Output} from '@angular/core';
+import {IdentificationCardScan} from '../../../../services/customer/domain/identification-card-scan.model';
+import {FormGroup} from '@angular/forms';
+
+export interface UploadIdentificationCardScanEvent {
+ scan: IdentificationCardScan;
+ file: File;
+}
+
+@Component({
+ selector: 'fims-identification-card-scan-list',
+ templateUrl: './scan.list.component.html'
+})
+export class CustomerIdentityCardScanListComponent {
+
+ @Input() scans: IdentificationCardScan[];
+
+ @Output() onView: EventEmitter<string> = new EventEmitter();
+
+ @Output() onDelete: EventEmitter<IdentificationCardScan> = new EventEmitter();
+
+ formGroup: FormGroup;
+
+ constructor() {}
+
+ viewScan(identifier: string): void {
+ this.onView.emit(identifier);
+ }
+
+ deleteScan(scan: IdentificationCardScan): void {
+ this.onDelete.emit(scan);
+ }
+
+}
diff --git a/src/app/customers/detail/payroll/form/create.form.component.html b/src/app/customers/detail/payroll/form/create.form.component.html
new file mode 100644
index 0000000..f9e9759
--- /dev/null
+++ b/src/app/customers/detail/payroll/form/create.form.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="Change payroll allocations" [navigateBackTo]="['../']">
+ <fims-customer-payroll-form
+ [productInstances]="productInstances$ | async"
+ [distribution]="distribution$ | async"
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()">
+ </fims-customer-payroll-form>
+</fims-layout-card-over>
diff --git a/src/app/customers/detail/payroll/form/create.form.component.ts b/src/app/customers/detail/payroll/form/create.form.component.ts
new file mode 100644
index 0000000..9e4bbc3
--- /dev/null
+++ b/src/app/customers/detail/payroll/form/create.form.component.ts
@@ -0,0 +1,64 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component} from '@angular/core';
+import {PayrollConfiguration} from '../../../../services/payroll/domain/payroll-configuration.model';
+import {Observable} from 'rxjs/Observable';
+import * as fromCustomers from '../../../store/index';
+import {CustomersStore} from '../../../store/index';
+import {ActivatedRoute, Router} from '@angular/router';
+import {UPDATE} from '../../../store/payroll/payroll.actions';
+import {ProductInstance} from '../../../../services/depositAccount/domain/instance/product-instance.model';
+import {DepositAccountService} from '../../../../services/depositAccount/deposit-account.service';
+
+@Component({
+ templateUrl: './create.form.component.html'
+})
+export class CreateCustomerPayrollFormComponent {
+
+ private customerId: string;
+
+ distribution$: Observable<PayrollConfiguration>;
+
+ productInstances$: Observable<ProductInstance[]>;
+
+ constructor(private store: CustomersStore, private router: Router, private route: ActivatedRoute,
+ private depositService: DepositAccountService) {
+ this.distribution$ = store.select(fromCustomers.getPayrollDistribution);
+
+ this.productInstances$ = this.store.select(fromCustomers.getSelectedCustomer)
+ .do(customer => this.customerId = customer.identifier)
+ .switchMap(customer => this.depositService.fetchProductInstances(customer.identifier))
+ .map(instances => instances.filter(instance => instance.state === 'ACTIVE'));
+ }
+
+ onSave(distribution: PayrollConfiguration): void {
+ this.store.dispatch({
+ type: UPDATE,
+ payload: {
+ customerId: this.customerId,
+ distribution,
+ activatedRoute: this.route
+ }
+ });
+ }
+
+ onCancel(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/customers/detail/payroll/form/form.component.html b/src/app/customers/detail/payroll/form/form.component.html
new file mode 100644
index 0000000..95137b0
--- /dev/null
+++ b/src/app/customers/detail/payroll/form/form.component.html
@@ -0,0 +1,58 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Update payroll allocations' | translate}}" [state]="form.valid ? 'complete' : form.pristine ? 'none' : 'required'">
+ <form [formGroup]="form">
+ <mat-form-field layout-margin>
+ <mat-select formControlName="mainAccountNumber" placeholder="{{ 'Select main account' | translate }}">
+ <mat-option *ngFor="let instance of productInstances" [value]="instance.accountIdentifier">
+ {{instance.accountIdentifier}}({{instance.productIdentifier}})
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ <div layout-gt-xs="column" layout-margin formArrayName="payrollAllocations">
+ <h4 translate>Allocations</h4>
+ <div *ngFor="let allocation of allocations; let i=index" [formGroupName]="i">
+ <mat-form-field layout-margin>
+ <mat-select formControlName="accountNumber" placeholder="{{ 'Select account' | translate }}">
+ <mat-option *ngFor="let instance of productInstances" [value]="instance.accountIdentifier">
+ {{instance.accountIdentifier}}({{instance.productIdentifier}})
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ <fims-number-input placeholder="Amount" [form]="allocation" controlName="amount"></fims-number-input>
+ <mat-checkbox formControlName="proportional" translate>Proportional?</mat-checkbox>
+ <button mat-button (click)="removeAllocation(i)">{{'Remove' | translate}}</button>
+ </div>
+ <p *ngIf="form.hasError('accountUnique')" class="tc-red-600" translate>
+ Allocation accounts can't use main account or overlap with other allocation accounts.
+ </p>
+ <button mat-button (click)="addAllocation()">{{'Add allocation' | translate}}</button>
+ </div>
+ </form>
+ <ng-template td-step-actions>
+ <fims-form-final-action
+ [resourceName]="'ALLOCATIONS'"
+ [editMode]="true"
+ [disabled]="!form.valid"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/customers/detail/payroll/form/form.component.ts b/src/app/customers/detail/payroll/form/form.component.ts
new file mode 100644
index 0000000..653e56e
--- /dev/null
+++ b/src/app/customers/detail/payroll/form/form.component.ts
@@ -0,0 +1,112 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild} from '@angular/core';
+import {PayrollConfiguration} from '../../../../services/payroll/domain/payroll-configuration.model';
+import {AbstractControl, FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {TdStepComponent} from '@covalent/core';
+import {FimsValidators} from '../../../../common/validator/validators';
+import {ProductInstance} from '../../../../services/depositAccount/domain/instance/product-instance.model';
+import {PayrollAllocation} from '../../../../services/payroll/domain/payroll-allocation.model';
+import {accountUnique} from './validator/account-unique.validator';
+
+@Component({
+ selector: 'fims-customer-payroll-form',
+ templateUrl: './form.component.html'
+})
+export class CustomerPayrollFormComponent implements OnInit, OnChanges {
+
+ form: FormGroup;
+
+ @ViewChild('detailsStep') detailsStep: TdStepComponent;
+
+ @Input('productInstances') productInstances: ProductInstance[];
+
+ @Input('distribution') distribution: PayrollConfiguration;
+
+ @Output('onSave') onSave = new EventEmitter<PayrollConfiguration>();
+
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder) {
+ this.form = this.formBuilder.group({
+ mainAccountNumber: ['', [Validators.required]],
+ payrollAllocations: this.initAllocations([])
+ }, { validator: accountUnique });
+ }
+
+ ngOnInit(): void {
+ this.detailsStep.open();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes.distribution) {
+ this.form.reset({
+ mainAccountNumber: this.distribution.mainAccountNumber
+ });
+
+ this.distribution.payrollAllocations.forEach(allocation => this.addAllocation(allocation));
+ }
+ }
+
+ save(): void {
+ const distribution = Object.assign({}, this.distribution, {
+ mainAccountNumber: this.form.get('mainAccountNumber').value,
+ payrollAllocations: this.form.get('payrollAllocations').value
+ });
+
+ this.onSave.emit(distribution);
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+ private initAllocations(allocations: PayrollAllocation[]): FormArray {
+ const formControls: FormGroup[] = [];
+ allocations.forEach(allocation => formControls.push(this.initAllocation(allocation)));
+ return this.formBuilder.array(formControls);
+ }
+
+ private initAllocation(allocation?: PayrollAllocation): FormGroup {
+ return this.formBuilder.group({
+ accountNumber: [allocation ? allocation.accountNumber : '', [Validators.required]],
+ amount: [allocation ? allocation.amount : '', [
+ Validators.required,
+ FimsValidators.minValue(0.001),
+ FimsValidators.maxValue(9999999999.99999)]
+ ],
+ proportional: [allocation ? allocation.proportional : false]
+ });
+ }
+
+ addAllocation(allocation?: PayrollAllocation): void {
+ const allocations: FormArray = this.form.get('payrollAllocations') as FormArray;
+ allocations.push(this.initAllocation(allocation));
+ }
+
+ removeAllocation(index: number): void {
+ const allocations: FormArray = this.form.get('payrollAllocations') as FormArray;
+ allocations.removeAt(index);
+ }
+
+ get allocations(): AbstractControl[] {
+ const allocations: FormArray = this.form.get('payrollAllocations') as FormArray;
+ return allocations.controls;
+ }
+}
diff --git a/src/app/customers/detail/payroll/form/validator/account-unique.validator.spec.ts b/src/app/customers/detail/payroll/form/validator/account-unique.validator.spec.ts
new file mode 100644
index 0000000..427589a
--- /dev/null
+++ b/src/app/customers/detail/payroll/form/validator/account-unique.validator.spec.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {accountUnique} from './account-unique.validator';
+import {FormArray, FormControl, FormGroup} from '@angular/forms';
+
+describe('accountUnique validator', () => {
+
+ function setup(mainAccountNumber: string, accountNumbers: string[]): FormGroup {
+ const payrollAllocations = new FormArray([]);
+ accountNumbers.forEach(number => payrollAllocations.push(
+ new FormGroup({
+ accountNumber: new FormControl(number),
+ amount: new FormControl(),
+ proportional: new FormControl()
+ })));
+
+ const group = new FormGroup({
+ mainAccountNumber: new FormControl(mainAccountNumber),
+ payrollAllocations
+ });
+
+ return group;
+ }
+
+ it('should not return error when no account overlap', () => {
+ const group = setup('testa', ['testb', 'testc']);
+
+ const result = accountUnique(group);
+
+ expect(result).toBeNull();
+ });
+
+ it('should return error when allocation account overlap with main account', () => {
+ const group = setup('testa', ['testa', 'testc']);
+
+ const result = accountUnique(group);
+
+ expect(result).toEqual({
+ accountUnique: true
+ });
+ });
+
+ it('should return error when allocation account overlap with other allocation account', () => {
+ const group = setup('testa', ['testb', 'testb']);
+
+ const result = accountUnique(group);
+
+ expect(result).toEqual({
+ accountUnique: true
+ });
+ });
+});
diff --git a/src/app/customers/detail/payroll/form/validator/account-unique.validator.ts b/src/app/customers/detail/payroll/form/validator/account-unique.validator.ts
new file mode 100644
index 0000000..9385ec3
--- /dev/null
+++ b/src/app/customers/detail/payroll/form/validator/account-unique.validator.ts
@@ -0,0 +1,46 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {FormGroup, ValidationErrors} from '@angular/forms';
+import {PayrollAllocation} from '../../../../../services/payroll/domain/payroll-allocation.model';
+import {isEmptyInputValue} from '../../../../../common/validator/validators';
+
+export function accountUnique(group: FormGroup): ValidationErrors | null {
+ const mainAccountNumber: string = group.controls.mainAccountNumber.value;
+ const payrollAllocations: PayrollAllocation[] = group.controls.payrollAllocations.value;
+
+ if (isEmptyInputValue(mainAccountNumber)) {
+ return;
+ }
+
+ const numbers = payrollAllocations
+ .filter(allocation => allocation.accountNumber.length > 0)
+ .map(allocation => allocation.accountNumber);
+
+ const set = new Set();
+
+ numbers.forEach(number => set.add(number));
+
+ if (numbers.indexOf(mainAccountNumber) > -1 || set.size !== numbers.length) {
+ return {
+ accountUnique: true
+ };
+ }
+
+ return null;
+}
diff --git a/src/app/customers/detail/payroll/payroll-exists.guard.ts b/src/app/customers/detail/payroll/payroll-exists.guard.ts
new file mode 100644
index 0000000..f127dc2
--- /dev/null
+++ b/src/app/customers/detail/payroll/payroll-exists.guard.ts
@@ -0,0 +1,69 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import {Injectable} from '@angular/core';
+import * as fromCustomers from '../../store/index';
+import {CustomersStore} from '../../store/index';
+import {Observable} from 'rxjs/Observable';
+import {of} from 'rxjs/observable/of';
+import {ExistsGuardService} from '../../../common/guards/exists-guard';
+import {LoadAction} from '../../store/payroll/payroll.actions';
+import {PayrollService} from '../../../services/payroll/payroll.service';
+
+@Injectable()
+export class PayrollExistsGuard implements CanActivate {
+
+ constructor(private store: CustomersStore,
+ private payrollService: PayrollService,
+ private existsGuardService: ExistsGuardService) {}
+
+ hasPayrollInStore(): Observable<boolean> {
+ const timestamp$ = this.store.select(fromCustomers.getPayrollDistributionLoadedAt);
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ loadPayrollFromApi(id: string): Observable<boolean> {
+ const getPayroll$ = this.payrollService.findPayrollConfiguration(id, true)
+ .catch(() => {
+ return Observable.of({
+ mainAccountNumber: '',
+ payrollAllocations: []
+ });
+ })
+ .map(distribution => new LoadAction(distribution))
+ .do((action: LoadAction) => this.store.dispatch(action))
+ .map(distribution => !!distribution);
+
+ return this.existsGuardService.routeTo404OnError(getPayroll$);
+ }
+
+ hasPayroll(id: string): Observable<boolean> {
+ return this.hasPayrollInStore()
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+ return this.loadPayrollFromApi(id);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasPayroll(route.parent.params['id']);
+ }
+}
diff --git a/src/app/customers/detail/payroll/payroll.detail.component.html b/src/app/customers/detail/payroll/payroll.detail.component.html
new file mode 100644
index 0000000..bb3ba37
--- /dev/null
+++ b/src/app/customers/detail/payroll/payroll.detail.component.html
@@ -0,0 +1,42 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Payroll allocations' | translate }}" [navigateBackTo]="['../../../../']">
+ <div class="mat-content inset" flex *ngIf="(distribution$ | async) as distribution">
+ <div layout="row">
+ <mat-list>
+ <mat-list-item>
+ <h3 matLine translate>Account number</h3>
+ <p matLine>{{distribution.mainAccountNumber}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Created by</h3>
+ <p matLine>{{distribution.createdBy}} - {{distribution.createdOn | date:'medium'}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Last modified by</h3>
+ <p matLine>{{distribution.lastModifiedBy}} - {{distribution.lastModifiedOn | date:'medium'}}</p>
+ </mat-list-item>
+ </mat-list>
+ </div>
+ <fims-data-table flex [columns]="columns" [data]="allocationData" [actionColumn]="false">
+ </fims-data-table>
+ </div>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Edit payroll distribution' | translate}}" icon="mode_edit" [link]="['edit']"
+ [permission]="{ id: 'customer_customers', accessLevel: 'CHANGE'}">
+</fims-fab-button>
diff --git a/src/app/customers/detail/payroll/payroll.detail.component.ts b/src/app/customers/detail/payroll/payroll.detail.component.ts
new file mode 100644
index 0000000..c2e1c91
--- /dev/null
+++ b/src/app/customers/detail/payroll/payroll.detail.component.ts
@@ -0,0 +1,50 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component} from '@angular/core';
+import {PayrollConfiguration} from '../../../services/payroll/domain/payroll-configuration.model';
+import {Observable} from 'rxjs/Observable';
+import * as fromCustomers from '../../store/index';
+import {CustomersStore} from '../../store/index';
+import {TableData} from '../../../common/data-table/data-table.component';
+
+@Component({
+ templateUrl: './payroll.detail.component.html'
+})
+export class CustomerPayrollDetailComponent {
+
+ distribution$: Observable<PayrollConfiguration>;
+
+ allocationData: TableData;
+
+ columns: any[] = [
+ { name: 'accountNumber', label: 'Account Number' },
+ { name: 'amount', label: 'Amount' },
+ { name: 'proportional', label: 'Proportional?' }
+ ];
+
+ constructor(private store: CustomersStore) {
+ this.distribution$ = store.select(fromCustomers.getPayrollDistribution)
+ .filter(distribution => !!distribution)
+ .do(distribution => this.allocationData = {
+ data: distribution.payrollAllocations,
+ totalElements: distribution.payrollAllocations.length,
+ totalPages: 1
+ });
+ }
+}
diff --git a/src/app/customers/detail/portrait/portrait.component.html b/src/app/customers/detail/portrait/portrait.component.html
new file mode 100644
index 0000000..bacdab5
--- /dev/null
+++ b/src/app/customers/detail/portrait/portrait.component.html
@@ -0,0 +1,49 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Upload portrait' | translate}}" [navigateBackTo]="['../']">
+ <mat-card-content>
+ <div layout="column" layout-align="center center">
+ <fims-portrait [blob]="portrait" [size]="'large'"></fims-portrait>
+ </div>
+ <div layout="column" layout-align="center center" *hasPermission="{ id: 'customer_portrait', accessLevel: 'CHANGE' }">
+ <span translate>Selected file:</span> {{ fileSelectMsg | translate }}
+ <span *ngIf="fileSelectMsg" translate>
+ Press the upload button below to upload the portrait.
+ </span>
+ <div layout="row" layout-margin>
+ <td-file-upload #singleFileUpload (select)="selectEvent($event)" (upload)="uploadEvent($event)"
+ accept=".jpg,.png">
+ <mat-icon>file_upload</mat-icon>
+ <span translate>Click here to upload</span><span>{{ singleFileUpload.files?.name }}</span>
+ <ng-template td-file-input-label>
+ <mat-icon>attach_file</mat-icon>
+ <span translate>Choose a file to upload(max size 512 KB)</span>
+ </ng-template>
+ </td-file-upload>
+ </div>
+ <span layout-margin *ngIf="invalidSize" class="tc-red-500" translate>
+ Portrait can't exceed size of 512KB.
+ </span>
+ <div layout="row" layout-margin *hasPermission="{ id: 'customer_portrait', accessLevel: 'DELETE' }">
+ <button mat-raised-button color="warn" (click)="deletePortrait()">{{'DELETE PORTRAIT' | translate}}
+ </button>
+ </div>
+
+ </div>
+ </mat-card-content>
+</fims-layout-card-over>
diff --git a/src/app/customers/detail/portrait/portrait.component.ts b/src/app/customers/detail/portrait/portrait.component.ts
new file mode 100644
index 0000000..d54e245
--- /dev/null
+++ b/src/app/customers/detail/portrait/portrait.component.ts
@@ -0,0 +1,112 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {CustomerService} from '../../../services/customer/customer.service';
+import {Subscription} from 'rxjs/Subscription';
+import {Customer} from '../../../services/customer/domain/customer.model';
+import {CustomersStore} from '../../store/index';
+import * as fromCustomers from '../../store';
+import {ActivatedRoute, Router} from '@angular/router';
+import {NotificationService, NotificationType} from '../../../services/notification/notification.service';
+import {TdDialogService} from '@covalent/core';
+import {Observable} from 'rxjs/Observable';
+import {TranslateService} from '@ngx-translate/core';
+
+@Component({
+ templateUrl: './portrait.component.html'
+})
+export class CustomerPortraitComponent implements OnInit, OnDestroy {
+
+ private customerSubscription: Subscription;
+
+ private customer: Customer;
+
+ fileSelectMsg = 'No file selected yet.';
+
+ invalidSize = false;
+
+ portrait: Blob;
+
+ constructor(private router: Router, private route: ActivatedRoute, private customerService: CustomerService,
+ private store: CustomersStore, private notificationService: NotificationService,
+ private dialogService: TdDialogService, private translate: TranslateService) {
+ }
+
+ ngOnInit(): void {
+ this.customerSubscription = this.store.select(fromCustomers.getSelectedCustomer)
+ .do(customer => this.customer = customer)
+ .flatMap(customer => this.customerService.getPortrait(customer.identifier))
+ .subscribe(portrait => this.portrait = portrait);
+ }
+
+ ngOnDestroy(): void {
+ this.customerSubscription.unsubscribe();
+ }
+
+ selectEvent(file: File): void {
+ this.invalidSize = file.size > 524288;
+ this.fileSelectMsg = file.name;
+ };
+
+ uploadEvent(file: File): void {
+ if (this.invalidSize) {
+ return;
+ }
+
+ this.customerService.uploadPortrait(this.customer.identifier, file).subscribe(() => {
+ this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Portrait is going to be uploaded'
+ });
+ this.navigateAway();
+ });
+ };
+
+ confirmDeletion(): Observable<boolean> {
+ const message = 'Do you want to delete the portrait?';
+ const title = 'Confirm deletion';
+ const button = 'DELETE PORTRAIT';
+
+ return this.translate.get([title, message, button])
+ .flatMap(result =>
+ this.dialogService.openConfirm({
+ message: result[message],
+ title: result[title],
+ acceptButton: result[button]
+ }).afterClosed()
+ );
+ }
+
+ deletePortrait(): void {
+ this.confirmDeletion()
+ .filter(accept => accept)
+ .flatMap(() => this.customerService.deletePortrait(this.customer.identifier))
+ .subscribe(() => {
+ this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Portrait is going to be deleted'
+ });
+ this.navigateAway();
+ });
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], {relativeTo: this.route});
+ }
+}
diff --git a/src/app/customers/detail/status/customer-task.component.html b/src/app/customers/detail/status/customer-task.component.html
new file mode 100644
index 0000000..4db5b15
--- /dev/null
+++ b/src/app/customers/detail/status/customer-task.component.html
@@ -0,0 +1,32 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<mat-list>
+ <mat-list-item>
+ <mat-icon matListAvatar>playlist_add_check</mat-icon>
+ <h3 matLine>{{task.name}}</h3>
+ <h4 matLine>{{task.description}}</h4>
+ <p matLine>
+ <span translate>Mandatory:</span>{{task.mandatory}}
+ </p>
+ <p matLine>
+ <span translate>Type:</span>{{formatType(task.type)}}
+ </p>
+ <mat-checkbox labelPosition="after" title="{{'Execute task' | translate}}"
+ (click)="selectTask($event)"></mat-checkbox>
+ </mat-list-item>
+</mat-list>
diff --git a/src/app/customers/detail/status/customer-task.component.ts b/src/app/customers/detail/status/customer-task.component.ts
new file mode 100644
index 0000000..ef123e3
--- /dev/null
+++ b/src/app/customers/detail/status/customer-task.component.ts
@@ -0,0 +1,53 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, Output} from '@angular/core';
+import {TaskDefinition, TaskDefinitionType} from '../../../services/customer/domain/task-definition.model';
+import {MatCheckboxChange} from '@angular/material';
+import {defaultTypeOptions} from '../../tasks/domain/type-options.model';
+
+export interface SelectTaskEvent {
+ taskIdentifier: string;
+ checked: boolean;
+}
+
+@Component({
+ selector: 'fims-customer-task',
+ templateUrl: './customer-task.component.html'
+})
+export class CustomerTaskComponent {
+
+ @Input() task: TaskDefinition;
+
+ @Input() disabled: boolean;
+
+ @Output() onSelectTask = new EventEmitter<SelectTaskEvent>();
+
+ constructor() {}
+
+ selectTask(change: MatCheckboxChange): void {
+ this.onSelectTask.emit({
+ taskIdentifier: this.task.identifier,
+ checked: change.checked
+ });
+ }
+
+ formatType(type: TaskDefinitionType): string {
+ return defaultTypeOptions.find(option => option.type === type).label;
+ }
+}
diff --git a/src/app/customers/detail/status/status.component.html b/src/app/customers/detail/status/status.component.html
new file mode 100644
index 0000000..0b3a013
--- /dev/null
+++ b/src/app/customers/detail/status/status.component.html
@@ -0,0 +1,42 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Tasks' | translate}}" [navigateBackTo]="['../']">
+ <div layout="row" layout-align="start start" layout-margin>
+ <div layout="column" flex="100">
+ <div layout="row" layout-align="start start" *ngFor="let step of processSteps$ | async">
+ <div layout="column" flex="100">
+ <h5 translate [translateParams]="{ value: step.command.action }">Please verify the following tasks before you
+ can action this member </h5>
+ <fims-customer-task *ngFor="let task of step.taskDefinitions"
+ [task]="task"
+ (onSelectTask)="executeTask($event)">
+ </fims-customer-task>
+ <div layout="row" layout-align="start center">
+ <mat-form-field layout-margin flex>
+ <textarea matInput placeholder="{{'Enter your comment here...' | translate}}"
+ [(ngModel)]="step.command.comment"></textarea>
+ </mat-form-field>
+ <button mat-raised-button style="margin-left: 10px;" color="accent" (click)="executeCommand(step.command)">
+ {{step.command.action}}
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</fims-layout-card-over>
diff --git a/src/app/customers/detail/status/status.component.ts b/src/app/customers/detail/status/status.component.ts
new file mode 100644
index 0000000..20388f9
--- /dev/null
+++ b/src/app/customers/detail/status/status.component.ts
@@ -0,0 +1,74 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {Customer} from '../../../services/customer/domain/customer.model';
+import {Command} from '../../../services/customer/domain/command.model';
+import {ActivatedRoute} from '@angular/router';
+import * as fromCustomers from '../../store';
+import {Subscription} from 'rxjs/Subscription';
+import {EXECUTE_COMMAND, EXECUTE_TASK, LOAD_ALL} from '../../store/customerTasks/customer-task.actions';
+import {CustomersStore} from '../../store/index';
+import {Observable} from 'rxjs/Observable';
+import {ProcessStep} from '../../../services/customer/domain/process-step.model';
+import {SelectTaskEvent} from './customer-task.component';
+
+
+@Component({
+ templateUrl: './status.component.html'
+})
+export class CustomerStatusComponent implements OnInit, OnDestroy {
+
+ private customerSubscription: Subscription;
+
+ customer: Customer;
+
+ processSteps$: Observable<ProcessStep[]>;
+
+ constructor(private route: ActivatedRoute, private store: CustomersStore) {}
+
+ ngOnInit(): void {
+ this.processSteps$ = this.store.select(fromCustomers.getCustomerTaskProcessSteps);
+
+ this.customerSubscription = this.store.select(fromCustomers.getSelectedCustomer)
+ .do(customer => this.store.dispatch({ type: LOAD_ALL, payload: customer.identifier }))
+ .subscribe(customer => this.customer = customer);
+ }
+
+ ngOnDestroy(): void {
+ this.customerSubscription.unsubscribe();
+ }
+
+ executeTask(event: SelectTaskEvent): void {
+ this.store.dispatch({ type: EXECUTE_TASK, payload: {
+ customerId: this.customer.identifier,
+ taskId: event.taskIdentifier
+ } });
+ }
+
+ executeCommand(command: Command): void {
+ this.store.dispatch({ type: EXECUTE_COMMAND, payload: {
+ customerId: this.customer.identifier,
+ command,
+ activatedRoute: this.route
+ } });
+ }
+
+
+
+}
diff --git a/src/app/customers/form/contact/contact.component.html b/src/app/customers/form/contact/contact.component.html
new file mode 100644
index 0000000..bbfc699
--- /dev/null
+++ b/src/app/customers/form/contact/contact.component.html
@@ -0,0 +1,22 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<form [formGroup]="form" layout="column">
+ <fims-text-input [form]="form" controlName="email" placeholder="{{'Email(optional)' | translate}}" type="email"></fims-text-input>
+ <fims-text-input [form]="form" controlName="phone" placeholder="{{'Phone(optional)' | translate}}" type="tel"></fims-text-input>
+ <fims-text-input [form]="form" controlName="mobile" placeholder="{{'Mobile(optional)' | translate}}" type="tel"></fims-text-input>
+</form>
diff --git a/src/app/customers/form/contact/contact.component.spec.ts b/src/app/customers/form/contact/contact.component.spec.ts
new file mode 100644
index 0000000..2fef748
--- /dev/null
+++ b/src/app/customers/form/contact/contact.component.spec.ts
@@ -0,0 +1,88 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, ViewChild} from '@angular/core';
+import {ContactDetail} from '../../../services/domain/contact/contact-detail.model';
+import {CustomerContactFormComponent} from './contact.component';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {ReactiveFormsModule} from '@angular/forms';
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {setValueByCssSelector} from '../../../common/testing/input-fields';
+import {TranslateModule} from '@ngx-translate/core';
+import {FimsSharedModule} from '../../../common/common.module';
+import {MatInputModule} from '@angular/material';
+
+const contactDetails: ContactDetail[] = [
+ { group: 'BUSINESS', type: 'EMAIL', value: 'test@test.de', preferenceLevel: 1 },
+ { group: 'BUSINESS', type: 'PHONE', value: '1234', preferenceLevel: 1 },
+ { group: 'BUSINESS', type: 'MOBILE', value: '5678', preferenceLevel: 1 }
+];
+
+describe('Test contact form', () => {
+
+ let fixture: ComponentFixture<TestComponent>;
+
+ let testComponent: TestComponent;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [
+ TestComponent,
+ CustomerContactFormComponent
+ ],
+ imports: [
+ TranslateModule.forRoot(),
+ FimsSharedModule,
+ ReactiveFormsModule,
+ MatInputModule,
+ NoopAnimationsModule
+ ]
+ });
+
+ fixture = TestBed.createComponent(TestComponent);
+ testComponent = fixture.componentInstance;
+ });
+
+ it('component should collect only contact fields with values', () => {
+ fixture.detectChanges();
+
+ setValueByCssSelector(fixture, '#email', '');
+ setValueByCssSelector(fixture, '#phone', '5678');
+
+ fixture.detectChanges();
+
+ const expectedResult: ContactDetail[] = [
+ { group: 'BUSINESS', type: 'MOBILE', value: '5678', preferenceLevel: 1 },
+ { group: 'BUSINESS', type: 'PHONE', value: '5678', preferenceLevel: 1 }
+ ];
+
+ expect(testComponent.formComponent.formData).toEqual(expectedResult);
+ });
+
+});
+
+@Component({
+ template: '<fims-customer-contact-form #form [formData]="formData"></fims-customer-contact-form>'
+})
+class TestComponent {
+
+ @ViewChild('form') formComponent: CustomerContactFormComponent;
+
+ formData: ContactDetail[] = contactDetails;
+
+}
diff --git a/src/app/customers/form/contact/contact.component.ts b/src/app/customers/form/contact/contact.component.ts
new file mode 100644
index 0000000..8ce2f17
--- /dev/null
+++ b/src/app/customers/form/contact/contact.component.ts
@@ -0,0 +1,81 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input} from '@angular/core';
+import {FormComponent} from '../../../common/forms/form.component';
+import {AbstractControl, FormBuilder, Validators} from '@angular/forms';
+import {BUSINESS, ContactDetail, ContactDetailType, EMAIL, MOBILE, PHONE} from '../../../services/domain/contact/contact-detail.model';
+import {getContactDetailValueByType} from '../../contact.helper';
+import {FimsValidators} from '../../../common/validator/validators';
+
+@Component({
+ selector: 'fims-customer-contact-form',
+ templateUrl: './contact.component.html'
+})
+export class CustomerContactFormComponent extends FormComponent<ContactDetail[]> {
+
+ @Input() set formData(contactDetails: ContactDetail[]) {
+ if (!contactDetails) {
+ throw new Error('contact details must be defined');
+ }
+
+ let phone = '';
+ let mobile = '';
+ let email = '';
+
+ const businessContacts: ContactDetail[] = contactDetails.filter(contactDetail => contactDetail.group === BUSINESS);
+
+ if (businessContacts.length) {
+ phone = getContactDetailValueByType(businessContacts, PHONE);
+ mobile = getContactDetailValueByType(businessContacts, MOBILE);
+ email = getContactDetailValueByType(businessContacts, EMAIL);
+ }
+
+ this.form = this.formBuilder.group({
+ email: [email, [Validators.maxLength(32), FimsValidators.email]],
+ phone: [phone, Validators.maxLength(32)],
+ mobile: [mobile, Validators.maxLength(32)]
+ });
+ };
+
+ constructor(private formBuilder: FormBuilder) {
+ super();
+ }
+
+ get formData(): ContactDetail[] {
+ const contactDetails: ContactDetail[] = [];
+
+ this.pushIfValue(contactDetails, this.form.get('email'), 'EMAIL');
+ this.pushIfValue(contactDetails, this.form.get('mobile'), 'MOBILE');
+ this.pushIfValue(contactDetails, this.form.get('phone'), 'PHONE');
+
+ return contactDetails;
+ }
+
+ private pushIfValue(contactDetails: ContactDetail[], control: AbstractControl, type: ContactDetailType): void {
+ if (control.value && control.value.length > 0) {
+ contactDetails.push({
+ group: 'BUSINESS',
+ type: type,
+ value: control.value,
+ preferenceLevel: 1
+ });
+ }
+ }
+
+}
diff --git a/src/app/customers/form/create/create.form.component.html b/src/app/customers/form/create/create.form.component.html
new file mode 100644
index 0000000..1aadbca
--- /dev/null
+++ b/src/app/customers/form/create/create.form.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Create new member ' | translate}}">
+ <fims-customer-form-component #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [catalog]="catalog$ | async"
+ [customer]="customer">
+ </fims-customer-form-component>
+</fims-layout-card-over>
diff --git a/src/app/customers/form/create/create.form.component.ts b/src/app/customers/form/create/create.form.component.ts
new file mode 100644
index 0000000..721c179
--- /dev/null
+++ b/src/app/customers/form/create/create.form.component.ts
@@ -0,0 +1,89 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Customer} from '../../../services/customer/domain/customer.model';
+import {CustomerFormComponent} from '../form.component';
+import * as fromCustomers from '../../store';
+import {Error} from '../../../services/domain/error.model';
+import {Subscription} from 'rxjs/Subscription';
+import {CustomersStore} from '../../store/index';
+import {CREATE, RESET_FORM} from '../../store/customer.actions';
+import {Catalog} from '../../../services/catalog/domain/catalog.model';
+import {Observable} from 'rxjs/Observable';
+
+@Component({
+ templateUrl: './create.form.component.html'
+})
+export class CreateCustomerFormComponent implements OnInit, OnDestroy {
+
+ private formStateSubscription: Subscription;
+
+ @ViewChild('form') formComponent: CustomerFormComponent;
+
+ customer: Customer = {
+ identifier: '',
+ type: 'PERSON',
+ givenName: '',
+ surname: '',
+ address: {
+ street: '',
+ city: '',
+ countryCode: '',
+ country: ''
+ },
+ member: true,
+ dateOfBirth: undefined,
+ contactDetails: [],
+ customValues: []
+ };
+
+ catalog$: Observable<Catalog>;
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: CustomersStore) {
+ this.catalog$ = store.select(fromCustomers.getCustomerCatalog);
+ }
+
+ ngOnInit() {
+ this.formStateSubscription = this.store.select(fromCustomers.getCustomerFormError)
+ .filter((error: Error) => !!error)
+ .subscribe((error: Error) => this.formComponent.showIdentifierValidationError());
+ }
+
+ ngOnDestroy(): void {
+ this.formStateSubscription.unsubscribe();
+
+ this.store.dispatch({ type: RESET_FORM });
+ }
+
+ onSave(customer: Customer) {
+ this.store.dispatch({ type: CREATE, payload: {
+ customer,
+ activatedRoute: this.route
+ } });
+ }
+
+ onCancel() {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/customers/form/customFields/custom-fields.component.html b/src/app/customers/form/customFields/custom-fields.component.html
new file mode 100644
index 0000000..4bca7fa
--- /dev/null
+++ b/src/app/customers/form/customFields/custom-fields.component.html
@@ -0,0 +1,51 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<form [formGroup]="form">
+ <h4>{{catalog?.name}}</h4>
+ <div *ngFor="let field of catalog?.fields" [ngSwitch]="field.dataType" layout="row">
+ <fims-text-input *ngSwitchCase="'TEXT'" [form]="form" [controlName]="field.identifier" [placeholder]="field.label" [title]="field.hint"></fims-text-input>
+ <fims-text-input *ngSwitchCase="'NUMBER'" type="number" [form]="form" [controlName]="field.identifier" [placeholder]="field.label" [title]="field.hint"></fims-text-input>
+ <fims-date-input *ngSwitchCase="'DATE'" [form]="form" [controlName]="field.identifier" [placeholder]="field.label" [title]="field.hint"></fims-date-input>
+ <div class="pad-bottom-xs" *ngSwitchCase="'SINGLE_SELECTION'">
+ <div class="mat-body-1">{{field.label}}</div>
+ <mat-radio-group [formControlName]="field.identifier">
+ <mat-radio-button [value]="" layout-margin *ngIf="!field.mandatory" translate>
+ None
+ </mat-radio-button>
+ <mat-radio-button *ngFor="let option of field.options" [value]="option.value" title="{{field.hint}}" layout-margin>
+ {{option.label}}
+ </mat-radio-button>
+ </mat-radio-group>
+ </div>
+ <div class="pad-bottom-xs" *ngSwitchCase="'MULTI_SELECTION'">
+ <div class="mat-body-1">{{field.label}}</div>
+ <td-chips [items]="field.options"
+ [formControlName]="field.identifier"
+ placeholder="{{field.hint}}">
+ <ng-template td-chip let-chip="chip">
+ <div class="tc-grey-100 bgc-teal-700" td-chip-avatar>
+ {{chip.label.substring(0, 1).toUpperCase()}}
+ </div> {{chip.label}}
+ </ng-template>
+ <ng-template td-autocomplete-option let-option="option">
+ {{option.label}}
+ </ng-template>
+ </td-chips>
+ </div>
+ </div>
+</form>
diff --git a/src/app/customers/form/customFields/custom-fields.component.ts b/src/app/customers/form/customFields/custom-fields.component.ts
new file mode 100644
index 0000000..8318c08
--- /dev/null
+++ b/src/app/customers/form/customFields/custom-fields.component.ts
@@ -0,0 +1,200 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input, OnChanges, SimpleChanges} from '@angular/core';
+import {Catalog} from '../../../services/catalog/domain/catalog.model';
+import {FormBuilder, FormControl, FormGroup, ValidatorFn, Validators} from '@angular/forms';
+import {Field} from '../../../services/catalog/domain/field.model';
+import {Value} from '../../../services/catalog/domain/value.model';
+import {FormComponent} from '../../../common/forms/form.component';
+import {FimsValidators} from '../../../common/validator/validators';
+import {addCurrentTime} from '../../../services/domain/date.converter';
+import {Option} from '../../../services/catalog/domain/option.model';
+
+@Component({
+ selector: 'fims-custom-fields-component',
+ templateUrl: './custom-fields.component.html'
+})
+export class CustomerCustomFieldsComponent extends FormComponent<Value[]> implements OnChanges {
+
+ private _formData: Value[];
+
+ @Input('catalog') catalog: Catalog;
+
+ @Input('formData') set formData(formData: Value[]) {
+ this._formData = formData;
+ };
+
+ constructor(private formBuilder: FormBuilder) {
+ super();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes.catalog || changes.formData) {
+ this.form = this.buildFormGroup();
+ }
+ }
+
+ private findValue(fieldIdentifier: string): Value {
+ return this._formData.find((value: Value) =>
+ value.fieldIdentifier === fieldIdentifier
+ );
+ }
+
+ private findField(fieldIdentifier: string): Field {
+ return this.catalog.fields.find((field: Field) => field.identifier === fieldIdentifier);
+ }
+
+ private buildFormGroup(): FormGroup {
+ const group: FormGroup = this.formBuilder.group({});
+
+ if (!this._formData || !this.catalog) {
+ return group;
+ }
+
+ for (const field of this.catalog.fields) {
+ const value = this.findValue(field.identifier);
+
+ const valueString: string = value && value.value ? value.value : '';
+
+ const formControl: FormControl = new FormControl({value: valueString, disabled: false});
+
+ const validators: ValidatorFn[] = [];
+
+ switch (field.dataType) {
+ case 'TEXT': {
+ validators.push(...this.buildTextValidators(field));
+ break;
+ }
+
+ case 'NUMBER': {
+ formControl.setValue(valueString.length ? Number(valueString) : undefined);
+ validators.push(...this.buildNumberValidators(field));
+ break;
+ }
+
+ case 'DATE': {
+ formControl.setValue(valueString.length ? valueString.substring(0, 10) : '');
+ break;
+ }
+
+ case 'SINGLE_SELECTION': {
+ formControl.setValue(valueString.length ? Number(valueString) : undefined);
+ break;
+ }
+
+ case 'MULTI_SELECTION': {
+ const optionValues = valueString.length ? valueString.split(',').map(optionValue => Number(optionValue)) : [];
+ const foundOptions = field.options
+ .filter((option: Option) => optionValues.indexOf(option.value) > -1);
+ formControl.setValue(foundOptions);
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ if (field.mandatory) {
+ validators.push(Validators.required);
+ }
+
+ formControl.setValidators(validators);
+
+ group.addControl(field.identifier, formControl);
+ }
+
+ return group;
+ }
+
+ get formData(): Value[] {
+ const fields: any = this.form.getRawValue();
+
+ const values: Value[] = [];
+
+ for (const fieldIdentifier in fields) {
+ if (fields.hasOwnProperty(fieldIdentifier)) {
+ let value = fields[fieldIdentifier];
+
+ const field: Field = this.findField(fieldIdentifier);
+
+ if (value == null || value.length === 0) {
+ continue;
+ }
+
+ switch (field.dataType) {
+ case 'NUMBER': {
+ value = value.toString();
+ break;
+ }
+
+ case 'DATE': {
+ const date = new Date(value);
+ value = addCurrentTime(date).toISOString();
+ break;
+ }
+
+ case 'SINGLE_SELECTION': {
+ value = value.toString();
+ break;
+ }
+
+ case 'MULTI_SELECTION': {
+ value = value.map(fieldValue => fieldValue.value).join(',');
+ break;
+ }
+ }
+
+ values.push({
+ catalogIdentifier: this.catalog.identifier,
+ fieldIdentifier,
+ value
+ });
+ }
+ }
+ return values;
+ }
+
+ private buildTextValidators(field: Field): ValidatorFn[] {
+ const validators: ValidatorFn[] = [];
+
+ if (field.length != null) {
+ validators.push(Validators.maxLength(field.length));
+ }
+
+ return validators;
+ }
+
+ private buildNumberValidators(field: Field): ValidatorFn[] {
+ const validators: ValidatorFn[] = [];
+
+ if (field.minValue != null) {
+ validators.push(FimsValidators.minValue(field.minValue));
+ }
+
+ if (field.maxValue != null) {
+ validators.push(FimsValidators.maxValue(field.maxValue));
+ }
+
+ if (field.precision != null) {
+ validators.push(FimsValidators.maxScale(field.precision));
+ }
+
+ return validators;
+ }
+}
diff --git a/src/app/customers/form/detail/detail.component.html b/src/app/customers/form/detail/detail.component.html
new file mode 100644
index 0000000..e5a57e3
--- /dev/null
+++ b/src/app/customers/form/detail/detail.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<form [formGroup]="form" layout="column">
+ <fims-id-input [form]="form" placeholder="Account" controlName="identifier" [readonly]="editMode"></fims-id-input>
+ <fims-text-input [form]="form" controlName="firstName" placeholder="{{'First name' | translate}}"></fims-text-input>
+ <fims-text-input [form]="form" controlName="middleName" placeholder="{{'Middle name(optional)' | translate}}"></fims-text-input>
+ <fims-text-input [form]="form" controlName="lastName" placeholder="{{'Last name' | translate}}"></fims-text-input>
+ <fims-date-input [form]="form" controlName="dayOfBirth" placeholder="{{'Day of birth' | translate}}"></fims-date-input>
+ <mat-checkbox formControlName="member" layout-margin translate>Is member?</mat-checkbox>
+</form>
diff --git a/src/app/customers/form/detail/detail.component.ts b/src/app/customers/form/detail/detail.component.ts
new file mode 100644
index 0000000..cfe7323
--- /dev/null
+++ b/src/app/customers/form/detail/detail.component.ts
@@ -0,0 +1,90 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input} from '@angular/core';
+import {FormComponent} from '../../../common/forms/form.component';
+import {FormBuilder, Validators} from '@angular/forms';
+import {FimsValidators} from '../../../common/validator/validators';
+
+export interface CustomerDetailFormData {
+ identifier: string;
+ firstName: string;
+ middleName: string;
+ lastName: string;
+ dateOfBirth: {
+ day?: number;
+ month?: number;
+ year?: number;
+ };
+ member: boolean;
+}
+
+@Component({
+ selector: 'fims-customer-detail-form',
+ templateUrl: './detail.component.html'
+})
+export class CustomerDetailFormComponent extends FormComponent<CustomerDetailFormData> {
+
+ @Input() set formData(formData: CustomerDetailFormData) {
+ const dateOfBirth = formData.dateOfBirth;
+
+ this.form = this.formBuilder.group({
+ identifier: [formData.identifier, [Validators.required, Validators.minLength(3), Validators.maxLength(32), FimsValidators.urlSafe]],
+ firstName: [formData.firstName, [Validators.required, Validators.maxLength(256)]],
+ middleName: [formData.middleName, Validators.maxLength(256)],
+ lastName: [formData.lastName, [Validators.required, Validators.maxLength(256)]],
+ dayOfBirth: [dateOfBirth ? this.formatDate(dateOfBirth.year, dateOfBirth.month, dateOfBirth.day) : undefined,
+ [Validators.required, FimsValidators.beforeToday]],
+ member: [formData.member],
+ });
+ };
+
+ @Input() editMode: boolean;
+
+ private formatDate(year: number, month: number, day: number): string {
+ return `${year}-${this.addZero(month)}-${this.addZero(day)}`;
+ }
+
+ private addZero(value: number): string {
+ return ('0' + value).slice(-2);
+ }
+
+ constructor(private formBuilder: FormBuilder) {
+ super();
+ }
+
+ get formData(): CustomerDetailFormData{
+ const birthDate: string = this.form.get('dayOfBirth').value;
+
+ const chunks: string[] = birthDate ? birthDate.split('-') : [];
+
+ return {
+ identifier: this.form.get('identifier').value,
+ firstName: this.form.get('firstName').value,
+ middleName: this.form.get('middleName').value,
+ lastName: this.form.get('lastName').value,
+ dateOfBirth: {
+ year: chunks.length ? Number(chunks[0]) : undefined,
+ month: chunks.length ? Number(chunks[1]) : undefined,
+ day: chunks.length ? Number(chunks[2]) : undefined,
+ },
+ member: this.form.get('member').value
+ };
+ }
+
+}
diff --git a/src/app/customers/form/edit/edit.form.component.html b/src/app/customers/form/edit/edit.form.component.html
new file mode 100644
index 0000000..8bdff89
--- /dev/null
+++ b/src/app/customers/form/edit/edit.form.component.html
@@ -0,0 +1,26 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Edit member ' | translate}}">
+ <fims-customer-form-component #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [catalog]="catalog$ | async"
+ [customer]="customer$ | async"
+ [editMode]="true">
+ </fims-customer-form-component>
+</fims-layout-card-over>
diff --git a/src/app/customers/form/edit/edit.form.component.ts b/src/app/customers/form/edit/edit.form.component.ts
new file mode 100644
index 0000000..55894c9
--- /dev/null
+++ b/src/app/customers/form/edit/edit.form.component.ts
@@ -0,0 +1,56 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Customer} from '../../../services/customer/domain/customer.model';
+import * as fromCustomers from '../../store';
+import {CustomersStore} from '../../store/index';
+import {UPDATE} from '../../store/customer.actions';
+import {Catalog} from '../../../services/catalog/domain/catalog.model';
+import {Observable} from 'rxjs/Observable';
+
+@Component({
+ templateUrl: './edit.form.component.html'
+})
+export class EditCustomerFormComponent {
+
+ customer$: Observable<Customer>;
+
+ catalog$: Observable<Catalog>;
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: CustomersStore) {
+ this.catalog$ = store.select(fromCustomers.getCustomerCatalog);
+ this.customer$ = store.select(fromCustomers.getSelectedCustomer);
+ }
+
+ onSave(customer: Customer) {
+ this.store.dispatch({ type: UPDATE, payload: {
+ customer,
+ activatedRoute: this.route
+ } });
+ }
+
+ onCancel() {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/customers/form/employees/employees.component.html b/src/app/customers/form/employees/employees.component.html
new file mode 100644
index 0000000..1f6046b
--- /dev/null
+++ b/src/app/customers/form/employees/employees.component.html
@@ -0,0 +1,29 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-select-list flex
+ [data]="employees"
+ id="identifier"
+ displayName="surname"
+ listIcon="group"
+ [preSelection]="preSelection"
+ (onSearch)="search($event)"
+ (onSelectionChange)="selectionChange($event)"
+ title="Assigned Employee"
+ noResultsMessage="No employee was found."
+ noSelectionMessage="No employee assigned to member , yet."
+></fims-select-list>
diff --git a/src/app/customers/form/employees/employees.component.ts b/src/app/customers/form/employees/employees.component.ts
new file mode 100644
index 0000000..e6c70f2
--- /dev/null
+++ b/src/app/customers/form/employees/employees.component.ts
@@ -0,0 +1,57 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {Employee} from '../../../services/office/domain/employee.model';
+import {FetchRequest} from '../../../services/domain/paging/fetch-request.model';
+import * as fromRoot from '../../../store';
+import {SEARCH} from '../../../store/employee/employee.actions';
+import {Store} from '@ngrx/store';
+
+@Component({
+ selector: 'fims-customer-employees-form',
+ templateUrl: './employees.component.html'
+})
+export class CustomerEmployeesComponent implements OnInit {
+
+ employees: Observable<Employee[]>;
+
+ @Input() preSelection: string[];
+
+ @Output() onSelectionChange = new EventEmitter<string[]>();
+
+ constructor(private store: Store<fromRoot.State>) {}
+
+ ngOnInit(): void {
+ this.employees = this.store.select(fromRoot.getEmployeeSearchResults)
+ .map(employeePage => employeePage.employees);
+ }
+
+ search(searchTerm) {
+ const fetchRequest: FetchRequest = {
+ searchTerm
+ };
+ this.store.dispatch({ type: SEARCH, payload: fetchRequest });
+ }
+
+ selectionChange(selections: string[]): void {
+ this.onSelectionChange.emit(selections);
+ }
+
+}
diff --git a/src/app/customers/form/form.component.html b/src/app/customers/form/form.component.html
new file mode 100644
index 0000000..0bcfdc5
--- /dev/null
+++ b/src/app/customers/form/form.component.html
@@ -0,0 +1,87 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Member details' | translate}}" [state]="detailForm.valid ? 'complete' : detailForm.pristine ? 'none' : 'required'">
+
+ <fims-customer-detail-form #detailForm [formData]="detailFormData" [editMode]="editMode"></fims-customer-detail-form>
+
+ <ng-template td-step-actions>
+ <fims-form-continue-action (onContinue)="addressStep.open()"></fims-form-continue-action>
+ </ng-template>
+ </td-step>
+
+ <td-step #addressStep label="{{'Member Address' | translate}}" [state]="addressForm.valid ? 'complete' : addressForm.pristine ? 'none' : 'required'">
+
+ <fims-address-form #addressForm [formData]="addressFormData"></fims-address-form>
+
+ <ng-template td-step-actions>
+ <fims-form-continue-action (onContinue)="contactStep.open()"></fims-form-continue-action>
+ </ng-template>
+ </td-step>
+
+ <td-step #contactStep label="{{'Member contact(optional)' | translate}}" [state]="contactForm.pristine ? 'none' : contactForm.valid ? 'completed' : 'required'">
+
+ <fims-customer-contact-form #contactForm [formData]="contactFormData"></fims-customer-contact-form>
+
+ <ng-template td-step-actions>
+ <fims-form-continue-action (onContinue)="officeStep.open()"></fims-form-continue-action>
+ </ng-template>
+ </td-step>
+
+ <td-step #officeStep label="{{'Assign member to office(optional)' | translate}}"
+ [state]="selectedOffices.length ? 'complete' : 'none'">
+
+ <fims-customer-offices-form [preSelection]="selectedOffices" (onSelectionChange)="selectOffice($event)"></fims-customer-offices-form>
+
+ <ng-template td-step-actions>
+ <fims-form-continue-action (onContinue)="employeeStep.open()"></fims-form-continue-action>
+ </ng-template>
+ <ng-template td-step-summary *ngIf="selectedOffices && selectedOffices.length > 0" [translate]="'Member is assigned to office:'" [translateParams]="{value: selectedOffices[0]}">
+ </ng-template>
+ </td-step>
+
+ <td-step #employeeStep label="{{'Assign member to employee(optional)' | translate}}"
+ [state]="selectedEmployees.length ? 'complete' : 'none'">
+
+ <fims-customer-employees-form [preSelection]="selectedEmployees" (onSelectionChange)="selectEmployee($event)"></fims-customer-employees-form>
+
+ <ng-template td-step-actions>
+ <fims-form-continue-action (onContinue)="customFieldsStep.open()"></fims-form-continue-action>
+ </ng-template>
+ </td-step>
+
+ <td-step #customFieldsStep label="{{'Custom fields' | translate}}" [state]="customFieldsForm.pristine ? 'none' : customFieldsForm.valid ? 'completed' : 'required'">
+ <fims-custom-fields-component
+ #customFieldsForm
+ [catalog]="catalog"
+ [formData]="customFieldsFormData">
+ </fims-custom-fields-component>
+ </td-step>
+
+ <td-step label="{{'Final step' | translate}}" [state]="'complete'">
+ <ng-template td-step-summary>
+ <fims-form-final-action
+ [resourceName]="'MEMBER'"
+ [editMode]="editMode"
+ [disabled]="!isValid"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/customers/form/form.component.spec.ts b/src/app/customers/form/form.component.spec.ts
new file mode 100644
index 0000000..6151e30
--- /dev/null
+++ b/src/app/customers/form/form.component.spec.ts
@@ -0,0 +1,202 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {CustomerDetailFormComponent} from './detail/detail.component';
+import {CustomerFormComponent} from './form.component';
+import {CustomerContactFormComponent} from './contact/contact.component';
+import {CustomerCustomFieldsComponent} from './customFields/custom-fields.component';
+import {ReactiveFormsModule} from '@angular/forms';
+import {CovalentChipsModule, CovalentStepsModule} from '@covalent/core';
+import {Component, EventEmitter, ViewChild} from '@angular/core';
+import {Customer} from '../../services/customer/domain/customer.model';
+import {TranslateModule} from '@ngx-translate/core';
+import {CustomerEmployeesComponent} from './employees/employees.component';
+import {CustomerOfficesComponent} from './offices/offices.component';
+import {Observable} from 'rxjs/Observable';
+import {Store} from '@ngrx/store';
+import {CustomersStore} from '../store/index';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {CountryService} from '../../services/country/country.service';
+import {Country} from '../../services/country/model/country.model';
+import {FimsSharedModule} from '../../common/common.module';
+import {MatAutocompleteModule, MatCheckboxModule, MatIconModule, MatInputModule, MatRadioModule} from '@angular/material';
+
+const customerTemplate: Customer = {
+ identifier: 'test',
+ currentState: 'ACTIVE',
+ type: 'PERSON',
+ givenName: 'test',
+ middleName: 'test',
+ surname: 'test',
+ address: {
+ street: 'test',
+ city: 'test',
+ countryCode: 'te',
+ country: 'test',
+ region: 'test',
+ postalCode: 'test'
+ },
+ dateOfBirth: {
+ year: 1982,
+ month: 6,
+ day: 24
+ },
+ member: true,
+ identificationCard: {
+ issuer: 'test',
+ expirationDate: {
+ year: 1982,
+ month: 6,
+ day: 24
+ },
+ type: 'passport',
+ number: '12312'
+ },
+ contactDetails: [{
+ type: 'EMAIL',
+ group: 'BUSINESS',
+ value: 'test@test.de',
+ preferenceLevel: 0
+ }],
+ customValues: []
+};
+
+const country: Country = {
+ displayName: '',
+ name: customerTemplate.address.country,
+ alpha2Code: customerTemplate.address.countryCode,
+ translations: {}
+};
+
+describe('Test customer form', () => {
+
+ let fixture: ComponentFixture<TestComponent>;
+
+ let testComponent: TestComponent;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [
+ TestComponent,
+ CustomerFormComponent,
+ CustomerDetailFormComponent,
+ CustomerContactFormComponent,
+ CustomerCustomFieldsComponent,
+ CustomerEmployeesComponent,
+ CustomerOfficesComponent
+ ],
+ imports: [
+ TranslateModule.forRoot(),
+ FimsSharedModule,
+ ReactiveFormsModule,
+ MatInputModule,
+ MatIconModule,
+ MatRadioModule,
+ MatAutocompleteModule,
+ MatCheckboxModule,
+ CovalentStepsModule,
+ CovalentChipsModule,
+ NoopAnimationsModule
+ ],
+ providers: [
+ {
+ // Used by address component
+ provide: CountryService, useClass: class {
+ fetchByCountryCode = jasmine.createSpy('fetchByCountryCode').and.returnValue(country);
+ fetchCountries = jasmine.createSpy('fetchCountries').and.returnValue([country]);
+ }
+ },
+ {
+ provide: CustomersStore, useClass: class {
+ dispatch = jasmine.createSpy('dispatch');
+ select = jasmine.createSpy('select').and.returnValue(Observable.empty());
+ }
+ },
+ {
+ provide: Store, useClass: class {
+ dispatch = jasmine.createSpy('dispatch');
+ select = jasmine.createSpy('select').and.returnValue(Observable.empty());
+ }
+ }
+ ]
+ });
+
+ fixture = TestBed.createComponent(TestComponent);
+ testComponent = fixture.componentInstance;
+ });
+
+ it('should test if the form save the original values', () => {
+ fixture.detectChanges();
+
+ testComponent.saveEmitter.subscribe((customer: Customer) => {
+ expect(customerTemplate.identifier).toEqual(customer.identifier);
+ expect(customerTemplate.currentState).toEqual(customer.currentState);
+ expect(customerTemplate.type).toEqual(customer.type);
+ expect(customerTemplate.givenName).toEqual(customer.givenName);
+ expect(customerTemplate.middleName).toEqual(customer.middleName);
+ expect(customerTemplate.surname).toEqual(customer.surname);
+
+ expect(customerTemplate.accountBeneficiary).toEqual(customer.accountBeneficiary);
+ expect(customerTemplate.referenceCustomer).toEqual(customer.referenceCustomer);
+ expect(customerTemplate.assignedOffice).toEqual(customer.assignedOffice);
+ expect(customerTemplate.assignedEmployee).toEqual(customer.assignedEmployee);
+
+ expect(customerTemplate.address.city).toEqual(customer.address.city);
+ expect(customerTemplate.address.country).toEqual(customer.address.country);
+ expect(customerTemplate.address.countryCode).toEqual(customer.address.countryCode);
+ expect(customerTemplate.address.postalCode).toEqual(customer.address.postalCode);
+ expect(customerTemplate.address.region).toEqual(customer.address.region);
+ expect(customerTemplate.address.street).toEqual(customer.address.street);
+
+ expect(customerTemplate.dateOfBirth.day).toEqual(customer.dateOfBirth.day);
+ expect(customerTemplate.dateOfBirth.month).toEqual(customer.dateOfBirth.month);
+ expect(customerTemplate.dateOfBirth.year).toEqual(customer.dateOfBirth.year);
+
+ expect(customer.contactDetails.length).toEqual(1);
+ });
+
+ testComponent.triggerSave();
+ });
+
+});
+
+@Component({
+ template: `
+ <fims-customer-form-component #form (onSave)="onSave($event)" (onCancel)="onCancel($event)" [customer]="customer">
+ </fims-customer-form-component>
+ `
+})
+class TestComponent {
+
+ saveEmitter = new EventEmitter<Customer>();
+
+ @ViewChild('form') formComponent: CustomerFormComponent;
+
+ customer: Customer = customerTemplate;
+
+ triggerSave(): void {
+ this.formComponent.save();
+ }
+
+ onSave(customer: Customer): void {
+ this.saveEmitter.emit(customer);
+ }
+
+ onCancel(): void {}
+}
diff --git a/src/app/customers/form/form.component.ts b/src/app/customers/form/form.component.ts
new file mode 100644
index 0000000..7b4de63
--- /dev/null
+++ b/src/app/customers/form/form.component.ts
@@ -0,0 +1,144 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
+import {TdStepComponent} from '@covalent/core';
+import {Customer} from '../../services/customer/domain/customer.model';
+import {CustomerDetailFormComponent, CustomerDetailFormData} from './detail/detail.component';
+import {AddressFormComponent} from '../../common/address/address.component';
+import {Address} from '../../services/domain/address/address.model';
+import {CustomerContactFormComponent} from './contact/contact.component';
+import {ContactDetail} from '../../services/domain/contact/contact-detail.model';
+import {Value} from '../../services/catalog/domain/value.model';
+import {CustomerCustomFieldsComponent} from './customFields/custom-fields.component';
+import {Catalog} from '../../services/catalog/domain/catalog.model';
+
+@Component({
+ selector: 'fims-customer-form-component',
+ templateUrl: './form.component.html'
+})
+export class CustomerFormComponent implements OnInit {
+
+ private _customer: Customer;
+
+ @Input('customer') set customer(customer: Customer) {
+ this._customer = customer;
+
+ this.detailFormData = {
+ identifier: customer.identifier,
+ firstName: customer.givenName,
+ middleName: customer.middleName,
+ lastName: customer.surname,
+ dateOfBirth: customer.dateOfBirth,
+ member: customer.member
+ };
+
+ this.addressFormData = customer.address;
+
+ this.contactFormData = customer.contactDetails;
+
+ this.selectedOffices = customer.assignedOffice ? [customer.assignedOffice] : [];
+
+ this.selectedEmployees = customer.assignedEmployee ? [customer.assignedEmployee] : [];
+
+ this.customFieldsFormData = customer.customValues;
+ };
+
+ @Input('catalog') catalog: Catalog;
+
+ @Input('editMode') editMode: boolean;
+
+ @Output('onSave') onSave = new EventEmitter<Customer>();
+
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ @ViewChild('detailsStep') step: TdStepComponent;
+
+ @ViewChild('detailForm') detailForm: CustomerDetailFormComponent;
+ detailFormData: CustomerDetailFormData;
+
+ @ViewChild('contactForm') contactForm: CustomerContactFormComponent;
+ contactFormData: ContactDetail[];
+
+ @ViewChild('addressForm') addressForm: AddressFormComponent;
+ addressFormData: Address;
+
+ selectedOffices: string[] = [];
+
+ selectedEmployees: string[] = [];
+
+ @ViewChild('customFieldsForm') customFieldsForm: CustomerCustomFieldsComponent;
+ customFieldsFormData: Value[];
+
+ ngOnInit() {
+ this.openDetailStep();
+ }
+
+ openDetailStep(): void {
+ this.step.open();
+ }
+
+ showIdentifierValidationError(): void {
+ this.detailForm.setError('identifier', 'unique', true);
+ this.openDetailStep();
+ }
+
+ selectOffice(selections: string[]): void {
+ this.selectedOffices = selections;
+ }
+
+ selectEmployee(selections: string[]): void {
+ this.selectedEmployees = selections;
+ }
+
+ get isValid(): boolean {
+ return (this.detailForm.valid && this.addressForm.valid)
+ && this.contactForm.validWhenOptional
+ && this.customFieldsForm.valid;
+ }
+
+ get customer(): Customer {
+ return this._customer;
+ }
+
+ save() {
+ const detailFormData = this.detailForm.formData;
+
+ const customer: Customer = {
+ identifier: detailFormData.identifier,
+ currentState: this.customer.currentState,
+ givenName: detailFormData.firstName,
+ surname: detailFormData.lastName,
+ middleName: detailFormData.middleName,
+ type: 'PERSON',
+ address: this.addressForm.formData,
+ contactDetails: this.contactForm.formData,
+ dateOfBirth: detailFormData.dateOfBirth,
+ member: detailFormData.member,
+ assignedOffice: this.selectedOffices && this.selectedOffices.length > 0 ? this.selectedOffices[0] : undefined,
+ assignedEmployee: this.selectedEmployees && this.selectedEmployees.length > 0 ? this.selectedEmployees[0] : undefined,
+ customValues: this.customFieldsForm.formData
+ };
+ this.onSave.emit(customer);
+ }
+
+ cancel() {
+ this.onCancel.emit();
+ }
+
+}
diff --git a/src/app/customers/form/offices/offices.component.html b/src/app/customers/form/offices/offices.component.html
new file mode 100644
index 0000000..8cdc509
--- /dev/null
+++ b/src/app/customers/form/offices/offices.component.html
@@ -0,0 +1,29 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-select-list flex
+ [data]="offices"
+ id="identifier"
+ displayName="name"
+ listIcon="store"
+ [preSelection]="preSelection"
+ (onSearch)="search($event)"
+ (onSelectionChange)="select($event)"
+ title="Assigned Office"
+ noResultsMessage="No office was found."
+ noSelectionMessage="No office assigned to member , yet."
+></fims-select-list>
diff --git a/src/app/customers/form/offices/offices.component.ts b/src/app/customers/form/offices/offices.component.ts
new file mode 100644
index 0000000..82a3061
--- /dev/null
+++ b/src/app/customers/form/offices/offices.component.ts
@@ -0,0 +1,58 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {Office} from '../../../services/office/domain/office.model';
+import {FetchRequest} from '../../../services/domain/paging/fetch-request.model';
+import {Store} from '@ngrx/store';
+import * as fromRoot from '../../../store';
+import {SEARCH} from '../../../store/office/office.actions';
+
+@Component({
+ selector: 'fims-customer-offices-form',
+ templateUrl: './offices.component.html'
+})
+export class CustomerOfficesComponent implements OnInit {
+
+ offices: Observable<Office[]>;
+
+ @Input() preSelection: string;
+
+ @Output() onSelectionChange = new EventEmitter<string[]>();
+
+ constructor(private store: Store<fromRoot.State>) {}
+
+ ngOnInit(): void {
+ this.offices = this.store.select(fromRoot.getOfficeSearchResults)
+ .map(officePage => officePage.offices);
+ }
+
+ search(searchTerm) {
+ const fetchRequest: FetchRequest = {
+ searchTerm
+ };
+
+ this.store.dispatch({ type: SEARCH, payload: fetchRequest });
+ }
+
+ select(selections: string[]): void {
+ this.onSelectionChange.emit(selections);
+ }
+
+}
diff --git a/src/app/customers/store/catalogs/catalog.actions.ts b/src/app/customers/store/catalogs/catalog.actions.ts
new file mode 100644
index 0000000..d6d05b8
--- /dev/null
+++ b/src/app/customers/store/catalogs/catalog.actions.ts
@@ -0,0 +1,152 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {type} from '../../../store/util';
+import {Action} from '@ngrx/store';
+import {Catalog} from '../../../services/catalog/domain/catalog.model';
+import {RoutePayload} from '../../../common/store/route-payload';
+import {Field} from '../../../services/catalog/domain/field.model';
+
+export const LOAD = type('[Customer Catalog] Load');
+
+export const CREATE = type('[Customer Catalog] Create');
+export const CREATE_SUCCESS = type('[Customer Catalog] Create Success');
+export const CREATE_FAIL = type('[Customer Catalog] Create Fail');
+
+export const DELETE = type('[Customer Catalog] Delete');
+export const DELETE_SUCCESS = type('[Customer Catalog] Delete Success');
+export const DELETE_FAIL = type('[Customer Catalog] Delete Fail');
+
+export const SELECT_FIELD = type('[Customer Catalog] Select Field');
+
+export const UPDATE_FIELD = type('[Customer Catalog] Update Field');
+export const UPDATE_FIELD_SUCCESS = type('[Customer Catalog] Update Field Success');
+export const UPDATE_FIELD_FAIL = type('[Customer Catalog] Update Field Fail');
+
+export const DELETE_FIELD = type('[Customer Catalog] Delete Field');
+export const DELETE_FIELD_SUCCESS = type('[Customer Catalog] Delete Field Success');
+export const DELETE_FIELD_FAIL = type('[Customer Catalog] Delete Field Fail');
+
+export interface CatalogRoutePayload extends RoutePayload {
+ catalog: Catalog;
+}
+
+export interface FieldRoutePayload extends RoutePayload {
+ catalogIdentifier: string;
+ field: Field;
+}
+
+export class LoadAction implements Action {
+ readonly type = LOAD;
+
+ constructor(public payload: Catalog) { }
+}
+
+export class CreateCatalogAction implements Action {
+ readonly type = CREATE;
+
+ constructor(public payload: CatalogRoutePayload) { }
+}
+
+export class CreateCatalogSuccessAction implements Action {
+ readonly type = CREATE_SUCCESS;
+
+ constructor(public payload: CatalogRoutePayload) { }
+}
+
+export class CreateCatalogFailAction implements Action {
+ readonly type = CREATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class DeleteCatalogAction implements Action {
+ readonly type = DELETE;
+
+ constructor(public payload: CatalogRoutePayload) { }
+}
+
+export class DeleteCatalogSuccessAction implements Action {
+ readonly type = DELETE_SUCCESS;
+
+ constructor(public payload: CatalogRoutePayload) { }
+}
+
+export class DeleteCatalogFailAction implements Action {
+ readonly type = DELETE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class SelectFieldAction implements Action {
+ readonly type = SELECT_FIELD;
+
+ constructor(public payload: string) { }
+}
+
+export class UpdateFieldAction implements Action {
+ readonly type = UPDATE_FIELD;
+
+ constructor(public payload: FieldRoutePayload) { }
+}
+
+export class UpdateFieldSuccessAction implements Action {
+ readonly type = UPDATE_FIELD_SUCCESS;
+
+ constructor(public payload: FieldRoutePayload) { }
+}
+
+export class UpdateFieldFailAction implements Action {
+ readonly type = UPDATE_FIELD_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class DeleteFieldAction implements Action {
+ readonly type = DELETE_FIELD;
+
+ constructor(public payload: FieldRoutePayload) { }
+}
+
+export class DeleteFieldSuccessAction implements Action {
+ readonly type = DELETE_FIELD_SUCCESS;
+
+ constructor(public payload: FieldRoutePayload) { }
+}
+
+export class DeleteFieldFailAction implements Action {
+ readonly type = DELETE_FIELD_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export type Actions
+ = LoadAction
+ | CreateCatalogAction
+ | CreateCatalogSuccessAction
+ | CreateCatalogFailAction
+ | DeleteCatalogAction
+ | DeleteCatalogSuccessAction
+ | DeleteCatalogFailAction
+ | SelectFieldAction
+ | UpdateFieldAction
+ | UpdateFieldSuccessAction
+ | UpdateFieldFailAction
+ | DeleteFieldAction
+ | DeleteFieldSuccessAction
+ | DeleteFieldFailAction;
diff --git a/src/app/customers/store/catalogs/catalog.reducer.ts b/src/app/customers/store/catalogs/catalog.reducer.ts
new file mode 100644
index 0000000..e1ddd20
--- /dev/null
+++ b/src/app/customers/store/catalogs/catalog.reducer.ts
@@ -0,0 +1,111 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as catalogActions from './catalog.actions';
+import {Catalog} from '../../../services/catalog/domain/catalog.model';
+import {Field} from '../../../services/catalog/domain/field.model';
+import {createSelector} from 'reselect';
+
+export interface State {
+ catalog: Catalog;
+ loadedAt: number;
+ selectedFieldIdentifier: string;
+}
+
+const initialState: State = {
+ catalog: null,
+ loadedAt: null,
+ selectedFieldIdentifier: null
+};
+
+export function reducer(state: State = initialState, action: catalogActions.Actions): State {
+
+ switch (action.type) {
+
+ case catalogActions.LOAD: {
+ const catalog: Catalog = action.payload;
+
+ return Object.assign({}, state, {
+ catalog,
+ loadedAt: Date.now()
+ });
+ }
+
+ case catalogActions.CREATE_SUCCESS: {
+ const catalog: Catalog = action.payload.catalog;
+
+ return Object.assign({}, state, {
+ catalog,
+ loadedAt: state.loadedAt
+ });
+ }
+
+ case catalogActions.DELETE_SUCCESS: {
+ return initialState;
+ }
+
+ case catalogActions.SELECT_FIELD: {
+ return Object.assign({}, state, {
+ selectedFieldIdentifier: action.payload
+ });
+ }
+
+ case catalogActions.UPDATE_FIELD_SUCCESS: {
+ const payload = action.payload;
+ const updatedField: Field = payload.field;
+
+ const catalog = Object.assign({}, state.catalog, {
+ fields: state.catalog.fields.map(field =>
+ field.identifier === updatedField.identifier ? updatedField : field
+ )
+ });
+
+ return Object.assign({}, state, {
+ catalog,
+ loadedAt: state.loadedAt
+ });
+ }
+
+ case catalogActions.DELETE_FIELD_SUCCESS: {
+ const payload = action.payload;
+ const deletedField: Field = payload.field;
+
+ const catalog = Object.assign({}, state.catalog, {
+ fields: state.catalog.fields.filter(field =>
+ field.identifier !== deletedField.identifier
+ )
+ });
+
+ return Object.assign({}, state, {
+ catalog,
+ loadedAt: state.loadedAt
+ });
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+export const getCustomerCatalog = (state: State) => state.catalog;
+export const getCustomerCatalogLoadedAt = (state: State) => state.loadedAt;
+export const getSelectedFieldId = (state: State) => state.selectedFieldIdentifier;
+export const getSelectedField = createSelector(getCustomerCatalog, getSelectedFieldId, (catalog, selectedId) => {
+ return catalog.fields.find(field => field.identifier === selectedId);
+});
diff --git a/src/app/customers/store/catalogs/effects/notification.effects.ts b/src/app/customers/store/catalogs/effects/notification.effects.ts
new file mode 100644
index 0000000..2483668
--- /dev/null
+++ b/src/app/customers/store/catalogs/effects/notification.effects.ts
@@ -0,0 +1,91 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {NotificationService, NotificationType} from '../../../../services/notification/notification.service';
+import {Action} from '@ngrx/store';
+import * as catalogActions from '../catalog.actions';
+import {DeleteCatalogFailAction, DeleteFieldFailAction, UpdateFieldFailAction} from '../catalog.actions';
+
+@Injectable()
+export class CatalogNotificationEffects {
+
+ @Effect({ dispatch: false })
+ createCatalogSuccess$: Observable<Action> = this.actions$
+ .ofType(catalogActions.CREATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Catalog is going to be saved'
+ }));
+
+ @Effect({ dispatch: false })
+ deleteCatalogSuccess$: Observable<Action> = this.actions$
+ .ofType(catalogActions.DELETE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Catalog is going to be deleted'
+ }));
+
+ @Effect({ dispatch: false })
+ deleteCatalogFail$: Observable<Action> = this.actions$
+ .ofType(catalogActions.DELETE_FAIL)
+ .do((action: DeleteCatalogFailAction) => this.notificationService.send({
+ type: NotificationType.ALERT,
+ title: 'Catalog can\'t be deleted',
+ message: action.payload.message
+ }));
+
+ @Effect({ dispatch: false })
+ updateFieldSuccess$: Observable<Action> = this.actions$
+ .ofType(catalogActions.UPDATE_FIELD_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Field is going to be updated'
+ }));
+
+ @Effect({ dispatch: false })
+ updateFieldFail$: Observable<Action> = this.actions$
+ .ofType(catalogActions.UPDATE_FIELD_FAIL)
+ .do((action: UpdateFieldFailAction) => this.notificationService.send({
+ type: NotificationType.ALERT,
+ title: 'Field can\'t be updated',
+ message: action.payload.message
+ }));
+
+ @Effect({ dispatch: false })
+ deleteFieldSuccess$: Observable<Action> = this.actions$
+ .ofType(catalogActions.DELETE_FIELD_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Field is going to be deleted'
+ }));
+
+ @Effect({ dispatch: false })
+ deleteFieldFail$: Observable<Action> = this.actions$
+ .ofType(catalogActions.DELETE_FIELD_FAIL)
+ .do((action: DeleteFieldFailAction) => this.notificationService.send({
+ type: NotificationType.ALERT,
+ title: 'Field can\'t be deleted',
+ message: action.payload.message
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+
+}
diff --git a/src/app/customers/store/catalogs/effects/route.effects.ts b/src/app/customers/store/catalogs/effects/route.effects.ts
new file mode 100644
index 0000000..efcf223
--- /dev/null
+++ b/src/app/customers/store/catalogs/effects/route.effects.ts
@@ -0,0 +1,55 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import * as catalogActions from '../catalog.actions';
+import {Action} from '@ngrx/store';
+import {Router} from '@angular/router';
+
+@Injectable()
+export class CatalogRouteEffects {
+
+ @Effect({ dispatch: false })
+ createCatalogSuccess: Observable<Action> = this.actions$
+ .ofType(catalogActions.CREATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute }));
+
+ @Effect({ dispatch: false })
+ deleteCatalogSuccess: Observable<Action> = this.actions$
+ .ofType(catalogActions.DELETE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../../../'], { relativeTo: payload.activatedRoute }));
+
+ @Effect({ dispatch: false })
+ updateFieldSuccess: Observable<Action> = this.actions$
+ .ofType(catalogActions.UPDATE_FIELD_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute }));
+
+ @Effect({ dispatch: false })
+ deleteFieldSuccess: Observable<Action> = this.actions$
+ .ofType(catalogActions.DELETE_FIELD_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../../../../../../'], { relativeTo: payload.activatedRoute }));
+
+ constructor(private actions$: Actions, private router: Router) { }
+
+}
diff --git a/src/app/customers/store/catalogs/effects/service.effects.ts b/src/app/customers/store/catalogs/effects/service.effects.ts
new file mode 100644
index 0000000..c834b27
--- /dev/null
+++ b/src/app/customers/store/catalogs/effects/service.effects.ts
@@ -0,0 +1,72 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+import * as catalogActions from '../catalog.actions';
+import {CatalogService} from '../../../../services/catalog/catalog.service';
+
+@Injectable()
+export class CatalogApiEffects {
+
+ @Effect()
+ createCatalog$: Observable<Action> = this.actions$
+ .ofType(catalogActions.CREATE)
+ .map((action: catalogActions.CreateCatalogAction) => action.payload)
+ .mergeMap(payload =>
+ this.catalogService.createCatalog(payload.catalog)
+ .map(() => new catalogActions.CreateCatalogSuccessAction(payload))
+ .catch((error) => of(new catalogActions.CreateCatalogFailAction(error)))
+ );
+
+ @Effect()
+ deleteCatalog$: Observable<Action> = this.actions$
+ .ofType(catalogActions.DELETE)
+ .map((action: catalogActions.DeleteCatalogAction) => action.payload)
+ .mergeMap(payload =>
+ this.catalogService.deleteCatalog(payload.catalog)
+ .map(() => new catalogActions.DeleteCatalogSuccessAction(payload))
+ .catch((error) => of(new catalogActions.DeleteCatalogFailAction(error)))
+ );
+
+ @Effect()
+ updateField$: Observable<Action> = this.actions$
+ .ofType(catalogActions.UPDATE_FIELD)
+ .map((action: catalogActions.UpdateFieldAction) => action.payload)
+ .mergeMap(payload =>
+ this.catalogService.updateField(payload.catalogIdentifier, payload.field)
+ .map(() => new catalogActions.UpdateFieldSuccessAction(payload))
+ .catch((error) => of(new catalogActions.UpdateFieldFailAction(error)))
+ );
+
+ @Effect()
+ deleteField$: Observable<Action> = this.actions$
+ .ofType(catalogActions.DELETE_FIELD)
+ .map((action: catalogActions.DeleteFieldAction) => action.payload)
+ .mergeMap(payload =>
+ this.catalogService.deleteField(payload.catalogIdentifier, payload.field)
+ .map(() => new catalogActions.DeleteFieldSuccessAction(payload))
+ .catch((error) => of(new catalogActions.DeleteFieldFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private catalogService: CatalogService) { }
+
+}
diff --git a/src/app/customers/store/commands/commands.actions.ts b/src/app/customers/store/commands/commands.actions.ts
new file mode 100644
index 0000000..d185afa
--- /dev/null
+++ b/src/app/customers/store/commands/commands.actions.ts
@@ -0,0 +1,40 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {type} from '../../../store/util';
+import {Action} from '@ngrx/store';
+import {Command} from '../../../services/customer/domain/command.model';
+
+export const LOAD_ALL = type('[Customer Command] Load All');
+export const LOAD_ALL_COMPLETE = type('[Customer Command] Load All Complete');
+
+export class LoadAllAction implements Action {
+ readonly type = LOAD_ALL;
+
+ constructor(public payload: string) { }
+}
+
+export class LoadAllCompleteAction implements Action {
+ readonly type = LOAD_ALL_COMPLETE;
+
+ constructor(public payload: Command[]) { }
+}
+
+export type Actions
+ = LoadAllAction
+ | LoadAllCompleteAction;
diff --git a/src/app/customers/store/commands/commands.reducer.ts b/src/app/customers/store/commands/commands.reducer.ts
new file mode 100644
index 0000000..01ca674
--- /dev/null
+++ b/src/app/customers/store/commands/commands.reducer.ts
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as command from './commands.actions';
+import {Command} from '../../../services/customer/domain/command.model';
+
+export interface State {
+ commands: Command[];
+}
+
+export const initialState: State = {
+ commands: []
+};
+
+export function reducer(state = initialState, action: command.Actions): State {
+
+ switch (action.type) {
+
+ case command.LOAD_ALL: {
+ return initialState;
+ }
+
+ case command.LOAD_ALL_COMPLETE: {
+ const commands = action.payload;
+
+ return {
+ commands: commands
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+export const getCommands = (state: State) => state.commands;
diff --git a/src/app/customers/store/commands/effects/service.effects.ts b/src/app/customers/store/commands/effects/service.effects.ts
new file mode 100644
index 0000000..c9017ad
--- /dev/null
+++ b/src/app/customers/store/commands/effects/service.effects.ts
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect, toPayload} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+import * as commandActions from '../commands.actions';
+import {CustomerService} from '../../../../services/customer/customer.service';
+
+@Injectable()
+export class CustomerCommandApiEffects {
+
+ @Effect()
+ loadCommands$: Observable<Action> = this.actions$
+ .ofType(commandActions.LOAD_ALL)
+ .map(toPayload)
+ .mergeMap(customerId =>
+ this.customerService.listCustomerCommand(customerId)
+ .map(commands => new commandActions.LoadAllCompleteAction(commands))
+ .catch((error) => of(new commandActions.LoadAllCompleteAction([])))
+ );
+
+ constructor(private actions$: Actions, private customerService: CustomerService) { }
+
+}
diff --git a/src/app/customers/store/customer.actions.ts b/src/app/customers/store/customer.actions.ts
new file mode 100644
index 0000000..87cc4c9
--- /dev/null
+++ b/src/app/customers/store/customer.actions.ts
@@ -0,0 +1,111 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Action} from '@ngrx/store';
+import {type} from '../../store/util';
+import {Error} from '../../services/domain/error.model';
+import {Customer} from '../../services/customer/domain/customer.model';
+import {RoutePayload} from '../../common/store/route-payload';
+import {
+ CreateResourceSuccessPayload,
+ LoadResourcePayload,
+ SelectResourcePayload,
+ UpdateResourceSuccessPayload
+} from '../../common/store/resource.reducer';
+
+export const LOAD = type('[Customer] Load');
+export const SELECT = type('[Customer] Select');
+
+export const CREATE = type('[Customer] Create');
+export const CREATE_SUCCESS = type('[Customer] Create Success');
+export const CREATE_FAIL = type('[Customer] Create Fail');
+
+export const UPDATE = type('[Customer] Update');
+export const UPDATE_SUCCESS = type('[Customer] Update Success');
+export const UPDATE_FAIL = type('[Customer] Update Fail');
+
+export const RESET_FORM = type('[Customer] Reset Form');
+
+export interface CustomerRoutePayload extends RoutePayload {
+ customer: Customer;
+}
+
+export class LoadAction implements Action {
+ readonly type = LOAD;
+
+ constructor(public payload: LoadResourcePayload) { }
+}
+
+export class SelectAction implements Action {
+ readonly type = SELECT;
+
+ constructor(public payload: SelectResourcePayload) { }
+}
+
+export class CreateCustomerAction implements Action {
+ readonly type = CREATE;
+
+ constructor(public payload: CustomerRoutePayload) { }
+}
+
+export class CreateCustomerSuccessAction implements Action {
+ readonly type = CREATE_SUCCESS;
+
+ constructor(public payload: CreateResourceSuccessPayload) { }
+}
+
+export class CreateCustomerFailAction implements Action {
+ readonly type = CREATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class UpdateCustomerAction implements Action {
+ readonly type = UPDATE;
+
+ constructor(public payload: CustomerRoutePayload) { }
+}
+
+export class UpdateCustomerSuccessAction implements Action {
+ readonly type = UPDATE_SUCCESS;
+
+ constructor(public payload: UpdateResourceSuccessPayload) { }
+}
+
+export class UpdateCustomerFailAction implements Action {
+ readonly type = UPDATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class ResetCustomerFormAction implements Action {
+ readonly type = RESET_FORM;
+
+ constructor() {}
+}
+
+export type Actions
+ = LoadAction
+ | SelectAction
+ | CreateCustomerAction
+ | CreateCustomerSuccessAction
+ | CreateCustomerFailAction
+ | UpdateCustomerAction
+ | UpdateCustomerSuccessAction
+ | UpdateCustomerFailAction
+ | ResetCustomerFormAction;
diff --git a/src/app/customers/store/customerTasks/customer-task.actions.ts b/src/app/customers/store/customerTasks/customer-task.actions.ts
new file mode 100644
index 0000000..b621609
--- /dev/null
+++ b/src/app/customers/store/customerTasks/customer-task.actions.ts
@@ -0,0 +1,103 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Action} from '@ngrx/store';
+import {type} from '../../../store/util';
+import {Error} from '../../../services/domain/error.model';
+import {Command} from '../../../services/customer/domain/command.model';
+import {RoutePayload} from '../../../common/store/route-payload';
+import {ProcessStep} from '../../../services/customer/domain/process-step.model';
+
+export const LOAD_ALL = type('[Customer Task] Load All Process Steps');
+export const LOAD_ALL_COMPLETE = type('[Customer Task] Load All Process Steps Complete');
+
+export const EXECUTE_TASK = type('[Customer Task] Execute');
+export const EXECUTE_TASK_SUCCESS = type('[Customer Task] Success');
+export const EXECUTE_TASK_FAIL = type('[Customer Task] Fail');
+
+export const EXECUTE_COMMAND = type('[Customer Command] Execute');
+export const EXECUTE_COMMAND_SUCCESS = type('[Customer Command] Success');
+export const EXECUTE_COMMAND_FAIL = type('[Customer Command] Fail');
+
+export interface ExecuteTaskPayload extends RoutePayload {
+ customerId: string;
+ taskId: string;
+}
+
+export interface ExecuteCommandPayload {
+ customerId: string;
+ command: Command;
+}
+
+export class LoadAllAction implements Action {
+ readonly type = LOAD_ALL;
+
+ constructor(public payload: string) { }
+}
+
+export class LoadAllCompleteAction implements Action {
+ readonly type = LOAD_ALL_COMPLETE;
+
+ constructor(public payload: ProcessStep[]) { }
+}
+
+export class ExecuteTaskAction implements Action {
+ readonly type = EXECUTE_TASK;
+
+ constructor(public payload: ExecuteTaskPayload) { }
+}
+
+export class ExecuteTaskSuccessAction implements Action {
+ readonly type = EXECUTE_TASK_SUCCESS;
+
+ constructor(public payload: ExecuteTaskPayload) { }
+}
+
+export class ExecuteTaskFailAction implements Action {
+ readonly type = EXECUTE_TASK_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class ExecuteCommandAction implements Action {
+ readonly type = EXECUTE_COMMAND;
+
+ constructor(public payload: ExecuteCommandPayload) { }
+}
+
+export class ExecuteCommandSuccessAction implements Action {
+ readonly type = EXECUTE_COMMAND_SUCCESS;
+
+ constructor(public payload: ExecuteCommandPayload) { }
+}
+
+export class ExecuteCommandFailAction implements Action {
+ readonly type = EXECUTE_COMMAND_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export type Actions
+ = LoadAllAction
+ | LoadAllCompleteAction
+ | ExecuteTaskAction
+ | ExecuteTaskSuccessAction
+ | ExecuteTaskFailAction
+ | ExecuteCommandAction
+ | ExecuteCommandSuccessAction
+ | ExecuteCommandFailAction;
diff --git a/src/app/customers/store/customerTasks/customer-tasks.reducer.ts b/src/app/customers/store/customerTasks/customer-tasks.reducer.ts
new file mode 100644
index 0000000..ccddda5
--- /dev/null
+++ b/src/app/customers/store/customerTasks/customer-tasks.reducer.ts
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as task from './customer-task.actions';
+import {ProcessStep} from '../../../services/customer/domain/process-step.model';
+
+export interface State {
+ processSteps: ProcessStep[];
+}
+
+const initialState: State = {
+ processSteps: []
+};
+
+export function reducer(state = initialState, action: task.Actions): State {
+
+ switch (action.type) {
+
+ case task.LOAD_ALL: {
+ return initialState;
+ }
+
+ case task.LOAD_ALL_COMPLETE: {
+ const processSteps = action.payload;
+
+ return {
+ processSteps
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+export const getProcessSteps = (state: State) => state.processSteps;
diff --git a/src/app/customers/store/customerTasks/domain/status-command.model.ts b/src/app/customers/store/customerTasks/domain/status-command.model.ts
new file mode 100644
index 0000000..a9b7035
--- /dev/null
+++ b/src/app/customers/store/customerTasks/domain/status-command.model.ts
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {CustomerState} from '../../../../services/customer/domain/customer-state.model';
+import {TaskDefinition} from '../../../../services/customer/domain/task-definition.model';
+import {CommandAction} from '../../../../services/customer/domain/command.model';
+
+export interface StatusCommand {
+ action: CommandAction;
+ comment?: string;
+ tasks: TaskDefinition[];
+ preStates: CustomerState[];
+}
diff --git a/src/app/customers/store/customerTasks/effects/notification.effects.ts b/src/app/customers/store/customerTasks/effects/notification.effects.ts
new file mode 100644
index 0000000..9a78cf4
--- /dev/null
+++ b/src/app/customers/store/customerTasks/effects/notification.effects.ts
@@ -0,0 +1,64 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as taskActions from '../customer-task.actions';
+import {NotificationService, NotificationType} from '../../../../services/notification/notification.service';
+
+@Injectable()
+export class CustomerTasksNotificationEffects {
+
+ @Effect({ dispatch: false })
+ executeCustomerTaskSuccess$: Observable<Action> = this.actions$
+ .ofType(taskActions.EXECUTE_TASK_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Task is going to be executed'
+ }));
+
+ @Effect({ dispatch: false })
+ executeCustomerTaskFail$: Observable<Action> = this.actions$
+ .ofType(taskActions.EXECUTE_TASK_FAIL)
+ .do(() => this.notificationService.send({
+ type: NotificationType.ALERT,
+ message: 'Sorry, there was a problem executing your task'
+ }));
+
+ @Effect({ dispatch: false })
+ executeCustomerCommandSuccess$: Observable<Action> = this.actions$
+ .ofType(taskActions.EXECUTE_COMMAND_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Command is going to be executed'
+ }));
+
+ @Effect({ dispatch: false })
+ executeCustomerCommandFail$: Observable<Action> = this.actions$
+ .ofType(taskActions.EXECUTE_COMMAND_FAIL)
+ .do(() => this.notificationService.send({
+ type: NotificationType.ALERT,
+ message: 'Sorry, there was a problem executing your command'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+
+}
+
diff --git a/src/app/customers/store/customerTasks/effects/route.effects.ts b/src/app/customers/store/customerTasks/effects/route.effects.ts
new file mode 100644
index 0000000..4e6e2aa
--- /dev/null
+++ b/src/app/customers/store/customerTasks/effects/route.effects.ts
@@ -0,0 +1,37 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as taskActions from '../customer-task.actions';
+import {Router} from '@angular/router';
+
+@Injectable()
+export class CustomerTasksRouteEffects {
+
+ @Effect({ dispatch: false })
+ executeCustomerTaskSuccess$: Observable<Action> = this.actions$
+ .ofType(taskActions.EXECUTE_COMMAND_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute }));
+
+ constructor(private actions$: Actions, private router: Router) { }
+
+}
diff --git a/src/app/customers/store/customerTasks/effects/service.effects.ts b/src/app/customers/store/customerTasks/effects/service.effects.ts
new file mode 100644
index 0000000..ff2551c
--- /dev/null
+++ b/src/app/customers/store/customerTasks/effects/service.effects.ts
@@ -0,0 +1,66 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+import * as taskActions from '../customer-task.actions';
+import {CustomerService} from '../../../../services/customer/customer.service';
+
+@Injectable()
+export class CustomerTasksApiEffects {
+
+ @Effect()
+ loadAll$: Observable<Action> = this.actions$
+ .ofType(taskActions.LOAD_ALL)
+ .debounceTime(300)
+ .map((action: taskActions.LoadAllAction) => action.payload)
+ .switchMap(id => {
+ const nextSearch$ = this.actions$.ofType(taskActions.LOAD_ALL).skip(1);
+
+ return this.customerService.fetchProcessSteps(id)
+ .takeUntil(nextSearch$)
+ .map(processSteps => new taskActions.LoadAllCompleteAction(processSteps))
+ .catch(() => of(new taskActions.LoadAllCompleteAction([])));
+ });
+
+ @Effect()
+ executeTask: Observable<Action> = this.actions$
+ .ofType(taskActions.EXECUTE_TASK)
+ .map((action: taskActions.ExecuteTaskAction) => action.payload)
+ .mergeMap(payload =>
+ this.customerService.markTaskAsExecuted(payload.customerId, payload.taskId)
+ .map(() => new taskActions.ExecuteTaskSuccessAction(payload))
+ .catch((error) => of(new taskActions.ExecuteTaskFailAction(error)))
+ );
+
+ @Effect()
+ executeCommand: Observable<Action> = this.actions$
+ .ofType(taskActions.EXECUTE_COMMAND)
+ .map((action: taskActions.ExecuteCommandAction) => action.payload)
+ .mergeMap(payload =>
+ this.customerService.executeCustomerCommand(payload.customerId, payload.command)
+ .map(() => new taskActions.ExecuteCommandSuccessAction(payload))
+ .catch((error) => of(new taskActions.ExecuteCommandFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private customerService: CustomerService) { }
+
+}
diff --git a/src/app/customers/store/customers.reducer.ts b/src/app/customers/store/customers.reducer.ts
new file mode 100644
index 0000000..6ab0d6a
--- /dev/null
+++ b/src/app/customers/store/customers.reducer.ts
@@ -0,0 +1,74 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as customer from './customer.actions';
+import * as customerTasks from './customerTasks/customer-task.actions';
+import {Command} from '../../services/customer/domain/command.model';
+import {CustomerState} from '../../services/customer/domain/customer-state.model';
+import {ResourceState} from '../../common/store/resource.reducer';
+
+export const initialState: ResourceState = {
+ ids: [],
+ entities: {},
+ loadedAt: {},
+ selectedId: null,
+};
+
+export function reducer(state = initialState, action: customer.Actions | customerTasks.Actions): ResourceState {
+
+ switch (action.type) {
+
+ case customerTasks.EXECUTE_COMMAND_SUCCESS: {
+ const payload = action.payload;
+
+ const customerId = payload.customerId;
+ const command: Command = payload.command;
+
+ const customer = state.entities[customerId];
+
+ let customerState: CustomerState = null;
+
+ if (command.action === 'ACTIVATE') {
+ customerState = 'ACTIVE';
+ }else if (command.action === 'LOCK') {
+ customerState = 'LOCKED';
+ }else if (command.action === 'UNLOCK') {
+ customerState = 'ACTIVE';
+ }else if (command.action === 'CLOSE') {
+ customerState = 'CLOSED';
+ }else if (command.action === 'REOPEN') {
+ customerState = 'ACTIVE';
+ }
+
+ customer.currentState = customerState;
+
+ return {
+ ids: [ ...state.ids ],
+ entities: Object.assign({}, state.entities, {
+ [customer.identifier]: customer
+ }),
+ loadedAt: state.loadedAt,
+ selectedId: state.selectedId
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
diff --git a/src/app/customers/store/effects/notification.effects.ts b/src/app/customers/store/effects/notification.effects.ts
new file mode 100644
index 0000000..f545265
--- /dev/null
+++ b/src/app/customers/store/effects/notification.effects.ts
@@ -0,0 +1,40 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as customerActions from '../customer.actions';
+import {NotificationService, NotificationType} from '../../../services/notification/notification.service';
+
+@Injectable()
+export class CustomerNotificationEffects {
+
+ @Effect({ dispatch: false })
+ createCustomerSuccess$: Observable<Action> = this.actions$
+ .ofType(customerActions.CREATE_SUCCESS, customerActions.UPDATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Member is going to be saved'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+
+}
+
diff --git a/src/app/customers/store/effects/route.effects.ts b/src/app/customers/store/effects/route.effects.ts
new file mode 100644
index 0000000..2d4e816
--- /dev/null
+++ b/src/app/customers/store/effects/route.effects.ts
@@ -0,0 +1,37 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as customerActions from '../customer.actions';
+import {Router} from '@angular/router';
+
+@Injectable()
+export class CustomerRouteEffects {
+
+ @Effect({ dispatch: false })
+ createCustomerSuccess$: Observable<Action> = this.actions$
+ .ofType(customerActions.CREATE_SUCCESS, customerActions.UPDATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute }));
+
+ constructor(private actions$: Actions, private router: Router) { }
+
+}
diff --git a/src/app/customers/store/effects/service.effects.ts b/src/app/customers/store/effects/service.effects.ts
new file mode 100644
index 0000000..4f8be9f
--- /dev/null
+++ b/src/app/customers/store/effects/service.effects.ts
@@ -0,0 +1,58 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+import * as customerActions from '../customer.actions';
+import {CustomerService} from '../../../services/customer/customer.service';
+
+@Injectable()
+export class CustomerApiEffects {
+
+ @Effect()
+ createCustomer$: Observable<Action> = this.actions$
+ .ofType(customerActions.CREATE)
+ .map((action: customerActions.CreateCustomerAction) => action.payload)
+ .mergeMap(payload =>
+ this.customerService.createCustomer(payload.customer)
+ .map(() => new customerActions.CreateCustomerSuccessAction({
+ resource: payload.customer,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new customerActions.CreateCustomerFailAction(error)))
+ );
+
+ @Effect()
+ updateCustomer$: Observable<Action> = this.actions$
+ .ofType(customerActions.UPDATE)
+ .map((action: customerActions.UpdateCustomerAction) => action.payload)
+ .mergeMap(payload =>
+ this.customerService.updateCustomer(payload.customer)
+ .map(() => new customerActions.UpdateCustomerSuccessAction({
+ resource: payload.customer,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new customerActions.UpdateCustomerFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private customerService: CustomerService) { }
+
+}
diff --git a/src/app/customers/store/identityCards/effects/notification.effects.ts b/src/app/customers/store/identityCards/effects/notification.effects.ts
new file mode 100644
index 0000000..b7e6d96
--- /dev/null
+++ b/src/app/customers/store/identityCards/effects/notification.effects.ts
@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {NotificationService, NotificationType} from '../../../../services/notification/notification.service';
+import {Action} from '@ngrx/store';
+import {Observable} from 'rxjs/Observable';
+import {Actions, Effect} from '@ngrx/effects';
+import {Injectable} from '@angular/core';
+import * as identificationCardActions from '../identity-cards.actions';
+
+@Injectable()
+export class CustomerIdentificationCardNotificationEffects {
+
+ @Effect({ dispatch: false })
+ createIdentificationCardSuccess$: Observable<Action> = this.actions$
+ .ofType(identificationCardActions.CREATE_SUCCESS, identificationCardActions.UPDATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Identification card is going to be saved'
+ }));
+
+ @Effect({ dispatch: false })
+ deleteIdentificationCardSuccess$: Observable<Action> = this.actions$
+ .ofType(identificationCardActions.DELETE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Identification card is going to be deleted'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+
+}
diff --git a/src/app/customers/store/identityCards/effects/route.effects.ts b/src/app/customers/store/identityCards/effects/route.effects.ts
new file mode 100644
index 0000000..c5ada1d
--- /dev/null
+++ b/src/app/customers/store/identityCards/effects/route.effects.ts
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Action} from '@ngrx/store';
+import {Observable} from 'rxjs/Observable';
+import {Actions, Effect} from '@ngrx/effects';
+import {Router} from '@angular/router';
+import {Injectable} from '@angular/core';
+import * as identificationCardActions from '../identity-cards.actions';
+
+@Injectable()
+export class CustomerIdentificationCardRouteEffects {
+ @Effect({ dispatch: false })
+ createIdentificationCardSuccess$: Observable<Action> = this.actions$
+ .ofType(identificationCardActions.CREATE_SUCCESS, identificationCardActions.UPDATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute }));
+
+ @Effect({ dispatch: false })
+ deleteIdentificationCardSuccess$: Observable<Action> = this.actions$
+ .ofType(identificationCardActions.DELETE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../../../../../../'], { relativeTo: payload.activatedRoute }));
+
+ constructor(private actions$: Actions, private router: Router) { }
+
+}
diff --git a/src/app/customers/store/identityCards/effects/service.effects.ts b/src/app/customers/store/identityCards/effects/service.effects.ts
new file mode 100644
index 0000000..60fa27c
--- /dev/null
+++ b/src/app/customers/store/identityCards/effects/service.effects.ts
@@ -0,0 +1,85 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {of} from 'rxjs/observable/of';
+import {Action} from '@ngrx/store';
+import {Observable} from 'rxjs/Observable';
+import {Actions, Effect} from '@ngrx/effects';
+import {CustomerService} from '../../../../services/customer/customer.service';
+import {Injectable} from '@angular/core';
+import * as identificationCards from '../identity-cards.actions';
+
+@Injectable()
+export class CustomerIdentificationCardApiEffects {
+
+ @Effect()
+ loadAll$: Observable<Action> = this.actions$
+ .ofType(identificationCards.LOAD_ALL)
+ .debounceTime(300)
+ .map((action: identificationCards.LoadAllAction) => action.payload)
+ .switchMap(id => {
+ const nextSearch$ = this.actions$.ofType(identificationCards.LOAD_ALL).skip(1);
+
+ return this.customerService.fetchIdentificationCards(id)
+ .takeUntil(nextSearch$)
+ .map(identifications => new identificationCards.LoadAllCompleteAction(identifications))
+ .catch(() => of(new identificationCards.LoadAllCompleteAction([])));
+ });
+
+ @Effect()
+ createIdentificationCard$: Observable<Action> = this.actions$
+ .ofType(identificationCards.CREATE)
+ .map((action: identificationCards.CreateIdentityCardAction) => action.payload)
+ .mergeMap(payload =>
+ this.customerService.createIdentificationCard(payload.customerId, payload.identificationCard)
+ .map(() => new identificationCards.CreateIdentityCardSuccessAction({
+ resource: payload.identificationCard,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new identificationCards.CreateIdentityCardFailAction(error)))
+ );
+
+ @Effect()
+ updateIdentificationCard$: Observable<Action> = this.actions$
+ .ofType(identificationCards.UPDATE)
+ .map((action: identificationCards.UpdateIdentityCardAction) => action.payload)
+ .mergeMap(payload =>
+ this.customerService.updateIdentificationCard(payload.customerId, payload.identificationCard)
+ .map(() => new identificationCards.UpdateIdentityCardSuccessAction({
+ resource: payload.identificationCard,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new identificationCards.UpdateIdentityCardFailAction(error)))
+ );
+
+ @Effect()
+ deleteIdentificationCard$: Observable<Action> = this.actions$
+ .ofType(identificationCards.DELETE)
+ .map((action: identificationCards.DeleteIdentityCardAction) => action.payload)
+ .mergeMap(payload =>
+ this.customerService.deleteIdentificationCard(payload.customerId, payload.identificationCard.number)
+ .map(() => new identificationCards.DeleteIdentityCardSuccessAction({
+ resource: payload.identificationCard,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new identificationCards.DeleteIdentityCardFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private customerService: CustomerService) {}
+
+}
diff --git a/src/app/customers/store/identityCards/identity-cards.actions.ts b/src/app/customers/store/identityCards/identity-cards.actions.ts
new file mode 100644
index 0000000..91f84db
--- /dev/null
+++ b/src/app/customers/store/identityCards/identity-cards.actions.ts
@@ -0,0 +1,155 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {type} from '../../../store/util';
+import {RoutePayload} from '../../../common/store/route-payload';
+import {IdentificationCard} from '../../../services/customer/domain/identification-card.model';
+import {Action} from '@ngrx/store';
+import {
+ CreateResourceSuccessPayload,
+ DeleteResourceSuccessPayload,
+ LoadResourcePayload,
+ SelectResourcePayload,
+ UpdateResourceSuccessPayload
+} from '../../../common/store/resource.reducer';
+import {Error} from '../../../services/domain/error.model';
+
+export const LOAD_ALL = type('[Customer Identity Card] Load All');
+export const LOAD_ALL_COMPLETE = type('[Customer Identity Card] Load All Complete');
+
+export const LOAD = type('[Customer Identity Card] Load');
+export const SELECT = type('[Customer Identity Card] Select');
+
+export const CREATE = type('[Customer Identity Card] Create');
+export const CREATE_SUCCESS = type('[Customer Identity Card] Create Success');
+export const CREATE_FAIL = type('[Customer Identity Card] Create Fail');
+
+export const UPDATE = type('[Customer Identity Card] Update');
+export const UPDATE_SUCCESS = type('[Customer Identity Card] Update Success');
+export const UPDATE_FAIL = type('[Customer Identity Card] Update Fail');
+
+export const DELETE = type('[Customer Identity Card] Delete');
+export const DELETE_SUCCESS = type('[Customer Identity Card] Delete Success');
+export const DELETE_FAIL = type('[Customer Identity Card] Delete Fail');
+
+export const RESET_FORM = type('[Customer Identity Card] Reset Form');
+
+export interface IdentityCardPayload extends RoutePayload {
+ customerId: string;
+ identificationCard: IdentificationCard;
+}
+
+export class LoadAllAction implements Action {
+ readonly type = LOAD_ALL;
+
+ constructor(public payload: string) { }
+}
+
+export class LoadAllCompleteAction implements Action {
+ readonly type = LOAD_ALL_COMPLETE;
+
+ constructor(public payload: IdentificationCard[]) { }
+}
+
+export class LoadAction implements Action {
+ readonly type = LOAD;
+
+ constructor(public payload: LoadResourcePayload) { }
+}
+
+export class SelectAction implements Action {
+ readonly type = SELECT;
+
+ constructor(public payload: SelectResourcePayload) { }
+}
+
+export class CreateIdentityCardAction implements Action {
+ readonly type = CREATE;
+
+ constructor(public payload: IdentityCardPayload) { }
+}
+
+export class CreateIdentityCardSuccessAction implements Action {
+ readonly type = CREATE_SUCCESS;
+
+ constructor(public payload: CreateResourceSuccessPayload) { }
+}
+
+export class CreateIdentityCardFailAction implements Action {
+ readonly type = CREATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class UpdateIdentityCardAction implements Action {
+ readonly type = UPDATE;
+
+ constructor(public payload: IdentityCardPayload) { }
+}
+
+export class UpdateIdentityCardSuccessAction implements Action {
+ readonly type = UPDATE_SUCCESS;
+
+ constructor(public payload: UpdateResourceSuccessPayload) { }
+}
+
+export class UpdateIdentityCardFailAction implements Action {
+ readonly type = UPDATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class DeleteIdentityCardAction implements Action {
+ readonly type = DELETE;
+
+ constructor(public payload: IdentityCardPayload) { }
+}
+
+export class DeleteIdentityCardSuccessAction implements Action {
+ readonly type = DELETE_SUCCESS;
+
+ constructor(public payload: DeleteResourceSuccessPayload) { }
+}
+
+export class DeleteIdentityCardFailAction implements Action {
+ readonly type = DELETE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class ResetIdentityCardForm implements Action {
+ readonly type = RESET_FORM;
+
+ constructor() {}
+}
+
+export type Actions
+ = LoadAllAction
+ | LoadAllCompleteAction
+ | LoadAction
+ | SelectAction
+ | CreateIdentityCardAction
+ | CreateIdentityCardSuccessAction
+ | CreateIdentityCardFailAction
+ | UpdateIdentityCardAction
+ | UpdateIdentityCardSuccessAction
+ | UpdateIdentityCardFailAction
+ | DeleteIdentityCardAction
+ | DeleteIdentityCardSuccessAction
+ | DeleteIdentityCardFailAction
+ | ResetIdentityCardForm;
diff --git a/src/app/customers/store/identityCards/identity-cards.reducer.ts b/src/app/customers/store/identityCards/identity-cards.reducer.ts
new file mode 100644
index 0000000..87f33e8
--- /dev/null
+++ b/src/app/customers/store/identityCards/identity-cards.reducer.ts
@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as identityCards from './identity-cards.actions';
+import {ResourceState} from '../../../common/store/resource.reducer';
+import {IdentificationCard} from '../../../services/customer/domain/identification-card.model';
+import {idsToHashWithCurrentTimestamp, resourcesToHash} from '../../../common/store/reducer.helper';
+
+export const initialState: ResourceState = {
+ ids: [],
+ entities: {},
+ loadedAt: {},
+ selectedId: null,
+};
+
+export function reducer(state = initialState, action: identityCards.Actions): ResourceState {
+
+ switch (action.type) {
+
+ case identityCards.LOAD_ALL: {
+ return initialState;
+ }
+
+ case identityCards.LOAD_ALL_COMPLETE: {
+ const identificationCards: IdentificationCard[] = action.payload;
+
+ const ids = identificationCards.map(identificationCard => identificationCard.number);
+
+ const entities = resourcesToHash(identificationCards, 'number');
+
+ const loadedAt = idsToHashWithCurrentTimestamp(ids);
+
+ return {
+ ids: [ ...ids ],
+ entities: entities,
+ loadedAt: loadedAt,
+ selectedId: state.selectedId
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
diff --git a/src/app/customers/store/identityCards/scans/effects/notification.effects.ts b/src/app/customers/store/identityCards/scans/effects/notification.effects.ts
new file mode 100644
index 0000000..324ec6f
--- /dev/null
+++ b/src/app/customers/store/identityCards/scans/effects/notification.effects.ts
@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Actions, Effect} from '@ngrx/effects';
+import {Injectable} from '@angular/core';
+import {NotificationService, NotificationType} from '../../../../../services/notification/notification.service';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as identificationCardScanActions from '../scans.actions';
+
+@Injectable()
+export class CustomerIdentificationCardScanNotificationEffects {
+
+ @Effect({ dispatch: false })
+ createIdentificationCardScanSuccess$: Observable<Action> = this.actions$
+ .ofType(identificationCardScanActions.CREATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Scan is going to be uploaded'
+ }));
+
+ @Effect({ dispatch: false })
+ deleteIdentificationCardScanSuccess$: Observable<Action> = this.actions$
+ .ofType(identificationCardScanActions.DELETE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Scan is going to be deleted'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+
+}
diff --git a/src/app/customers/store/identityCards/scans/effects/route.effects.ts b/src/app/customers/store/identityCards/scans/effects/route.effects.ts
new file mode 100644
index 0000000..40a8549
--- /dev/null
+++ b/src/app/customers/store/identityCards/scans/effects/route.effects.ts
@@ -0,0 +1,36 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Router} from '@angular/router';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as identificationCardScanActions from '../scans.actions';
+
+@Injectable()
+export class CustomerIdentificationCardScanRouteEffects {
+
+ @Effect({ dispatch: false })
+ createIdentificationCardScanSuccess$: Observable<Action> = this.actions$
+ .ofType(identificationCardScanActions.CREATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute }));
+
+ constructor(private actions$: Actions, private router: Router) { }
+}
diff --git a/src/app/customers/store/identityCards/scans/effects/service.effects.ts b/src/app/customers/store/identityCards/scans/effects/service.effects.ts
new file mode 100644
index 0000000..5079f47
--- /dev/null
+++ b/src/app/customers/store/identityCards/scans/effects/service.effects.ts
@@ -0,0 +1,73 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {of} from 'rxjs/observable/of';
+import {Injectable} from '@angular/core';
+import {CustomerService} from '../../../../../services/customer/customer.service';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as identificationCardScans from '../scans.actions';
+
+@Injectable()
+export class CustomerIdentificationCardScanApiEffects {
+
+ @Effect()
+ loadAll$: Observable<Action> = this.actions$
+ .ofType(identificationCardScans.LOAD_ALL)
+ .debounceTime(300)
+ .map((action: identificationCardScans.LoadAllAction) => action.payload)
+ .switchMap(payload => {
+ const nextSearch$ = this.actions$.ofType(identificationCardScans.LOAD_ALL).skip(1);
+
+ return this.customerService.fetchIdentificationCardScans(payload.customerIdentifier, payload.identificationCardNumber)
+ .takeUntil(nextSearch$)
+ .map(scans => new identificationCardScans.LoadAllCompleteAction(scans))
+ .catch(() => of(new identificationCardScans.LoadAllCompleteAction([])));
+ });
+
+ @Effect()
+ createIdentificationCardScan$: Observable<Action> = this.actions$
+ .ofType(identificationCardScans.CREATE)
+ .map((action: identificationCardScans.CreateIdentityCardScanAction) => action.payload)
+ .mergeMap(payload =>
+ this.customerService.uploadIdentificationCardScan(payload.customerIdentifier, payload.identificationCardNumber,
+ payload.scan, payload.file)
+ .map(() => new identificationCardScans.CreateIdentityCardScanSuccessAction({
+ resource: payload.scan,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new identificationCardScans.CreateIdentityCardScanFailAction(error)))
+ );
+
+ @Effect()
+ deleteIdentificationCardScan$: Observable<Action> = this.actions$
+ .ofType(identificationCardScans.DELETE)
+ .map((action: identificationCardScans.DeleteIdentityCardScanAction) => action.payload)
+ .mergeMap(payload =>
+ this.customerService.deleteIdentificationCardScan(payload.customerIdentifier, payload.identificationCardNumber,
+ payload.scan.identifier)
+ .map(() => new identificationCardScans.DeleteIdentityCardScanSuccessAction({
+ resource: payload.scan,
+ activatedRoute: undefined
+ }))
+ .catch((error) => of(new identificationCardScans.DeleteIdentityCardScanFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private customerService: CustomerService) {}
+}
diff --git a/src/app/customers/store/identityCards/scans/scans.actions.ts b/src/app/customers/store/identityCards/scans/scans.actions.ts
new file mode 100644
index 0000000..6b1dcb5
--- /dev/null
+++ b/src/app/customers/store/identityCards/scans/scans.actions.ts
@@ -0,0 +1,119 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {type} from '../../../../store/util';
+import {IdentificationCardScan} from '../../../../services/customer/domain/identification-card-scan.model';
+import {Action} from '@ngrx/store';
+import {CreateResourceSuccessPayload, DeleteResourceSuccessPayload} from '../../../../common/store/resource.reducer';
+import {RoutePayload} from '../../../../common/store/route-payload';
+
+export const LOAD_ALL = type('[Customer Identity Card Scan] Load All');
+export const LOAD_ALL_COMPLETE = type('[Customer Identity Card Scan] Load All Complete');
+
+export const CREATE = type('[Customer Identity Card Scan] Create');
+export const CREATE_SUCCESS = type('[Customer Identity Card Scan] Create Success');
+export const CREATE_FAIL = type('[Customer Identity Card Scan] Create Fail');
+
+export const DELETE = type('[Customer Identity Card Scan] Delete');
+export const DELETE_SUCCESS = type('[Customer Identity Card Scan] Delete Success');
+export const DELETE_FAIL = type('[Customer Identity Card Scan] Delete Fail');
+
+export const RESET_FORM = type('[Customer Identity Card Scan] Reset Form');
+
+export interface LoadAllPayload {
+ customerIdentifier: string;
+ identificationCardNumber: string;
+}
+
+export interface IdentityCardScanPayload extends RoutePayload {
+ customerIdentifier: string;
+ identificationCardNumber: string;
+ scan: IdentificationCardScan;
+ file: File;
+}
+
+export interface DeleteIdentityCardScanPayload {
+ customerIdentifier: string;
+ identificationCardNumber: string;
+ scan: IdentificationCardScan;
+}
+
+export class LoadAllAction implements Action {
+ readonly type = LOAD_ALL;
+
+ constructor(public payload: LoadAllPayload) { }
+}
+
+export class LoadAllCompleteAction implements Action {
+ readonly type = LOAD_ALL_COMPLETE;
+
+ constructor(public payload: IdentificationCardScan[]) { }
+}
+
+export class CreateIdentityCardScanAction implements Action {
+ readonly type = CREATE;
+
+ constructor(public payload: IdentityCardScanPayload) { }
+}
+
+export class CreateIdentityCardScanSuccessAction implements Action {
+ readonly type = CREATE_SUCCESS;
+
+ constructor(public payload: CreateResourceSuccessPayload) { }
+}
+
+export class CreateIdentityCardScanFailAction implements Action {
+ readonly type = CREATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class DeleteIdentityCardScanAction implements Action {
+ readonly type = DELETE;
+
+ constructor(public payload: DeleteIdentityCardScanPayload) { }
+}
+
+export class DeleteIdentityCardScanSuccessAction implements Action {
+ readonly type = DELETE_SUCCESS;
+
+ constructor(public payload: DeleteResourceSuccessPayload) { }
+}
+
+export class DeleteIdentityCardScanFailAction implements Action {
+ readonly type = DELETE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class ResetIdentityCardScanForm implements Action {
+ readonly type = RESET_FORM;
+
+ constructor() {}
+}
+
+export type Actions
+ = LoadAllAction
+ | LoadAllCompleteAction
+ | CreateIdentityCardScanAction
+ | CreateIdentityCardScanSuccessAction
+ | CreateIdentityCardScanFailAction
+ | DeleteIdentityCardScanAction
+ | DeleteIdentityCardScanSuccessAction
+ | DeleteIdentityCardScanFailAction
+ | ResetIdentityCardScanForm;
diff --git a/src/app/customers/store/identityCards/scans/scans.reducer.ts b/src/app/customers/store/identityCards/scans/scans.reducer.ts
new file mode 100644
index 0000000..30ae17e
--- /dev/null
+++ b/src/app/customers/store/identityCards/scans/scans.reducer.ts
@@ -0,0 +1,57 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ResourceState} from '../../../../common/store/resource.reducer';
+import {IdentificationCardScan} from '../../../../services/customer/domain/identification-card-scan.model';
+import {idsToHashWithCurrentTimestamp, resourcesToHash} from '../../../../common/store/reducer.helper';
+import * as identityCardScans from './scans.actions';
+
+export const initialState: ResourceState = {
+ ids: [],
+ entities: {},
+ loadedAt: {},
+ selectedId: null,
+};
+
+export function reducer(state = initialState, action: identityCardScans.Actions): ResourceState {
+
+ switch (action.type) {
+
+ case identityCardScans.LOAD_ALL: {
+ return initialState;
+ }
+
+ case identityCardScans.LOAD_ALL_COMPLETE: {
+ const cardScans: IdentificationCardScan[] = action.payload;
+ const ids = cardScans.map(scan => scan.identifier);
+ const entities = resourcesToHash(cardScans);
+ const loadedAt = idsToHashWithCurrentTimestamp(ids);
+
+ return {
+ ids: [ ...ids ],
+ entities,
+ loadedAt,
+ selectedId: state.selectedId
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
diff --git a/src/app/customers/store/index.ts b/src/app/customers/store/index.ts
new file mode 100644
index 0000000..8662cb3
--- /dev/null
+++ b/src/app/customers/store/index.ts
@@ -0,0 +1,153 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as fromRoot from '../../store';
+import * as fromCustomers from './customers.reducer';
+import * as fromCustomerTasks from './customerTasks/customer-tasks.reducer';
+import * as fromCustomerIdentificationCards from './identityCards/identity-cards.reducer';
+import * as fromCatalogs from './catalogs/catalog.reducer';
+import * as fromCommands from './commands/commands.reducer';
+import * as fromScans from './identityCards/scans/scans.reducer';
+import * as fromTasks from './tasks/tasks.reducer';
+import * as fromPayrollDistribution from './payroll/payroll.reducer';
+
+import {ActionReducer, Store} from '@ngrx/store';
+import {createReducer} from '../../store/index';
+import {createSelector} from 'reselect';
+import {
+ createResourceReducer,
+ getResourceAll,
+ getResourceLoadedAt,
+ getResourceSelected,
+ ResourceState
+} from '../../common/store/resource.reducer';
+import {createFormReducer, FormState, getFormError} from '../../common/store/form.reducer';
+
+export interface State extends fromRoot.State {
+ customers: ResourceState;
+ customerForm: FormState;
+ tasks: ResourceState;
+ taskForm: FormState;
+ customerTasks: fromCustomerTasks.State;
+ customerCatalog: fromCatalogs.State;
+ customerCommands: fromCommands.State;
+ customerIdentificationCards: ResourceState;
+ customerIdentificationCardForm: FormState;
+ customerIdentificationCardScans: ResourceState;
+ customerIdentificationCardScanForm: FormState;
+ customerPayrollDistribution: fromPayrollDistribution.State;
+}
+
+const reducers = {
+ customers: createResourceReducer('Customer', fromCustomers.reducer),
+ customerForm: createFormReducer('Customer'),
+ tasks: createResourceReducer('Task', fromTasks.reducer),
+ taskForm: createFormReducer('Task'),
+ customerTasks: fromCustomerTasks.reducer,
+ customerCatalog: fromCatalogs.reducer,
+ customerCommands: fromCommands.reducer,
+ customerIdentificationCards: createResourceReducer('Customer Identity Card', fromCustomerIdentificationCards.reducer, 'number'),
+ customerIdentificationCardForm: createFormReducer('Customer Identity Card'),
+ customerIdentificationCardScans: createResourceReducer('Customer Identity Card Scan', fromScans.reducer),
+ customerIdentificationCardScanForm: createFormReducer('Customer Identity Card Scan'),
+ customerPayrollDistribution: fromPayrollDistribution.reducer
+};
+
+export class CustomersStore extends Store<State> {}
+
+export const customerModuleReducer: ActionReducer<State> = createReducer(reducers);
+
+export function customerStoreFactory(appStore: Store<fromRoot.State>) {
+ appStore.replaceReducer(customerModuleReducer);
+ return appStore;
+}
+
+export const getCustomersState = (state: State) => state.customers;
+
+export const getCustomerFormState = (state: State) => state.customerForm;
+export const getCustomerFormError = createSelector(getCustomerFormState, getFormError);
+
+export const getCustomerLoadedAt = createSelector(getCustomersState, getResourceLoadedAt);
+export const getSelectedCustomer = createSelector(getCustomersState, getResourceSelected);
+
+/**
+ * Task Selectors
+ */
+export const getTasksState = (state: State) => state.tasks;
+
+export const getAllTaskEntities = createSelector(getTasksState, getResourceAll);
+
+export const getTaskLoadedAt = createSelector(getTasksState, getResourceLoadedAt);
+export const getSelectedTask = createSelector(getTasksState, getResourceSelected);
+
+/**
+ * Customer Task Selectors
+ */
+export const getCustomerTaskCommandsState = (state: State) => state.customerTasks;
+
+export const getCustomerTaskProcessSteps = createSelector(getCustomerTaskCommandsState, fromCustomerTasks.getProcessSteps);
+
+
+/**
+ * Customer Command Selectors
+ */
+
+export const getCustomerCommandsState = (state: State) => state.customerCommands;
+
+export const getAllCustomerCommands = createSelector(getCustomerCommandsState, fromCommands.getCommands);
+
+/**
+ * Customer Identification Card Selectors
+ */
+export const getCustomerIdentificationCardsState = (state: State) => state.customerIdentificationCards;
+
+export const getAllCustomerIdentificationCardEntities = createSelector(getCustomerIdentificationCardsState, getResourceAll);
+
+export const getCustomerIdentificationCardFormState = (state: State) => state.customerIdentificationCardForm;
+export const getCustomerIdentificationCardFormError = createSelector(getCustomerIdentificationCardFormState, getFormError);
+
+export const getIdentificationCardLoadedAt = createSelector(getCustomerIdentificationCardsState, getResourceLoadedAt);
+export const getSelectedIdentificationCard = createSelector(getCustomerIdentificationCardsState, getResourceSelected);
+
+/**
+ * Customer Identification Card Scan Selectors
+ */
+export const getIdentificationCardScansState = (state: State) => state.customerIdentificationCardScans;
+
+export const getAllIdentificationCardScanEntities = createSelector(getIdentificationCardScansState, getResourceAll);
+
+export const getCustomerIdentificationCardScanFormState = (state: State) => state.customerIdentificationCardScanForm;
+export const getCustomerIdentificationCardScanFormError = createSelector(getCustomerIdentificationCardScanFormState, getFormError);
+
+/**
+ * Customer Payroll Distribution Selectors
+ */
+export const getPayrollDistributionState = (state: State) => state.customerPayrollDistribution;
+
+export const getPayrollDistribution = createSelector(getPayrollDistributionState, fromPayrollDistribution.getPayrollDistribution);
+export const getPayrollDistributionLoadedAt = createSelector(getPayrollDistributionState,
+ fromPayrollDistribution.getPayrollDistributionLoadedAt);
+
+/**
+ * Customer Catalog Selectors
+ */
+export const getCustomerCatalogState = (state: State) => state.customerCatalog;
+
+export const getCustomerCatalog = createSelector(getCustomerCatalogState, fromCatalogs.getCustomerCatalog);
+export const getCustomerCatalogLoadedAt = createSelector(getCustomerCatalogState, fromCatalogs.getCustomerCatalogLoadedAt);
+export const getSelectedField = createSelector(getCustomerCatalogState, fromCatalogs.getSelectedField);
diff --git a/src/app/customers/store/payroll/effects/notification.effects.ts b/src/app/customers/store/payroll/effects/notification.effects.ts
new file mode 100644
index 0000000..281a210
--- /dev/null
+++ b/src/app/customers/store/payroll/effects/notification.effects.ts
@@ -0,0 +1,39 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {NotificationService, NotificationType} from '../../../../services/notification/notification.service';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as payrollActions from '../payroll.actions';
+
+@Injectable()
+export class CustomerPayrollNotificationEffects {
+
+ @Effect({ dispatch: false })
+ updatePayrollSuccess$: Observable<Action> = this.actions$
+ .ofType(payrollActions.UPDATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Payroll is going to be saved'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+
+}
diff --git a/src/app/customers/store/payroll/effects/route.effects.ts b/src/app/customers/store/payroll/effects/route.effects.ts
new file mode 100644
index 0000000..fda200f
--- /dev/null
+++ b/src/app/customers/store/payroll/effects/route.effects.ts
@@ -0,0 +1,36 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Router} from '@angular/router';
+import {Observable} from 'rxjs/Observable';
+import * as payrollActions from '../payroll.actions';
+import {Action} from '@ngrx/store';
+
+@Injectable()
+export class CustomerPayrollRouteEffects {
+ @Effect({ dispatch: false })
+ updatePayrollSuccess$: Observable<Action> = this.actions$
+ .ofType(payrollActions.UPDATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute }));
+
+ constructor(private actions$: Actions, private router: Router) { }
+
+}
diff --git a/src/app/customers/store/payroll/effects/service.effects.ts b/src/app/customers/store/payroll/effects/service.effects.ts
new file mode 100644
index 0000000..4466b8a
--- /dev/null
+++ b/src/app/customers/store/payroll/effects/service.effects.ts
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import * as payrollActions from '../payroll.actions';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+import {PayrollService} from '../../../../services/payroll/payroll.service';
+
+@Injectable()
+export class CustomerPayrollApiEffects {
+
+ @Effect()
+ updatePayroll$: Observable<Action> = this.actions$
+ .ofType(payrollActions.UPDATE)
+ .map((action: payrollActions.UpdatePayrollDistributionAction) => action.payload)
+ .mergeMap(payload =>
+ this.payrollService.setPayrollConfiguration(payload.customerId, payload.distribution)
+ .map(() => new payrollActions.UpdatePayrollDistributionSuccessAction(payload))
+ .catch((error) => of(new payrollActions.UpdatePayrollDistributionFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private payrollService: PayrollService) { }
+
+}
diff --git a/src/app/customers/store/payroll/payroll.actions.ts b/src/app/customers/store/payroll/payroll.actions.ts
new file mode 100644
index 0000000..662646e
--- /dev/null
+++ b/src/app/customers/store/payroll/payroll.actions.ts
@@ -0,0 +1,63 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {type} from '../../../store/util';
+import {Action} from '@ngrx/store';
+import {RoutePayload} from '../../../common/store/route-payload';
+import {PayrollConfiguration} from '../../../services/payroll/domain/payroll-configuration.model';
+
+export const LOAD = type('[Customer Payroll] Load');
+
+export const UPDATE = type('[Customer Payroll] Update');
+export const UPDATE_SUCCESS = type('[Customer Payroll] Update Success');
+export const UPDATE_FAIL = type('[Customer Payroll] Update Fail');
+
+export interface PayrollDistributionRoutePayload extends RoutePayload {
+ customerId: string;
+ distribution: PayrollConfiguration;
+}
+
+export class LoadAction implements Action {
+ readonly type = LOAD;
+
+ constructor(public payload: PayrollConfiguration) { }
+}
+
+export class UpdatePayrollDistributionAction implements Action {
+ readonly type = UPDATE;
+
+ constructor(public payload: PayrollDistributionRoutePayload) { }
+}
+
+export class UpdatePayrollDistributionSuccessAction implements Action {
+ readonly type = UPDATE_SUCCESS;
+
+ constructor(public payload: PayrollDistributionRoutePayload) { }
+}
+
+export class UpdatePayrollDistributionFailAction implements Action {
+ readonly type = UPDATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export type Actions
+ = LoadAction
+ | UpdatePayrollDistributionAction
+ | UpdatePayrollDistributionSuccessAction
+ | UpdatePayrollDistributionFailAction;
diff --git a/src/app/customers/store/payroll/payroll.reducer.ts b/src/app/customers/store/payroll/payroll.reducer.ts
new file mode 100644
index 0000000..619ec31
--- /dev/null
+++ b/src/app/customers/store/payroll/payroll.reducer.ts
@@ -0,0 +1,62 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {PayrollConfiguration} from '../../../services/payroll/domain/payroll-configuration.model';
+import * as payrollActions from './payroll.actions';
+import {PayrollDistributionRoutePayload} from './payroll.actions';
+
+export interface State {
+ distribution: PayrollConfiguration;
+ loadedAt: number;
+}
+
+const initialState: State = {
+ distribution: null,
+ loadedAt: null
+};
+
+export function reducer(state: State = initialState, action: payrollActions.Actions): State {
+
+ switch (action.type) {
+
+ case payrollActions.LOAD: {
+ const distribution: PayrollConfiguration = action.payload;
+
+ return {
+ distribution,
+ loadedAt: Date.now()
+ };
+ }
+
+ case payrollActions.UPDATE_SUCCESS: {
+ const payload: PayrollDistributionRoutePayload = action.payload;
+
+ return {
+ distribution: payload.distribution,
+ loadedAt: state.loadedAt
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+export const getPayrollDistribution = (state: State) => state.distribution;
+export const getPayrollDistributionLoadedAt = (state: State) => state.loadedAt;
diff --git a/src/app/customers/store/tasks/effects/notification.effects.ts b/src/app/customers/store/tasks/effects/notification.effects.ts
new file mode 100644
index 0000000..a049100
--- /dev/null
+++ b/src/app/customers/store/tasks/effects/notification.effects.ts
@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {NotificationService, NotificationType} from '../../../../services/notification/notification.service';
+import {Observable} from 'rxjs/Observable';
+import {Actions, Effect} from '@ngrx/effects';
+import {Injectable} from '@angular/core';
+import {Action} from '@ngrx/store';
+import * as taskActions from '../task.actions';
+
+@Injectable()
+export class TasksNotificationEffects {
+
+ @Effect({dispatch: false})
+ createTaskSuccess$: Observable<Action> = this.actions$
+ .ofType(taskActions.CREATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Task is going to be saved'
+ }));
+
+ @Effect({dispatch: false})
+ updateTaskSuccess$: Observable<Action> = this.actions$
+ .ofType(taskActions.UPDATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Task is going to be updated'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+
+}
diff --git a/src/app/customers/store/tasks/effects/route.effects.ts b/src/app/customers/store/tasks/effects/route.effects.ts
new file mode 100644
index 0000000..0a510b3
--- /dev/null
+++ b/src/app/customers/store/tasks/effects/route.effects.ts
@@ -0,0 +1,36 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Observable} from 'rxjs/Observable';
+import {Actions, Effect} from '@ngrx/effects';
+import {Action} from '@ngrx/store';
+import {Router} from '@angular/router';
+import {Injectable} from '@angular/core';
+import * as taskActions from '../task.actions';
+
+@Injectable()
+export class TasksRouteEffects {
+
+ @Effect({ dispatch: false })
+ createCustomerTaskSuccess$: Observable<Action> = this.actions$
+ .ofType(taskActions.CREATE_SUCCESS, taskActions.UPDATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute }));
+
+ constructor(private actions$: Actions, private router: Router) { }
+}
diff --git a/src/app/customers/store/tasks/effects/service.effects.ts b/src/app/customers/store/tasks/effects/service.effects.ts
new file mode 100644
index 0000000..0319df4
--- /dev/null
+++ b/src/app/customers/store/tasks/effects/service.effects.ts
@@ -0,0 +1,71 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {CustomerService} from '../../../../services/customer/customer.service';
+import * as taskActions from '../task.actions';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+
+@Injectable()
+export class TasksApiEffects {
+
+ @Effect()
+ loadAll$: Observable<Action> = this.actions$
+ .ofType(taskActions.LOAD_ALL)
+ .debounceTime(300)
+ .map((action: taskActions.LoadAllAction) => action.payload)
+ .switchMap(() => {
+ const nextSearch$ = this.actions$.ofType(taskActions.LOAD_ALL).skip(1);
+
+ return this.customerService.fetchTasks()
+ .takeUntil(nextSearch$)
+ .map(taskPage => new taskActions.LoadAllCompleteAction(taskPage))
+ .catch(() => of(new taskActions.LoadAllCompleteAction([])));
+ });
+
+ @Effect()
+ createTask$: Observable<Action> = this.actions$
+ .ofType(taskActions.CREATE)
+ .map((action: taskActions.CreateTaskAction) => action.payload)
+ .mergeMap(payload =>
+ this.customerService.createTask(payload.task)
+ .map(() => new taskActions.CreateTaskSuccessAction({
+ resource: payload.task,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new taskActions.CreateTaskFailAction(error)))
+ );
+
+ @Effect()
+ updateTask$: Observable<Action> = this.actions$
+ .ofType(taskActions.UPDATE)
+ .map((action: taskActions.CreateTaskAction) => action.payload)
+ .mergeMap(payload =>
+ this.customerService.updateTask(payload.task)
+ .map(() => new taskActions.UpdateTaskSuccessAction({
+ resource: payload.task,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new taskActions.UpdateTaskFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private customerService: CustomerService) {}
+}
diff --git a/src/app/customers/store/tasks/task.actions.ts b/src/app/customers/store/tasks/task.actions.ts
new file mode 100644
index 0000000..5c11a1c
--- /dev/null
+++ b/src/app/customers/store/tasks/task.actions.ts
@@ -0,0 +1,152 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {type} from '../../../store/util';
+import {Action} from '@ngrx/store';
+import {TaskDefinition} from '../../../services/customer/domain/task-definition.model';
+import {
+ CreateResourceSuccessPayload,
+ LoadResourcePayload,
+ SelectResourcePayload,
+ UpdateResourceSuccessPayload
+} from '../../../common/store/resource.reducer';
+import {RoutePayload} from '../../../common/store/route-payload';
+
+export const LOAD_ALL = type('[Task] Load All');
+export const LOAD_ALL_COMPLETE = type('[Task] Load All Complete');
+
+export const LOAD = type('[Task] Load');
+export const SELECT = type('[Task] Select');
+
+export const CREATE = type('[Task] Create');
+export const CREATE_SUCCESS = type('[Task] Create Success');
+export const CREATE_FAIL = type('[Task] Create Fail');
+
+export const UPDATE = type('[Task] Update');
+export const UPDATE_SUCCESS = type('[Task] Update Success');
+export const UPDATE_FAIL = type('[Task] Update Fail');
+
+export const DELETE = type('[Task] Delete');
+export const DELETE_SUCCESS = type('[Task] Delete Success');
+export const DELETE_FAIL = type('[Task] Delete Fail');
+
+export const RESET_FORM = type('[Task] Reset Form');
+
+export interface TaskRoutePayload extends RoutePayload {
+ task: TaskDefinition;
+}
+
+export class LoadAllAction implements Action {
+ readonly type = LOAD_ALL;
+
+ constructor(public payload: string) { }
+}
+
+export class LoadAllCompleteAction implements Action {
+ readonly type = LOAD_ALL_COMPLETE;
+
+ constructor(public payload: TaskDefinition[]) { }
+}
+
+export class LoadAction implements Action {
+ readonly type = LOAD;
+
+ constructor(public payload: LoadResourcePayload) { }
+}
+
+export class SelectAction implements Action {
+ readonly type = SELECT;
+
+ constructor(public payload: SelectResourcePayload) { }
+}
+
+export class CreateTaskAction implements Action {
+ readonly type = CREATE;
+
+ constructor(public payload: TaskRoutePayload) { }
+}
+
+export class CreateTaskSuccessAction implements Action {
+ readonly type = CREATE_SUCCESS;
+
+ constructor(public payload: CreateResourceSuccessPayload) { }
+}
+
+export class CreateTaskFailAction implements Action {
+ readonly type = CREATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class UpdateTaskAction implements Action {
+ readonly type = UPDATE;
+
+ constructor(public payload: TaskRoutePayload) { }
+}
+
+export class UpdateTaskSuccessAction implements Action {
+ readonly type = UPDATE_SUCCESS;
+
+ constructor(public payload: UpdateResourceSuccessPayload) { }
+}
+
+export class UpdateTaskFailAction implements Action {
+ readonly type = UPDATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class DeleteTaskAction implements Action {
+ readonly type = DELETE;
+
+ constructor(public payload: TaskRoutePayload) { }
+}
+
+export class DeleteTaskSuccessAction implements Action {
+ readonly type = DELETE_SUCCESS;
+
+ constructor(public payload: UpdateResourceSuccessPayload) { }
+}
+
+export class DeleteTaskFailAction implements Action {
+ readonly type = DELETE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class ResetTaskFormAction implements Action {
+ readonly type = RESET_FORM;
+
+ constructor() {}
+}
+
+export type Actions
+ = LoadAllAction
+ | LoadAllCompleteAction
+ | LoadAction
+ | SelectAction
+ | CreateTaskAction
+ | CreateTaskSuccessAction
+ | CreateTaskFailAction
+ | UpdateTaskAction
+ | UpdateTaskSuccessAction
+ | UpdateTaskFailAction
+ | DeleteTaskAction
+ | DeleteTaskSuccessAction
+ | DeleteTaskFailAction
+ | ResetTaskFormAction;
diff --git a/src/app/customers/store/tasks/tasks.reducer.ts b/src/app/customers/store/tasks/tasks.reducer.ts
new file mode 100644
index 0000000..6c02922
--- /dev/null
+++ b/src/app/customers/store/tasks/tasks.reducer.ts
@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ResourceState} from '../../../common/store/resource.reducer';
+import {TaskDefinition} from '../../../services/customer/domain/task-definition.model';
+import {idsToHashWithCurrentTimestamp, resourcesToHash} from '../../../common/store/reducer.helper';
+import * as task from './task.actions';
+
+export const initialState: ResourceState = {
+ ids: [],
+ entities: {},
+ loadedAt: {},
+ selectedId: null,
+};
+
+export function reducer(state = initialState, action: task.Actions): ResourceState {
+
+ switch (action.type) {
+
+ case task.LOAD_ALL: {
+ return initialState;
+ }
+
+ case task.LOAD_ALL_COMPLETE: {
+ const taskDefinitions: TaskDefinition[] = action.payload;
+
+ const ids = taskDefinitions.map(task => task.identifier);
+
+ const entities = resourcesToHash(taskDefinitions);
+
+ const loadedAt = idsToHashWithCurrentTimestamp(ids);
+
+ return {
+ ids: [ ...ids ],
+ entities: entities,
+ loadedAt: loadedAt,
+ selectedId: state.selectedId
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
diff --git a/src/app/customers/tasks/domain/command-options.model.ts b/src/app/customers/tasks/domain/command-options.model.ts
new file mode 100644
index 0000000..020bb46
--- /dev/null
+++ b/src/app/customers/tasks/domain/command-options.model.ts
@@ -0,0 +1,30 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {TaskDefinitionCommand} from '../../../services/customer/domain/task-definition.model';
+
+interface CommandOption {
+ command: TaskDefinitionCommand;
+ label: string;
+}
+
+export const defaultCommandOptions: CommandOption[] = [
+ { command: 'ACTIVATE', label: 'is activated' },
+ { command: 'UNLOCK', label: 'is unlocked' },
+ { command: 'REOPEN', label: 'is reopened' }
+];
diff --git a/src/app/customers/tasks/domain/type-options.model.ts b/src/app/customers/tasks/domain/type-options.model.ts
new file mode 100644
index 0000000..8702333
--- /dev/null
+++ b/src/app/customers/tasks/domain/type-options.model.ts
@@ -0,0 +1,30 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {TaskDefinitionType} from '../../../services/customer/domain/task-definition.model';
+
+interface TypeOption {
+ type: TaskDefinitionType;
+ label: string;
+}
+
+export const defaultTypeOptions: TypeOption[] = [
+ { type: 'ID_CARD', label: 'Identification card' },
+ { type: 'FOUR_EYES', label: 'Four eyes' },
+ { type: 'CUSTOM', label: 'Custom' }
+];
diff --git a/src/app/customers/tasks/form/create.form.component.html b/src/app/customers/tasks/form/create.form.component.html
new file mode 100644
index 0000000..bfa84f5
--- /dev/null
+++ b/src/app/customers/tasks/form/create.form.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Create new task definition' | translate}}">
+ <fims-task-form-component
+ #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [task]="task">
+ </fims-task-form-component>
+</fims-layout-card-over>
diff --git a/src/app/customers/tasks/form/create.form.component.ts b/src/app/customers/tasks/form/create.form.component.ts
new file mode 100644
index 0000000..a935d41
--- /dev/null
+++ b/src/app/customers/tasks/form/create.form.component.ts
@@ -0,0 +1,58 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component} from '@angular/core';
+import {TaskDefinition} from '../../../services/customer/domain/task-definition.model';
+import {ActivatedRoute, Router} from '@angular/router';
+import {CustomersStore} from '../../store/index';
+import {CREATE} from '../../store/tasks/task.actions';
+
+@Component({
+ templateUrl: './create.form.component.html'
+})
+export class TaskCreateFormComponent {
+
+ task: TaskDefinition = {
+ identifier: '',
+ name: '',
+ description: '',
+ type: 'ID_CARD',
+ commands: [
+ 'ACTIVATE'
+ ],
+ mandatory: false,
+ predefined: false
+ };
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: CustomersStore) {}
+
+ onSave(task: TaskDefinition): void {
+ this.store.dispatch({ type: CREATE, payload: {
+ task,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel(): void {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/customers/tasks/form/edit.form.component.html b/src/app/customers/tasks/form/edit.form.component.html
new file mode 100644
index 0000000..bee6675
--- /dev/null
+++ b/src/app/customers/tasks/form/edit.form.component.html
@@ -0,0 +1,26 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Edit task definition' | translate}}">
+ <fims-task-form-component
+ #form
+ [editMode]="true"
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [task]="task$ | async">
+ </fims-task-form-component>
+</fims-layout-card-over>
diff --git a/src/app/customers/tasks/form/edit.form.component.ts b/src/app/customers/tasks/form/edit.form.component.ts
new file mode 100644
index 0000000..f73c2db
--- /dev/null
+++ b/src/app/customers/tasks/form/edit.form.component.ts
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component} from '@angular/core';
+import {TaskDefinition} from '../../../services/customer/domain/task-definition.model';
+import {Observable} from 'rxjs/Observable';
+import {ActivatedRoute, Router} from '@angular/router';
+import * as fromCustomer from '../../store/index';
+import {CustomersStore} from '../../store/index';
+import {UPDATE} from '../../store/tasks/task.actions';
+
+@Component({
+ templateUrl: './edit.form.component.html'
+})
+export class TaskEditFormComponent {
+
+ task$: Observable<TaskDefinition>;
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: CustomersStore) {
+ this.task$ = this.store.select(fromCustomer.getSelectedTask);
+ }
+
+ onSave(task: TaskDefinition): void {
+ this.store.dispatch({ type: UPDATE, payload: {
+ task,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel(): void {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/customers/tasks/form/form.component.html b/src/app/customers/tasks/form/form.component.html
new file mode 100644
index 0000000..3e133fa
--- /dev/null
+++ b/src/app/customers/tasks/form/form.component.html
@@ -0,0 +1,65 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Task details' | translate}}" [state]="form.valid ? 'complete' : form.pristine ? 'none' : 'required'">
+ <form [formGroup]="form" layout="column">
+ <fims-id-input flex [form]="form" controlName="identifier" [readonly]="editMode"></fims-id-input>
+ <fims-text-input [form]="form" controlName="name" placeholder="{{'Name' | translate}}"></fims-text-input>
+ <mat-form-field layout-margin flex>
+ <textarea matInput placeholder="{{'Description' | translate}}" formControlName="description"></textarea>
+ <mat-error *ngIf="form.get('description').hasError('required')" translate>
+ Required
+ </mat-error>
+ </mat-form-field>
+ <mat-radio-group formControlName="type">
+ <mat-radio-button *ngFor="let type of typeOptions" [value]="type.type" layout-margin>
+ {{type.label}}
+ </mat-radio-button>
+ </mat-radio-group>
+ <mat-checkbox formControlName="mandatory" layout-margin translate>
+ Mandatory
+ </mat-checkbox>
+ <mat-checkbox formControlName="predefined" layout-margin translate>
+ Automatically assign this task when member is created
+ </mat-checkbox>
+ <div layout-gt-xs="column" layout-margin formArrayName="commands">
+ <h4 translate>Task needs to be executed, before member </h4>
+ <div *ngFor="let test of commands; let i=index" layout="row" [formGroupName]="i">
+ <mat-form-field>
+ <mat-select formControlName="command">
+ <mat-option *ngFor="let commandOption of commandOptions" [value]="commandOption.command">
+ {{commandOption.label}}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ <button mat-button (click)="removeCommand(i)" [disabled]="i === 0">{{'Remove' | translate}}</button>
+ </div>
+ <button mat-button (click)="addCommand()">{{'Add trigger' | translate}}</button>
+ </div>
+ </form>
+ <ng-template td-step-actions>
+ <fims-form-final-action
+ [resourceName]="'TASK'"
+ [editMode]="editMode"
+ [disabled]="!form.valid"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/customers/tasks/form/form.component.ts b/src/app/customers/tasks/form/form.component.ts
new file mode 100644
index 0000000..4d5d051
--- /dev/null
+++ b/src/app/customers/tasks/form/form.component.ts
@@ -0,0 +1,128 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild} from '@angular/core';
+import {TaskDefinition, TaskDefinitionCommand} from '../../../services/customer/domain/task-definition.model';
+import {AbstractControl, FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {FimsValidators} from '../../../common/validator/validators';
+import {TdStepComponent} from '@covalent/core';
+import {defaultTypeOptions} from '../domain/type-options.model';
+import {defaultCommandOptions} from '../domain/command-options.model';
+
+@Component({
+ selector: 'fims-task-form-component',
+ templateUrl: './form.component.html'
+})
+export class TaskFormComponent implements OnInit, OnChanges {
+
+ form: FormGroup;
+
+ typeOptions = defaultTypeOptions;
+
+ commandOptions = defaultCommandOptions;
+
+ @ViewChild('detailsStep') detailsStep: TdStepComponent;
+
+ @Input('task') task: TaskDefinition;
+
+ @Input('editMode') editMode: boolean;
+
+ @Output('onSave') onSave = new EventEmitter<TaskDefinition>();
+
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder) {
+ this.form = formBuilder.group({
+ identifier: ['', [Validators.required, Validators.minLength(3), Validators.maxLength(32), FimsValidators.urlSafe]],
+ type: ['', [Validators.required]],
+ description: ['', [Validators.required, Validators.maxLength(4096)]],
+ name: ['', [Validators.required, Validators.maxLength(256)]],
+ mandatory: ['', [Validators.required]],
+ commands: this.initCommands([]),
+ predefined: ['', [Validators.required]]
+ });
+ }
+
+ ngOnInit(): void {
+ this.detailsStep.open();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes.task) {
+ this.form.reset({
+ identifier: this.task.identifier,
+ type: this.task.type,
+ description: this.task.description,
+ name: this.task.name,
+ mandatory: this.task.mandatory,
+ predefined: this.task.predefined
+ });
+
+ this.task.commands.forEach(command => this.addCommand(command));
+ }
+ }
+
+ save() {
+ const formCommands = this.form.get('commands').value;
+ const commands: TaskDefinitionCommand[] = formCommands.map(command => command.command);
+
+ const task: TaskDefinition = {
+ identifier: this.form.get('identifier').value,
+ type: this.form.get('type').value,
+ description: this.form.get('description').value,
+ name: this.form.get('name').value,
+ mandatory: this.form.get('mandatory').value,
+ commands,
+ predefined: this.form.get('predefined').value
+ };
+
+ this.onSave.emit(task);
+ }
+
+ cancel() {
+ this.onCancel.emit();
+ }
+
+ private initCommands(commands: TaskDefinitionCommand[]): FormArray {
+ const formControls: FormGroup[] = [];
+ commands.forEach(value => formControls.push(this.initCommand(value)));
+ return this.formBuilder.array(formControls);
+ }
+
+ private initCommand(value?: TaskDefinitionCommand): FormGroup {
+ return this.formBuilder.group({
+ command: [value ? value : '', Validators.required]
+ });
+ }
+
+ addCommand(value?: TaskDefinitionCommand): void {
+ const commands: FormArray = this.form.get('commands') as FormArray;
+ commands.push(this.initCommand(value));
+ }
+
+ removeCommand(index: number): void {
+ const commands: FormArray = this.form.get('commands') as FormArray;
+ commands.removeAt(index);
+ }
+
+ get commands(): AbstractControl[] {
+ const commands: FormArray = this.form.get('commands') as FormArray;
+ return commands.controls;
+ }
+
+}
diff --git a/src/app/customers/tasks/task-exists.guard.ts b/src/app/customers/tasks/task-exists.guard.ts
new file mode 100644
index 0000000..ab3dc61
--- /dev/null
+++ b/src/app/customers/tasks/task-exists.guard.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import {Injectable} from '@angular/core';
+import * as fromCustomers from '../store';
+import {Observable} from 'rxjs/Observable';
+import {of} from 'rxjs/observable/of';
+import {CustomersStore} from '../store/index';
+import {CustomerService} from '../../services/customer/customer.service';
+import {ExistsGuardService} from '../../common/guards/exists-guard';
+import {LoadAction} from '../store/tasks/task.actions';
+
+@Injectable()
+export class TaskExistsGuard implements CanActivate {
+
+ constructor(private store: CustomersStore,
+ private customerService: CustomerService,
+ private existsGuardService: ExistsGuardService) {
+ }
+
+ hasTaskInStore(id: string): Observable<boolean> {
+ const timestamp$: Observable<number> = this.store.select(fromCustomers.getTaskLoadedAt)
+ .map(loadedAt => loadedAt[id]);
+
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ hasTaskInApi(id: string): Observable<boolean> {
+ const getTask$: Observable<any> = this.customerService.getTask(id)
+ .map(taskEntity => new LoadAction({
+ resource: taskEntity
+ }))
+ .do((action: LoadAction) => this.store.dispatch(action))
+ .map(task => !!task);
+
+ return this.existsGuardService.routeTo404OnError(getTask$);
+ }
+
+ hasTask(id: string): Observable<boolean> {
+ return this.hasTaskInStore(id)
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+ return this.hasTaskInApi(id);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasTask(route.params['id']);
+ }
+}
diff --git a/src/app/customers/tasks/task.detail.component.html b/src/app/customers/tasks/task.detail.component.html
new file mode 100644
index 0000000..f03fd20
--- /dev/null
+++ b/src/app/customers/tasks/task.detail.component.html
@@ -0,0 +1,42 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over [title]="task.name" [subTitle]="task.description" *ngIf="task$ | async as task" [navigateBackTo]="['../../../']">
+ <div class="mat-content inset" flex>
+ <div layout="row">
+ <mat-list>
+ <mat-list-item>
+ <h3 matLine translate>Type</h3>
+ <p matLine>{{formatType(task.type)}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Mandatory</h3>
+ <p matLine>{{task.mandatory}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Automatically assign this task when member is created</h3>
+ <p matLine>{{task.predefined}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Trigger</h3>
+ <p matLine>{{formatCommands(task.commands)}}</p>
+ </mat-list-item>
+ </mat-list>
+ </div>
+ </div>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Edit task' | translate}}" icon="mode_edit" [link]="['edit']" [permission]="{ id: 'customer_tasks', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/customers/tasks/task.detail.component.ts b/src/app/customers/tasks/task.detail.component.ts
new file mode 100644
index 0000000..1135206
--- /dev/null
+++ b/src/app/customers/tasks/task.detail.component.ts
@@ -0,0 +1,49 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component} from '@angular/core';
+import {TaskDefinition, TaskDefinitionCommand, TaskDefinitionType} from '../../services/customer/domain/task-definition.model';
+import {Observable} from 'rxjs/Observable';
+import * as fromCustomers from '../store/index';
+import {CustomersStore} from '../store/index';
+import {defaultCommandOptions} from './domain/command-options.model';
+import {defaultTypeOptions} from './domain/type-options.model';
+
+@Component({
+ templateUrl: './task.detail.component.html'
+})
+export class TaskDetailComponent {
+
+ task$: Observable<TaskDefinition>;
+
+ constructor(private store: CustomersStore) {
+ this.task$ = store.select(fromCustomers.getSelectedTask);
+ }
+
+ formatType(type: TaskDefinitionType): string {
+ return defaultTypeOptions.find(option => option.type === type).label;
+ }
+
+ formatCommands(commands: TaskDefinitionCommand[]): string {
+ const options = commands.map(command =>
+ defaultCommandOptions.find(option => option.command === command).label
+ );
+
+ return options.join(', ');
+ }
+}
diff --git a/src/app/customers/tasks/task.index.component.html b/src/app/customers/tasks/task.index.component.html
new file mode 100644
index 0000000..ca721b3
--- /dev/null
+++ b/src/app/customers/tasks/task.index.component.html
@@ -0,0 +1,18 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<router-outlet></router-outlet>
diff --git a/src/app/customers/tasks/task.index.component.ts b/src/app/customers/tasks/task.index.component.ts
new file mode 100644
index 0000000..1655ada
--- /dev/null
+++ b/src/app/customers/tasks/task.index.component.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {SelectAction} from '../store/tasks/task.actions';
+import {CustomersStore} from '../store/index';
+import {ActivatedRoute} from '@angular/router';
+import {Subscription} from 'rxjs/Subscription';
+
+@Component({
+ templateUrl: './task.index.component.html'
+})
+export class TaskIndexComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ constructor(private route: ActivatedRoute, private customersStore: CustomersStore) {}
+
+ ngOnInit(): void {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectAction(params['id']))
+ .subscribe(this.customersStore);
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ }
+}
diff --git a/src/app/customers/tasks/task.list.component.html b/src/app/customers/tasks/task.list.component.html
new file mode 100644
index 0000000..11ec167
--- /dev/null
+++ b/src/app/customers/tasks/task.list.component.html
@@ -0,0 +1,27 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Manage tasks' | translate}}" [navigateBackTo]="['../']">
+ <fims-data-table
+ (onActionCellClick)="rowSelect($event)"
+ [columns]="columns"
+ [data]="tasksData$ | async"
+ [sortable]="false"
+ [pageable]="false">
+ </fims-data-table>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Create new task' | translate}}" icon="add" [link]="['create']" [permission]="{ id: 'customer_tasks', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/customers/tasks/task.list.component.ts b/src/app/customers/tasks/task.list.component.ts
new file mode 100644
index 0000000..6d0d16f
--- /dev/null
+++ b/src/app/customers/tasks/task.list.component.ts
@@ -0,0 +1,67 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ActivatedRoute, Router} from '@angular/router';
+import * as fromCustomers from '../store';
+import {Component} from '@angular/core';
+import {CustomersStore} from '../store/index';
+import {TaskDefinition, TaskDefinitionType} from '../../services/customer/domain/task-definition.model';
+import {Observable} from 'rxjs/Observable';
+import {LOAD_ALL} from '../store/tasks/task.actions';
+import {TableData} from '../../common/data-table/data-table.component';
+import {defaultTypeOptions} from './domain/type-options.model';
+
+@Component({
+ templateUrl: './task.list.component.html'
+})
+export class TaskListComponent {
+
+ tasksData$: Observable<TableData>;
+
+ columns: any[] = [
+ { name: 'identifier', label: 'Id' },
+ { name: 'name', label: 'Name' },
+ { name: 'mandatory', label: 'Mandatory?' },
+ { name: 'predefined', label: 'Auto assign?' },
+ { name: 'type', label: 'Type',
+ format: (value: TaskDefinitionType) =>
+ defaultTypeOptions.find(option => option.type === value).label
+ }
+ ];
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: CustomersStore) {
+ this.tasksData$ = this.store.select(fromCustomers.getAllTaskEntities)
+ .map(tasks => ({
+ data: tasks,
+ totalElements: tasks.length,
+ totalPages: 1
+ }));
+
+ this.fetchTasks();
+ }
+
+ fetchTasks(): void {
+ this.store.dispatch({
+ type: LOAD_ALL
+ });
+ }
+
+ rowSelect(task: TaskDefinition): void {
+ this.router.navigate(['detail', task.identifier], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/depositAccount/deposit-account.component.html b/src/app/depositAccount/deposit-account.component.html
new file mode 100644
index 0000000..42ce16d
--- /dev/null
+++ b/src/app/depositAccount/deposit-account.component.html
@@ -0,0 +1,21 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Manage deposit products' | translate}}">
+ <fims-data-table flex (onFetch)="fetchProducts($event)" (onActionCellClick)="rowSelect($event)" [columns]="columns" [data]="productData | async"></fims-data-table>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Create new product' | translate}}" icon="add" [link]="['create']" [permission]="{ id: 'deposit_definitions', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/depositAccount/deposit-account.component.ts b/src/app/depositAccount/deposit-account.component.ts
new file mode 100644
index 0000000..4c5d500
--- /dev/null
+++ b/src/app/depositAccount/deposit-account.component.ts
@@ -0,0 +1,63 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {TableData} from '../common/data-table/data-table.component';
+import {FetchRequest} from '../services/domain/paging/fetch-request.model';
+import * as fromDepositAccounts from './store';
+import {Observable} from 'rxjs/Observable';
+import {SEARCH} from './store/product.actions';
+import {DepositAccountStore} from './store/index';
+import {ProductDefinition} from '../services/depositAccount/domain/definition/product-definition.model';
+
+@Component({
+ templateUrl: './deposit-account.component.html'
+})
+export class DepositProductComponent implements OnInit {
+
+ productData: Observable<TableData>;
+
+ columns: any[] = [
+ { name: 'identifier', label: 'Id' },
+ { name: 'name', label: 'Name' },
+ { name: 'type', label: 'Type' },
+ { name: 'active', label: 'Enabled'},
+ { name: 'interest', label: 'Interest'}
+ ];
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: DepositAccountStore) {}
+
+ ngOnInit(): void {
+ this.productData = this.store.select(fromDepositAccounts.getProductSearchResults)
+ .map(productPage => ({
+ data: productPage.products,
+ totalElements: productPage.totalElements,
+ totalPages: productPage.totalPages
+ }));
+ this.fetchProducts();
+ }
+
+ fetchProducts(fetchRequest?: FetchRequest): void {
+ this.store.dispatch({ type: SEARCH });
+ }
+
+ rowSelect(productDefinition: ProductDefinition): void {
+ this.router.navigate(['detail', productDefinition.identifier], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/depositAccount/deposit-account.module.ts b/src/app/depositAccount/deposit-account.module.ts
new file mode 100644
index 0000000..f10d04b
--- /dev/null
+++ b/src/app/depositAccount/deposit-account.module.ts
@@ -0,0 +1,107 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Store} from '@ngrx/store';
+import {DepositAccountStore, depositAccountStoreFactory} from './store/index';
+import {DepositProductDefinitionNotificationEffects} from './store/effects/notification.effects';
+import {DepositProductDefinitionRouteEffects} from './store/effects/route.effects';
+import {DepositProductDefinitionApiEffects} from './store/effects/service.effects';
+import {EffectsModule} from '@ngrx/effects';
+import {CovalentMessageModule, CovalentStepsModule} from '@covalent/core';
+import {
+ MatButtonModule,
+ MatCardModule,
+ MatCheckboxModule,
+ MatIconModule,
+ MatInputModule,
+ MatListModule,
+ MatRadioModule,
+ MatSelectModule,
+ MatSlideToggleModule,
+ MatToolbarModule
+} from '@angular/material';
+import {CommonModule} from '@angular/common';
+import {TranslateModule} from '@ngx-translate/core';
+import {FimsSharedModule} from '../common/common.module';
+import {NgModule} from '@angular/core';
+import {RouterModule} from '@angular/router';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {DepositAccountRoutes} from './deposit-account.routes';
+import {ProductDefinitionExistsGuard} from './product-definition-exists.guard';
+import {DepositProductComponent} from './deposit-account.component';
+import {DepositProductCreateComponent} from './form/create.component';
+import {DepositProductFormComponent} from './form/form.component';
+import {DepositProductChargesFormComponent} from './form/charges/charges.component';
+import {DepositProductDetailComponent} from './detail/deposit-product.detail.component';
+import {DepositProductIndexComponent} from './detail/deposit-product.index.component';
+import {DepositProductEditComponent} from './form/edit.component';
+import {DepositProductDividendsComponent} from './detail/dividends/dividends.component';
+import {DepositProductDividendApiEffects} from './store/dividends/effects/service.effects';
+import {DepositProductDividendNotificationEffects} from './store/dividends/effects/notification.effects';
+import {DepositProductDividendRouteEffects} from './store/dividends/effects/route.effects';
+import {DividendFormComponent} from './detail/dividends/form/form.component';
+import {CreateDividendFormComponent} from './detail/dividends/form/create.component';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(DepositAccountRoutes),
+ FimsSharedModule,
+ TranslateModule,
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ MatCardModule,
+ MatIconModule,
+ MatListModule,
+ MatToolbarModule,
+ MatInputModule,
+ MatButtonModule,
+ MatSelectModule,
+ MatRadioModule,
+ MatCheckboxModule,
+ MatSlideToggleModule,
+
+ CovalentStepsModule,
+ CovalentMessageModule,
+
+ EffectsModule.run(DepositProductDefinitionApiEffects),
+ EffectsModule.run(DepositProductDefinitionRouteEffects),
+ EffectsModule.run(DepositProductDefinitionNotificationEffects),
+
+ EffectsModule.run(DepositProductDividendApiEffects),
+ EffectsModule.run(DepositProductDividendRouteEffects),
+ EffectsModule.run(DepositProductDividendNotificationEffects)
+ ],
+ declarations: [
+ DepositProductComponent,
+ DepositProductCreateComponent,
+ DepositProductEditComponent,
+ DepositProductFormComponent,
+ DepositProductChargesFormComponent,
+ DepositProductIndexComponent,
+ DepositProductDetailComponent,
+ DepositProductDividendsComponent,
+ CreateDividendFormComponent,
+ DividendFormComponent
+ ],
+ providers: [
+ ProductDefinitionExistsGuard,
+ { provide: DepositAccountStore, useFactory: depositAccountStoreFactory, deps: [Store]}
+ ]
+})
+export class DepositAccountModule {}
diff --git a/src/app/depositAccount/deposit-account.routes.ts b/src/app/depositAccount/deposit-account.routes.ts
new file mode 100644
index 0000000..0f8e7f3
--- /dev/null
+++ b/src/app/depositAccount/deposit-account.routes.ts
@@ -0,0 +1,66 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Routes} from '@angular/router';
+import {DepositProductComponent} from './deposit-account.component';
+import {DepositProductCreateComponent} from './form/create.component';
+import {DepositProductDetailComponent} from './detail/deposit-product.detail.component';
+import {ProductDefinitionExistsGuard} from './product-definition-exists.guard';
+import {DepositProductIndexComponent} from './detail/deposit-product.index.component';
+import {DepositProductEditComponent} from './form/edit.component';
+import {DepositProductDividendsComponent} from './detail/dividends/dividends.component';
+import {CreateDividendFormComponent} from './detail/dividends/form/create.component';
+
+export const DepositAccountRoutes: Routes = [
+ {
+ path: '',
+ component: DepositProductComponent,
+ data: { hasPermission: { id: 'deposit_definitions', accessLevel: 'READ' } },
+ },
+ {
+ path: 'create',
+ component: DepositProductCreateComponent,
+ data: { hasPermission: { id: 'deposit_definitions', accessLevel: 'CHANGE' } },
+ },
+ {
+ path: 'detail/:id',
+ component: DepositProductIndexComponent,
+ canActivate: [ProductDefinitionExistsGuard],
+ data: { hasPermission: { id: 'deposit_definitions', accessLevel: 'READ' } },
+ children: [
+ {
+ path: '',
+ component: DepositProductDetailComponent
+ },
+ {
+ path: 'dividends',
+ component: DepositProductDividendsComponent
+ },
+ {
+ path: 'dividends/distribute',
+ component: CreateDividendFormComponent,
+ data: { hasPermission: { id: 'deposit_definitions', accessLevel: 'CHANGE' } },
+ },
+ {
+ path: 'edit',
+ component: DepositProductEditComponent,
+ data: { hasPermission: { id: 'deposit_definitions', accessLevel: 'CHANGE' } },
+ }
+ ]
+ }
+];
diff --git a/src/app/depositAccount/detail/deposit-product.detail.component.html b/src/app/depositAccount/detail/deposit-product.detail.component.html
new file mode 100644
index 0000000..b9d2585
--- /dev/null
+++ b/src/app/depositAccount/detail/deposit-product.detail.component.html
@@ -0,0 +1,93 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+
+<fims-layout-card-over [title]="definition.name" [subTitle]="definition.description" [navigateBackTo]="['../../../']">
+ <fims-layout-card-over-header-menu>
+ <button mat-icon-button (click)="deleteProduct()" title="{{'Delete this product' | translate}}" *hasPermission="{ id: 'deposit_definitions', accessLevel: 'DELETE'}"><mat-icon>delete</mat-icon></button>
+ </fims-layout-card-over-header-menu>
+ <td-message *ngIf="!definition.active" label="{{'Product not enabled' | translate }}"
+ sublabel="{{'To assign this product to a member it needs to be enabled first' | translate }}"
+ color="warn" icon="error">
+ <button td-message-actions mat-button (click)="enableProduct()" *hasPermission="{ id: 'deposit_definitions', accessLevel: 'CHANGE'}" translate>ENABLE PRODUCT</button>
+ </td-message>
+ <td-message *ngIf="definition.active" label="{{'Product enabled' | translate }}"
+ sublabel="{{'This product can be assigned to a member ' | translate }}" color="accent" icon="check">
+ <button td-message-actions mat-button (click)="disableProduct()" *hasPermission="{ id: 'deposit_definitions', accessLevel: 'CHANGE'}" translate>DISABLE PRODUCT</button>
+ </td-message>
+ <fims-two-column-layout>
+ <mat-nav-list left *ngIf="canDistributeDividends$ | async">
+ <h3 mat-subheader translate>Management</h3>
+ <a mat-list-item [routerLink]="['./dividends']">
+ <mat-icon matListAvatar>zoom_out_map</mat-icon>
+ <h3 matLine translate>Dividends</h3>
+ <p matLine translate>View and distribute dividends</p>
+ </a>
+ </mat-nav-list>
+ <ng-container right>
+ <div layout="row">
+ <mat-list dense>
+ <mat-list-item>
+ <h3 matLine translate>Type</h3>
+ <p matLine>{{definition.type}}</p>
+ </mat-list-item>
+ <mat-list-item *ngIf="hasTerm(definition)">
+ <h3 matLine translate>Term</h3>
+ <p matLine>{{definition.term?.period + ' ' + definition.term?.timeUnit}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Interest payable</h3>
+ <p matLine>{{definition.term?.interestPayable}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Minimum balance</h3>
+ <p matLine>{{definition.minimumBalance | number:numberFormat}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Interest</h3>
+ <p matLine>{{definition.interest | number:numberFormat}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Currency</h3>
+ <p matLine>{{definition.currency.code}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Cash account</h3>
+ <p matLine>{{definition.cashAccountIdentifier}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Expense account</h3>
+ <p matLine>{{definition.expenseAccountIdentifier}}</p>
+ </mat-list-item>
+ <mat-list-item *ngIf="definition.accrueAccountIdentifier">
+ <h3 matLine translate>Accrue account</h3>
+ <p matLine>{{definition.accrueAccountIdentifier}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Equity ledger</h3>
+ <p matLine>{{definition.equityLedgerIdentifier}}</p>
+ </mat-list-item>
+ </mat-list>
+ </div>
+ <div layout="column">
+ <h4>Fees</h4>
+ <fims-data-table flex [data]="charges" [columns]="columns" [actionColumn]="false"></fims-data-table>
+ </div>
+ </ng-container>
+ </fims-two-column-layout>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Edit product' | translate}}" icon="mode_edit" [link]="['edit']" [permission]="{ id: 'deposit_definitions', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/depositAccount/detail/deposit-product.detail.component.ts b/src/app/depositAccount/detail/deposit-product.detail.component.ts
new file mode 100644
index 0000000..0c514b2
--- /dev/null
+++ b/src/app/depositAccount/detail/deposit-product.detail.component.ts
@@ -0,0 +1,138 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {ProductDefinition} from '../../services/depositAccount/domain/definition/product-definition.model';
+import {Subscription} from 'rxjs/Subscription';
+import {ActivatedRoute, Router} from '@angular/router';
+import {DepositAccountStore} from '../store/index';
+import * as fromDepositAccounts from './../store';
+import * as fromRoot from '../../store';
+import {TableData} from '../../common/data-table/data-table.component';
+import {TdDialogService} from '@covalent/core';
+import {Observable} from 'rxjs/Observable';
+import {DELETE, EXECUTE_COMMAND} from '../store/product.actions';
+import {FimsPermission} from '../../services/security/authz/fims-permission.model';
+
+@Component({
+ templateUrl: './deposit-product.detail.component.html'
+})
+export class DepositProductDetailComponent implements OnInit, OnDestroy {
+
+ private productSubscription: Subscription;
+
+ numberFormat = '1.2-2';
+
+ definition: ProductDefinition;
+
+ canDistributeDividends$: Observable<boolean>;
+
+ charges: TableData;
+
+ columns: any[] = [
+ { name: 'name', label: 'Name' },
+ { name: 'description', label: 'Description' },
+ { name: 'actionIdentifier', label: 'Applied on' },
+ { name: 'proportional', label: 'Proportional?' },
+ { name: 'incomeAccountIdentifier', label: 'Income account' },
+ { name: 'amount', label: 'Amount', numeric: true, format: value => value ? value.toFixed(2) : undefined }
+ ];
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: DepositAccountStore,
+ private dialogService: TdDialogService) {}
+
+ ngOnInit(): void {
+ const selectedProduct$ = this.store.select(fromDepositAccounts.getSelectedProduct)
+ .filter(product => !!product);
+
+ this.productSubscription = selectedProduct$
+ .subscribe(product => {
+ this.definition = product;
+ this.charges = {
+ data: product.charges,
+ totalElements: product.charges.length,
+ totalPages: 1
+ };
+ });
+
+ this.canDistributeDividends$ = Observable.combineLatest(
+ selectedProduct$,
+ this.store.select(fromRoot.getPermissions),
+ (product, permissions) => ({
+ isShareProduct: product.type === 'SHARE',
+ hasPermission: this.hasChangePermission(permissions)
+ })
+ ).map(result => result.isShareProduct && result.hasPermission);
+ }
+
+ ngOnDestroy(): void {
+ this.productSubscription.unsubscribe();
+ }
+
+ goToTasks(): void {
+ this.router.navigate(['tasks'], { relativeTo: this.route });
+ }
+
+ confirmDeletion(): Observable<boolean> {
+ return this.dialogService.openConfirm({
+ message: 'Do you want to delete this product?',
+ title: 'Confirm deletion',
+ acceptButton: 'DELETE PRODUCT',
+ }).afterClosed();
+ }
+
+ deleteProduct(): void {
+ this.confirmDeletion()
+ .filter(accept => accept)
+ .subscribe(() => this.store.dispatch({
+ type: DELETE, payload: {
+ productDefinition: this.definition,
+ activatedRoute: this.route
+ }
+ }));
+ }
+
+ hasTerm(defininition: ProductDefinition): boolean {
+ return !!defininition.term.timeUnit || !!defininition.term.period;
+ }
+
+ private hasChangePermission(permissions: FimsPermission[]): boolean {
+ return permissions.filter(permission =>
+ permission.id === 'deposit_definitions' &&
+ permission.accessLevel === 'CHANGE'
+ ).length > 0;
+ }
+
+ enableProduct(): void {
+ this.store.dispatch({ type: EXECUTE_COMMAND, payload: {
+ definitionId: this.definition.identifier,
+ command: {
+ action: 'ACTIVATE'
+ }
+ }});
+ }
+
+ disableProduct(): void {
+ this.store.dispatch({ type: EXECUTE_COMMAND, payload: {
+ definitionId: this.definition.identifier,
+ command: {
+ action: 'DEACTIVATE'
+ }
+ }});
+ }
+}
diff --git a/src/app/depositAccount/detail/deposit-product.index.component.html b/src/app/depositAccount/detail/deposit-product.index.component.html
new file mode 100644
index 0000000..ca721b3
--- /dev/null
+++ b/src/app/depositAccount/detail/deposit-product.index.component.html
@@ -0,0 +1,18 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<router-outlet></router-outlet>
diff --git a/src/app/depositAccount/detail/deposit-product.index.component.ts b/src/app/depositAccount/detail/deposit-product.index.component.ts
new file mode 100644
index 0000000..ed558ee
--- /dev/null
+++ b/src/app/depositAccount/detail/deposit-product.index.component.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {DepositAccountStore} from '../store/index';
+import {Subscription} from 'rxjs/Subscription';
+import {ActivatedRoute} from '@angular/router';
+import {SelectAction} from '../store/product.actions';
+
+@Component({
+ templateUrl: './deposit-product.index.component.html'
+})
+export class DepositProductIndexComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ constructor(private route: ActivatedRoute, private store: DepositAccountStore) {}
+
+ ngOnInit(): void {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectAction(params['id']))
+ .subscribe(this.store);
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ }
+}
diff --git a/src/app/depositAccount/detail/dividends/dividends.component.html b/src/app/depositAccount/detail/dividends/dividends.component.html
new file mode 100644
index 0000000..58ef036
--- /dev/null
+++ b/src/app/depositAccount/detail/dividends/dividends.component.html
@@ -0,0 +1,21 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over [title]="'Dividend distributions' | translate" [navigateBackTo]="['../']">
+ <fims-data-table flex [columns]="columns" [data]="dividendData$ | async" [actionColumn]="false"></fims-data-table>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Distribute dividends' | translate}}" icon="zoom_out_map" [link]="['distribute']" [permission]="{ id: 'deposit_definitions', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/depositAccount/detail/dividends/dividends.component.ts b/src/app/depositAccount/detail/dividends/dividends.component.ts
new file mode 100644
index 0000000..7907943
--- /dev/null
+++ b/src/app/depositAccount/detail/dividends/dividends.component.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import * as fromDepositAccounts from '../../store/index';
+import {DepositAccountStore} from '../../store/index';
+import {Observable} from 'rxjs/Observable';
+import {TableData} from '../../../common/data-table/data-table.component';
+import {LOAD_ALL} from '../../store/dividends/dividend.actions';
+import {Subscription} from 'rxjs/Subscription';
+import {DisplayFimsDate} from '../../../common/date/fims-date.pipe';
+
+@Component({
+ providers: [DisplayFimsDate],
+ templateUrl: './dividends.component.html'
+})
+export class DepositProductDividendsComponent implements OnInit, OnDestroy {
+
+ private productSubscription: Subscription;
+
+ private productIdentifer: string;
+
+ dividendData$: Observable<TableData>;
+
+ columns: any[] = [
+ { name: 'dueDate', label: 'Due date', format: value => this.displayFimsDate.transform(value) },
+ { name: 'dividendRate', label: 'Dividend rate' }
+ ];
+
+ constructor(private store: DepositAccountStore, private displayFimsDate: DisplayFimsDate) {}
+
+ ngOnInit() {
+ this.productSubscription = this.store.select(fromDepositAccounts.getSelectedProduct)
+ .filter(product => !!product)
+ .subscribe(product => this.productIdentifer = product.identifier);
+
+ this.dividendData$ = this.store.select(fromDepositAccounts.getDividends)
+ .map(dividends => ({
+ data: dividends,
+ totalElements: dividends.length,
+ totalPages: 1
+ }));
+
+ this.store.dispatch({
+ type: LOAD_ALL,
+ payload: this.productIdentifer
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.productSubscription.unsubscribe();
+ }
+}
diff --git a/src/app/depositAccount/detail/dividends/form/create.component.html b/src/app/depositAccount/detail/dividends/form/create.component.html
new file mode 100644
index 0000000..0715c27
--- /dev/null
+++ b/src/app/depositAccount/detail/dividends/form/create.component.html
@@ -0,0 +1,24 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over>
+ <fims-deposit-product-dividend-form
+ [productDefinitionId]="productDefinitionId$ | async"
+ (onSave)="save($event)"
+ (onCancel)="cancel()">
+ </fims-deposit-product-dividend-form>
+</fims-layout-card-over>
diff --git a/src/app/depositAccount/detail/dividends/form/create.component.ts b/src/app/depositAccount/detail/dividends/form/create.component.ts
new file mode 100644
index 0000000..72a7bc8
--- /dev/null
+++ b/src/app/depositAccount/detail/dividends/form/create.component.ts
@@ -0,0 +1,59 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import {DepositAccountStore} from '../../../store/index';
+import {CREATE} from '../../../store/dividends/dividend.actions';
+import * as fromDepositAccounts from './../../../store';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Observable} from 'rxjs/Observable';
+import {DistributeDividendFormData} from './form.component';
+
+@Component({
+ templateUrl: './create.component.html'
+})
+export class CreateDividendFormComponent implements OnInit {
+
+ productDefinitionId$: Observable<string>;
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: DepositAccountStore) {}
+
+ ngOnInit() {
+ this.productDefinitionId$ = this.store.select(fromDepositAccounts.getSelectedProduct)
+ .filter(product => !!product)
+ .map(product => product.identifier);
+ }
+
+ save(payload: DistributeDividendFormData): void {
+ this.store.dispatch({
+ type: CREATE,
+ payload: {
+ productDefinitionId: payload.productDefinitionId,
+ dividendDistribution: {
+ dueDate: payload.dueDate,
+ dividendRate: payload.dividendRate
+ },
+ activatedRoute: this.route
+ }
+ });
+ }
+
+ cancel(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/depositAccount/detail/dividends/form/form.component.html b/src/app/depositAccount/detail/dividends/form/form.component.html
new file mode 100644
index 0000000..9810f92
--- /dev/null
+++ b/src/app/depositAccount/detail/dividends/form/form.component.html
@@ -0,0 +1,33 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Distribute dividend' | translate}}" [state]="form.valid ? 'complete' : form.pristine ? 'none' : 'required'" [active]="true">
+ <form [formGroup]="form" layout="column">
+ <fims-date-input [form]="form" controlName="dueDate" placeholder="{{'Due date' | translate}}"></fims-date-input>
+ <fims-number-input [form]="form" controlName="dividendRate" placeholder="{{'Dividend rate' | translate}}"></fims-number-input>
+ </form>
+ </td-step>
+
+ <td-step label="{{'Final step' | translate}}" [state]="'complete'">
+ <ng-template td-step-summary>
+ <button mat-raised-button color="primary" (click)="save()" [disabled]="form.invalid">{{'DISTRIBUTE DIVIDEND'}}</button>
+ <span flex></span>
+ <button mat-button (click)="cancel()">{{'CANCEL' | translate}}</button>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/depositAccount/detail/dividends/form/form.component.ts b/src/app/depositAccount/detail/dividends/form/form.component.ts
new file mode 100644
index 0000000..da2653f
--- /dev/null
+++ b/src/app/depositAccount/detail/dividends/form/form.component.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
+import {FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {FimsValidators} from '../../../../common/validator/validators';
+import {todayAsISOString, toFimsDate} from '../../../../services/domain/date.converter';
+
+export interface DistributeDividendFormData {
+ productDefinitionId: string;
+ dueDate: {
+ year?: number;
+ month?: number;
+ day?: number;
+ };
+ dividendRate: string;
+}
+
+@Component({
+ selector: 'fims-deposit-product-dividend-form',
+ templateUrl: './form.component.html'
+})
+export class DividendFormComponent implements OnInit {
+
+ @Input() productDefinitionId: string;
+
+ form: FormGroup;
+
+ @Output() onSave = new EventEmitter<DistributeDividendFormData>();
+
+ @Output() onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder) {}
+
+ ngOnInit(): void {
+ this.form = this.formBuilder.group({
+ dueDate: [todayAsISOString(), [Validators.required]],
+ dividendRate: ['', [ Validators.required, FimsValidators.minValue(0)] ]
+ });
+ }
+
+ save(): void {
+ this.onSave.emit({
+ productDefinitionId: this.productDefinitionId,
+ dueDate: toFimsDate(this.form.get('dueDate').value),
+ dividendRate: this.form.get('dividendRate').value
+ });
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+}
diff --git a/src/app/depositAccount/domain/interest-payable-option-list.model.ts b/src/app/depositAccount/domain/interest-payable-option-list.model.ts
new file mode 100644
index 0000000..4719307
--- /dev/null
+++ b/src/app/depositAccount/domain/interest-payable-option-list.model.ts
@@ -0,0 +1,31 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {InterestPayable} from '../../services/depositAccount/domain/interest-payable.model';
+
+export interface InterestPayableOption {
+ label: string;
+ type: InterestPayable;
+}
+
+export const interestPayableOptionList: InterestPayableOption[] = [
+ { type: 'MATURITY', label: 'Maturity'},
+ { type: 'ANNUALLY', label: 'Annually'},
+ { type: 'QUARTERLY', label: 'Quarterly'},
+ { type: 'MONTHLY', label: 'Monthly'}
+];
diff --git a/src/app/depositAccount/domain/time-unit-option-list.model.ts b/src/app/depositAccount/domain/time-unit-option-list.model.ts
new file mode 100644
index 0000000..b61d898
--- /dev/null
+++ b/src/app/depositAccount/domain/time-unit-option-list.model.ts
@@ -0,0 +1,29 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {TimeUnit} from '../../services/depositAccount/domain/time-unit.model';
+
+export interface TimeUnitOption {
+ label: string;
+ type: TimeUnit;
+}
+
+export const timeUnitOptionList: TimeUnitOption[] = [
+ { type: 'MONTH', label: 'Month'},
+ { type: 'YEAR', label: 'Year'},
+];
diff --git a/src/app/depositAccount/domain/type-option-list.model.ts b/src/app/depositAccount/domain/type-option-list.model.ts
new file mode 100644
index 0000000..6d9a4a8
--- /dev/null
+++ b/src/app/depositAccount/domain/type-option-list.model.ts
@@ -0,0 +1,30 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Type} from '../../services/depositAccount/domain/type.model';
+
+export interface TypeOption {
+ label: string;
+ type: Type;
+}
+
+export const typeOptionList: TypeOption[] = [
+ { type: 'CHECKING', label: 'Checking'},
+ { type: 'SAVINGS', label: 'Savings'},
+ { type: 'SHARE', label: 'Share'}
+];
diff --git a/src/app/depositAccount/form/charges/charges.component.html b/src/app/depositAccount/form/charges/charges.component.html
new file mode 100644
index 0000000..f2daa6b
--- /dev/null
+++ b/src/app/depositAccount/form/charges/charges.component.html
@@ -0,0 +1,58 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<form [formGroup]="form">
+ <div layout-gt-xs="column" layout-margin formArrayName="charges">
+ <mat-card *ngFor="let charge of charges; let i=index" [formGroupName]="i">
+ <mat-card-content layout="column">
+ <fims-text-input [form]="charge" controlName="name" placeholder="{{'Fee name' | translate}}"></fims-text-input>
+ <mat-form-field layout-margin flex>
+ <textarea matInput placeholder="{{'Description' | translate}}" formControlName="description"></textarea>
+ <mat-error *ngIf="charge.get('description').hasError('maxlength')">
+ {{ 'Only characters allowed.' | translate:{ value: charge.get('description').getError('maxlength')['requiredLength']} }}
+ </mat-error>
+ </mat-form-field>
+ <div layout="row">
+ <fims-number-input [form]="getFormGroup(i)" controlName="amount" placeholder="{{'Amount' | translate}}"></fims-number-input>
+ <mat-form-field layout-margin>
+ <mat-select formControlName="actionIdentifier" placeholder="{{'Applied on' | translate}}">
+ <mat-option *ngFor="let basis of actions" [value]="basis.identifier">
+ {{basis.name | translate}}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ <mat-checkbox layout-margin formControlName="proportional" translate>Proportional?</mat-checkbox>
+ </div>
+ <fims-account-select title="{{'Income account' | translate}}" formControlName="incomeAccountIdentifier" [type]="'REVENUE'">
+ <ng-container *ngIf="!charge.get('incomeAccountIdentifier').pristine && charge.get('incomeAccountIdentifier').hasError('required')" translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="charge.get('incomeAccountIdentifier').hasError('invalidAccount')" translate>
+ Invalid account
+ </ng-container>
+ </fims-account-select>
+ </mat-card-content>
+ <mat-card-actions>
+ <button mat-button (click)="removeCharge(i)">{{'REMOVE FEE' | translate}}</button>
+ </mat-card-actions>
+ </mat-card>
+
+ <div layout="row">
+ <button flex mat-button mat-raised-button (click)="addCharge()">{{'ADD FEE' | translate}}</button>
+ </div>
+ </div>
+</form>
diff --git a/src/app/depositAccount/form/charges/charges.component.ts b/src/app/depositAccount/form/charges/charges.component.ts
new file mode 100644
index 0000000..bc3a6e6
--- /dev/null
+++ b/src/app/depositAccount/form/charges/charges.component.ts
@@ -0,0 +1,93 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input} from '@angular/core';
+import {AbstractControl, FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {Charge} from '../../../services/depositAccount/domain/definition/charge.model';
+import {FormComponent} from '../../../common/forms/form.component';
+import {Action} from '../../../services/depositAccount/domain/definition/action.model';
+import {accountExists} from '../../../common/validator/account-exists.validator';
+import {AccountingService} from '../../../services/accounting/accounting.service';
+import {FimsValidators} from '../../../common/validator/validators';
+
+@Component({
+ selector: 'fims-deposit-product-charges-form',
+ templateUrl: './charges.component.html'
+})
+export class DepositProductChargesFormComponent extends FormComponent<Charge[]> {
+
+ @Input('actions') actions: Action[];
+
+ @Input('formData') set formData(charges: Charge[]) {
+ charges = charges || [];
+ this.form = this.formBuilder.group({
+ charges: this.initCharges(charges),
+ });
+ }
+
+ constructor(private formBuilder: FormBuilder, private accountingService: AccountingService) {
+ super();
+ }
+
+ get formData(): Charge[] {
+ const charges = this.form.get('charges').value;
+
+ return charges.map(charge => Object.assign({}, charge, {
+ amount: parseFloat(charge.amount)
+ }));
+ }
+
+ private initCharges(charges: Charge[]): FormArray {
+ const formControls: FormGroup[] = [];
+ charges.forEach(charge => formControls.push(this.initCharge(charge)));
+ return this.formBuilder.array(formControls);
+ }
+
+ private initCharge(charge?: Charge): FormGroup {
+ const amount = charge ? charge.amount : 0;
+
+ return this.formBuilder.group({
+ actionIdentifier: [charge ? charge.actionIdentifier : '', Validators.required],
+ incomeAccountIdentifier: [charge ? charge.incomeAccountIdentifier : '', [Validators.required], accountExists(this.accountingService)],
+ name: [charge ? charge.name : '', [Validators.required, Validators.maxLength(256)]],
+ description: [charge ? charge.description : '', Validators.maxLength(2048)],
+ proportional: [charge ? charge.proportional : false ],
+ amount: [amount.toFixed(2), [ FimsValidators.minValue(0)] ]
+ });
+ }
+
+ addCharge(): void {
+ const charges: FormArray = this.form.get('charges') as FormArray;
+ charges.push(this.initCharge());
+ }
+
+ removeCharge(index: number): void {
+ const charges: FormArray = this.form.get('charges') as FormArray;
+ charges.removeAt(index);
+ }
+
+ get charges(): AbstractControl[] {
+ const charges: FormArray = this.form.get('charges') as FormArray;
+ return charges.controls;
+ }
+
+ getFormGroup(index: number): FormGroup {
+ const charges = this.form.get('charges') as FormArray;
+ return charges.at(index) as FormGroup;
+ }
+}
diff --git a/src/app/depositAccount/form/create.component.html b/src/app/depositAccount/form/create.component.html
new file mode 100644
index 0000000..64f7e85
--- /dev/null
+++ b/src/app/depositAccount/form/create.component.html
@@ -0,0 +1,26 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Create new deposit product' | translate}}">
+ <fims-deposit-product-form #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [definition]="definition"
+ [currencies]="currencies | async"
+ [actions]="actions | async">
+ </fims-deposit-product-form>
+</fims-layout-card-over>
diff --git a/src/app/depositAccount/form/create.component.ts b/src/app/depositAccount/form/create.component.ts
new file mode 100644
index 0000000..8bcf444
--- /dev/null
+++ b/src/app/depositAccount/form/create.component.ts
@@ -0,0 +1,107 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {DepositAccountStore} from '../store/index';
+import {CREATE, RESET_FORM} from '../store/product.actions';
+import {Subscription} from 'rxjs/Subscription';
+import * as fromDepositAccount from '../store';
+import {Error} from '../../services/domain/error.model';
+import {ProductDefinition} from '../../services/depositAccount/domain/definition/product-definition.model';
+import {DepositProductFormComponent} from './form.component';
+import {CurrencyService} from '../../services/currency/currency.service';
+import {DepositAccountService} from '../../services/depositAccount/deposit-account.service';
+import {Currency} from '../../services/currency/domain/currency.model';
+import {Action} from '../../services/depositAccount/domain/definition/action.model';
+import {Observable} from 'rxjs/Observable';
+
+@Component({
+ templateUrl: './create.component.html'
+})
+export class DepositProductCreateComponent implements OnInit, OnDestroy {
+
+ private formStateSubscription: Subscription;
+
+ @ViewChild('form') formComponent: DepositProductFormComponent;
+
+ definition: ProductDefinition = {
+ type: 'CHECKING',
+ identifier: '',
+ name: '',
+ interest: 0,
+ charges: [],
+ currency: {
+ code: 'USD',
+ name: '',
+ scale: 2,
+ sign: ''
+ },
+ flexible: false,
+ minimumBalance: 0,
+ cashAccountIdentifier: '',
+ expenseAccountIdentifier: '',
+ term: {
+ interestPayable: 'ANNUALLY'
+ }
+ };
+
+ currencies: Observable<Currency[]>;
+
+ actions: Observable<Action[]>;
+
+ constructor(private router: Router, private route: ActivatedRoute, private depositStore: DepositAccountStore,
+ private depositService: DepositAccountService, private currencyService: CurrencyService) {}
+
+ ngOnInit(): void {
+ this.currencies = this.currencyService.fetchCurrencies();
+ this.actions = this.depositService.fetchActions();
+
+ this.formStateSubscription = this.depositStore.select(fromDepositAccount.getProductFormError)
+ .filter((error: Error) => !!error)
+ .subscribe((error: Error) => {
+ const detailForm = this.formComponent.formGroup;
+ const errors = detailForm.get('identifier').errors || {};
+ errors['unique'] = true;
+ detailForm.get('identifier').setErrors(errors);
+ this.formComponent.step.open();
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.formStateSubscription.unsubscribe();
+
+ this.depositStore.dispatch({ type: RESET_FORM });
+ }
+
+ onSave(productDefinition: ProductDefinition): void {
+ this.depositStore.dispatch({ type: CREATE, payload: {
+ productDefinition,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel() {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+
+}
diff --git a/src/app/depositAccount/form/edit.component.html b/src/app/depositAccount/form/edit.component.html
new file mode 100644
index 0000000..d93938b
--- /dev/null
+++ b/src/app/depositAccount/form/edit.component.html
@@ -0,0 +1,27 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Edit deposit product' | translate}}">
+ <fims-deposit-product-form #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [definition]="definition$ | async"
+ [currencies]="currencies | async"
+ [actions]="actions | async"
+ [editMode]="true">
+ </fims-deposit-product-form>
+</fims-layout-card-over>
diff --git a/src/app/depositAccount/form/edit.component.ts b/src/app/depositAccount/form/edit.component.ts
new file mode 100644
index 0000000..745d771
--- /dev/null
+++ b/src/app/depositAccount/form/edit.component.ts
@@ -0,0 +1,73 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {DepositAccountStore} from '../store/index';
+import {RESET_FORM, UPDATE} from '../store/product.actions';
+import * as fromDepositAccount from '../store';
+import {ProductDefinition} from '../../services/depositAccount/domain/definition/product-definition.model';
+import {DepositProductFormComponent} from './form.component';
+import {CurrencyService} from '../../services/currency/currency.service';
+import {DepositAccountService} from '../../services/depositAccount/deposit-account.service';
+import {Currency} from '../../services/currency/domain/currency.model';
+import {Action} from '../../services/depositAccount/domain/definition/action.model';
+import {Observable} from 'rxjs/Observable';
+
+@Component({
+ templateUrl: './edit.component.html'
+})
+export class DepositProductEditComponent implements OnInit, OnDestroy {
+
+ @ViewChild('form') formComponent: DepositProductFormComponent;
+
+ definition$: Observable<ProductDefinition>;
+
+ currencies: Observable<Currency[]>;
+
+ actions: Observable<Action[]>;
+
+ constructor(private router: Router, private route: ActivatedRoute, private depositStore: DepositAccountStore,
+ private depositService: DepositAccountService, private currencyService: CurrencyService) {}
+
+ ngOnInit(): void {
+ this.currencies = this.currencyService.fetchCurrencies();
+ this.actions = this.depositService.fetchActions();
+ this.definition$ = this.depositStore.select(fromDepositAccount.getSelectedProduct);
+ }
+
+ ngOnDestroy(): void {
+ this.depositStore.dispatch({ type: RESET_FORM });
+ }
+
+ onSave(productDefinition: ProductDefinition): void {
+ this.depositStore.dispatch({ type: UPDATE, payload: {
+ productDefinition,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel() {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+
+}
diff --git a/src/app/depositAccount/form/form.component.html b/src/app/depositAccount/form/form.component.html
new file mode 100644
index 0000000..46c5bd6
--- /dev/null
+++ b/src/app/depositAccount/form/form.component.html
@@ -0,0 +1,134 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Product details' | translate}}" [state]="formGroup.valid ? 'complete' : formGroup.pristine ? 'none' : 'required'">
+ <form [formGroup]="formGroup" layout="column">
+ <div layout="row">
+ <mat-radio-group formControlName="type">
+ <mat-radio-button *ngFor="let basis of typeOptions" [value]="basis.type" layout-margin>
+ {{ basis.label | translate }}
+ </mat-radio-button>
+ </mat-radio-group>
+ </div>
+ <div layout="row">
+ <fims-id-input placeholder="Short name" [form]="formGroup" controlName="identifier" [readonly]="editMode"></fims-id-input>
+ </div>
+ <div layout="row">
+ <fims-text-input [form]="formGroup" controlName="name" placeholder="{{'Name' | translate}}"></fims-text-input>
+ </div>
+ <div layout="row">
+ <mat-form-field layout-margin flex>
+ <textarea matInput placeholder="{{'Description(optional)' | translate}}" formControlName="description"></textarea>
+ <mat-error *ngIf="formGroup.get('description').hasError('maxlength')">
+ {{ 'Only characters allowed.' | translate:{ value: formGroup.get('description').getError('maxlength')['requiredLength']} }}
+ </mat-error>
+ </mat-form-field>
+ </div>
+ <div layout="row">
+ <mat-form-field layout-margin>
+ <mat-select formControlName="currencyCode" placeholder="{{ 'Currency' | translate }}">
+ <mat-option *ngFor="let currency of currencies" [value]="currency.code">
+ {{currency.code}}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ </div>
+ <div layout="row">
+ <fims-number-input [form]="formGroup" controlName="minimumBalance" placeholder="{{'Minimum balance' | translate}}"></fims-number-input>
+ </div>
+ <div layout="column">
+ <span translate>Interest payable:</span>
+ <mat-radio-group formControlName="termInterestPayable">
+ <mat-radio-button *ngFor="let basis of interestPayableOptions" [value]="basis.type" layout-margin>
+ {{ basis.label | translate }}
+ </mat-radio-button>
+ </mat-radio-group>
+ </div>
+ <div layout="row" layout-margin>
+ <mat-checkbox formControlName="flexible" [disabled]="editMode" translate>Flexible interest during the term?</mat-checkbox>
+ </div>
+ <div layout="row">
+ <fims-number-input [form]="formGroup" controlName="interest" placeholder="{{'Interest' | translate}}"></fims-number-input>
+ </div>
+ <div layout="row">
+ <mat-slide-toggle formControlName="fixedTermEnabled" layout-margin translate>
+ Fixed term?
+ </mat-slide-toggle>
+ </div>
+ <div layout="row">
+ <fims-text-input type="number" [form]="formGroup" controlName="termPeriod" placeholder="{{'Term period' | translate}}"></fims-text-input>
+ <mat-radio-group formControlName="termTimeUnit">
+ <mat-radio-button *ngFor="let basis of timeUnitOptions" [value]="basis.type" layout-margin>
+ {{ basis.label | translate }}
+ </mat-radio-button>
+ </mat-radio-group>
+ </div>
+ <fims-account-select title="{{'Cash account(Asset accounts only)' | translate}}" formControlName="cashAccountIdentifier" [type]="'ASSET'">
+ <ng-container *ngIf="!formGroup.get('cashAccountIdentifier').pristine && formGroup.get('cashAccountIdentifier').hasError('required')" translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="formGroup.get('cashAccountIdentifier').hasError('invalidAccount')" translate>
+ Invalid account
+ </ng-container>
+ </fims-account-select>
+ <fims-account-select title="{{'Expense account(Expense accounts only)' | translate}}" formControlName="expenseAccountIdentifier" [type]="'EXPENSE'">
+ <ng-container *ngIf="!formGroup.get('expenseAccountIdentifier').pristine && formGroup.get('expenseAccountIdentifier').hasError('required')" translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="formGroup.get('expenseAccountIdentifier').hasError('invalidAccount')" translate>
+ Invalid account
+ </ng-container>
+ </fims-account-select>
+ <fims-account-select title="{{'Accrue account(Liability accounts only)' | translate}}" formControlName="accrueAccountIdentifier" [type]="'LIABILITY'" *ngIf="formGroup.get('accrueAccountIdentifier').status !== 'DISABLED'">
+ <ng-container *ngIf="!formGroup.get('accrueAccountIdentifier').pristine && formGroup.get('accrueAccountIdentifier').hasError('required')" translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="formGroup.get('accrueAccountIdentifier').hasError('invalidAccount')" translate>
+ Invalid account
+ </ng-container>
+ </fims-account-select>
+ <fims-ledger-select title="{{'Equity ledger(Equity ledgers only)' | translate}}" formControlName="equityLedgerIdentifier" [type]="'EQUITY'">
+ <ng-container *ngIf="!formGroup.get('equityLedgerIdentifier').pristine && formGroup.get('equityLedgerIdentifier').hasError('required')" translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="formGroup.get('equityLedgerIdentifier').hasError('invalidLedger')" translate>
+ Invalid ledger
+ </ng-container>
+ </fims-ledger-select>
+ </form>
+ <ng-template td-step-actions>
+ <fims-form-continue-action (onContinue)="chargesStep.open()"></fims-form-continue-action>
+ </ng-template>
+ </td-step>
+
+ <td-step #chargesStep label="{{'Fees' | translate}}" [state]="chargesForm.valid ? 'complete' : chargesForm.pristine ? 'none' : 'required'">
+ <fims-deposit-product-charges-form #chargesForm [actions]="actions" [formData]="charges"></fims-deposit-product-charges-form>
+ </td-step>
+
+ <td-step label="{{'Final step' | translate}}" [state]="'complete'">
+ <ng-template td-step-summary>
+ <fims-form-final-action
+ [resourceName]="'PRODUCT'"
+ [editMode]="editMode"
+ [disabled]="!isValid"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/depositAccount/form/form.component.ts b/src/app/depositAccount/form/form.component.ts
new file mode 100644
index 0000000..71cdba3
--- /dev/null
+++ b/src/app/depositAccount/form/form.component.ts
@@ -0,0 +1,222 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, ViewChild} from '@angular/core';
+import {ProductDefinition} from '../../services/depositAccount/domain/definition/product-definition.model';
+import {TdStepComponent} from '@covalent/core';
+import {AsyncValidatorFn, FormBuilder, FormControl, FormGroup, ValidatorFn, Validators} from '@angular/forms';
+import {FimsValidators} from '../../common/validator/validators';
+import {interestPayableOptionList} from '../domain/interest-payable-option-list.model';
+import {timeUnitOptionList} from '../domain/time-unit-option-list.model';
+import {DepositProductChargesFormComponent} from './charges/charges.component';
+import {Charge} from '../../services/depositAccount/domain/definition/charge.model';
+import {Currency} from '../../services/currency/domain/currency.model';
+import {Action} from '../../services/depositAccount/domain/definition/action.model';
+import {typeOptionList} from '../domain/type-option-list.model';
+import {accountExists} from '../../common/validator/account-exists.validator';
+import {AccountingService} from '../../services/accounting/accounting.service';
+import {ledgerExists} from '../../common/validator/ledger-exists.validator';
+import {Subscription} from 'rxjs/Subscription';
+import {Type} from '../../services/depositAccount/domain/type.model';
+
+@Component({
+ selector: 'fims-deposit-product-form',
+ templateUrl: './form.component.html'
+})
+export class DepositProductFormComponent implements OnInit, OnDestroy, OnChanges {
+
+ private termChangeSubscription: Subscription;
+
+ private typeChangeSubscription: Subscription;
+
+ interestPayableOptions = interestPayableOptionList;
+
+ timeUnitOptions = timeUnitOptionList;
+
+ typeOptions = typeOptionList;
+
+ formGroup: FormGroup;
+
+ @ViewChild('detailsStep') step: TdStepComponent;
+
+ @ViewChild('chargesForm') chargesForm: DepositProductChargesFormComponent;
+ charges: Charge[];
+
+ @Input('editMode') editMode: boolean;
+
+ @Input('definition') definition: ProductDefinition;
+
+ @Input('currencies') currencies: Currency[];
+
+ @Input('actions') actions: Action[];
+
+ @Output('onSave') onSave = new EventEmitter<ProductDefinition>();
+
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder, private accountingService: AccountingService) {
+ this.formGroup = this.formBuilder.group({
+ identifier: ['', [Validators.required, Validators.minLength(3), Validators.maxLength(32), FimsValidators.urlSafe]],
+ type: ['', [Validators.required]],
+ name: ['', [Validators.required, Validators.maxLength(256)]],
+ description: ['', Validators.maxLength(2048)],
+ currencyCode: ['', [Validators.required]],
+ minimumBalance: ['', [Validators.required]],
+ fixedTermEnabled: [false],
+ interest: ['', [Validators.required, FimsValidators.minValue(0)]],
+ flexible: ['', [Validators.required]],
+ termPeriod: [''],
+ termTimeUnit: [''],
+ termInterestPayable: ['', [Validators.required]],
+ cashAccountIdentifier: ['', [Validators.required], accountExists(this.accountingService)],
+ expenseAccountIdentifier: ['', [Validators.required], accountExists(this.accountingService)],
+ equityLedgerIdentifier: ['', [Validators.required], ledgerExists(this.accountingService)],
+ accrueAccountIdentifier: ['', [Validators.required], accountExists(this.accountingService)]
+ });
+
+ this.termChangeSubscription = this.formGroup.get('fixedTermEnabled').valueChanges
+ .startWith(null)
+ .subscribe(enabled => this.toggleFixedTerm(enabled));
+
+ this.typeChangeSubscription = this.formGroup.get('type').valueChanges
+ .startWith(null)
+ .subscribe(type => this.toggleType(type));
+ }
+
+ ngOnInit(): void {
+ this.step.open();
+ }
+
+ ngOnDestroy(): void {
+ this.termChangeSubscription.unsubscribe();
+ this.typeChangeSubscription.unsubscribe();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ this.charges = this.definition.charges;
+
+ const interestDisabled = this.editMode && !this.definition.flexible;
+
+ const fixedTermEnabled: boolean = this.hasPeriodOrTimeUnit(this.definition);
+
+ this.formGroup.reset({
+ identifier: this.definition.identifier,
+ type: this.definition.type,
+ name: this.definition.name,
+ description: this.definition.description,
+ currencyCode: this.definition.currency.code,
+ minimumBalance: this.definition.minimumBalance.toFixed(2),
+ fixedTermEnabled: fixedTermEnabled,
+ interest: { value: this.definition.interest.toFixed(2), disabled: interestDisabled },
+ flexible: { value: this.definition.flexible, disabled: this.editMode },
+ termPeriod: this.definition.term.period,
+ termTimeUnit: this.definition.term.timeUnit,
+ termInterestPayable: this.definition.term.interestPayable,
+ cashAccountIdentifier: this.definition.cashAccountIdentifier,
+ expenseAccountIdentifier: this.definition.expenseAccountIdentifier,
+ accrueAccountIdentifier: this.definition.accrueAccountIdentifier,
+ equityLedgerIdentifier: this.definition.equityLedgerIdentifier
+ });
+ }
+
+ toggleFixedTerm(enabled: boolean): void {
+ const termPeriodControl: FormControl = this.formGroup.get('termPeriod') as FormControl;
+ const termTimeUnitControl: FormControl = this.formGroup.get('termTimeUnit') as FormControl;
+
+ if (enabled) {
+ this.enable(termPeriodControl, [Validators.required, FimsValidators.minValue(1), FimsValidators.maxScale(0)]);
+ this.enable(termTimeUnitControl, [Validators.required]);
+ } else {
+ this.disable(termPeriodControl);
+ this.disable(termTimeUnitControl);
+ }
+ }
+
+ toggleType(type: Type): void {
+ const enableAccrueAccount = type !== 'SHARE';
+
+ const accrueAccountControl: FormControl = this.formGroup.get('accrueAccountIdentifier') as FormControl;
+
+ if (enableAccrueAccount) {
+ this.enable(accrueAccountControl, [Validators.required], accountExists(this.accountingService));
+ } else {
+ this.disable(accrueAccountControl);
+ }
+ }
+
+ private enable(formControl: FormControl, validators: ValidatorFn[], asyncValidator?: AsyncValidatorFn): void {
+ formControl.enable();
+ formControl.setValidators(validators);
+ formControl.setAsyncValidators(asyncValidator);
+ formControl.updateValueAndValidity();
+ }
+
+ private disable(formControl: FormControl): void {
+ formControl.disable();
+ formControl.clearValidators();
+ formControl.updateValueAndValidity();
+ }
+
+ hasPeriodOrTimeUnit(product: ProductDefinition): boolean {
+ return !!product.term.timeUnit || !!product.term.period;
+ }
+
+ save(): void {
+ const foundCurrency = this.currencies.find(currency => currency.code === this.formGroup.get('currencyCode').value);
+
+ const isShare = this.formGroup.get('type').value === 'SHARE';
+
+ const fixedTerm: boolean = this.formGroup.get('fixedTermEnabled').value === true;
+
+ const definition: ProductDefinition = {
+ identifier: this.formGroup.get('identifier').value,
+ type: this.formGroup.get('type').value,
+ name: this.formGroup.get('name').value,
+ description: this.formGroup.get('description').value,
+ minimumBalance: parseFloat(this.formGroup.get('minimumBalance').value),
+ interest: parseFloat(this.formGroup.get('interest').value),
+ flexible: this.formGroup.get('flexible').value,
+ term: {
+ period: fixedTerm ? this.formGroup.get('termPeriod').value : undefined,
+ timeUnit: fixedTerm ? this.formGroup.get('termTimeUnit').value : undefined,
+ interestPayable: this.formGroup.get('termInterestPayable').value
+ },
+ currency: {
+ code: foundCurrency.code,
+ name: foundCurrency.name,
+ sign: foundCurrency.sign,
+ scale: foundCurrency.digits
+ },
+ charges: this.chargesForm.formData,
+ expenseAccountIdentifier: this.formGroup.get('expenseAccountIdentifier').value,
+ equityLedgerIdentifier: this.formGroup.get('equityLedgerIdentifier').value,
+ cashAccountIdentifier: this.formGroup.get('cashAccountIdentifier').value,
+ accrueAccountIdentifier: !isShare ? this.formGroup.get('accrueAccountIdentifier').value : undefined
+ };
+
+ this.onSave.emit(definition);
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+ get isValid(): boolean {
+ return this.formGroup.valid && this.chargesForm.valid;
+ }
+}
diff --git a/src/app/depositAccount/product-definition-exists.guard.ts b/src/app/depositAccount/product-definition-exists.guard.ts
new file mode 100644
index 0000000..6f184de
--- /dev/null
+++ b/src/app/depositAccount/product-definition-exists.guard.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {of} from 'rxjs/observable/of';
+import {Observable} from 'rxjs/Observable';
+import {LoadAction} from './store/product.actions';
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import {Injectable} from '@angular/core';
+import * as fromProducts from './store/index';
+import {DepositAccountStore} from './store/index';
+import {DepositAccountService} from '../services/depositAccount/deposit-account.service';
+import {ExistsGuardService} from '../common/guards/exists-guard';
+
+@Injectable()
+export class ProductDefinitionExistsGuard implements CanActivate {
+
+ constructor(private store: DepositAccountStore,
+ private accountService: DepositAccountService,
+ private existsGuardService: ExistsGuardService) {}
+
+ hasProductInStore(id: string): Observable<boolean> {
+ const timestamp$ = this.store.select(fromProducts.getProductsLoadedAt)
+ .map(loadedAt => loadedAt[id]);
+
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ hasProductInApi(id: string): Observable<boolean> {
+ const getProduct = this.accountService.findProductDefinition(id)
+ .map(productEntity => new LoadAction({
+ resource: productEntity
+ }))
+ .do((action: LoadAction) => this.store.dispatch(action))
+ .map(product => !!product);
+
+ return this.existsGuardService.routeTo404OnError(getProduct);
+ }
+
+ hasProduct(id: string): Observable<boolean> {
+ return this.hasProductInStore(id)
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+
+ return this.hasProductInApi(id);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasProduct(route.params['id']);
+ }
+}
diff --git a/src/app/depositAccount/store/dividends/dividend.actions.ts b/src/app/depositAccount/store/dividends/dividend.actions.ts
new file mode 100644
index 0000000..10cec0b
--- /dev/null
+++ b/src/app/depositAccount/store/dividends/dividend.actions.ts
@@ -0,0 +1,71 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {type} from '../../../store/util';
+import {RoutePayload} from '../../../common/store/route-payload';
+import {Action} from '@ngrx/store';
+import {DividendDistribution} from '../../../services/depositAccount/domain/definition/dividend-distribution.model';
+
+export const LOAD_ALL = type('[Deposit Product Definition Dividend] Load All');
+export const LOAD_ALL_COMPLETE = type('[Deposit Product Definition Dividend] Load All Complete');
+
+export const CREATE = type('[Deposit Product Definition Dividend] Create');
+export const CREATE_SUCCESS = type('[Deposit Product Definition Dividend] Create Success');
+export const CREATE_FAIL = type('[Deposit Product Definition Dividend] Create Fail');
+
+export interface DividendPayload extends RoutePayload {
+ productDefinitionId: string;
+ dividendDistribution: DividendDistribution;
+}
+
+export class LoadAllAction implements Action {
+ readonly type = LOAD_ALL;
+
+ constructor(public payload: string) { }
+}
+
+export class LoadAllCompleteAction implements Action {
+ readonly type = LOAD_ALL_COMPLETE;
+
+ constructor(public payload: DividendDistribution[]) { }
+}
+
+export class CreateDividendDistributionAction implements Action {
+ readonly type = CREATE;
+
+ constructor(public payload: DividendPayload) { }
+}
+
+export class CreateDividendDistributionSuccessAction implements Action {
+ readonly type = CREATE_SUCCESS;
+
+ constructor(public payload: DividendPayload) { }
+}
+
+export class CreateDividendDistributionFailAction implements Action {
+ readonly type = CREATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export type Actions
+ = LoadAllAction
+ | LoadAllCompleteAction
+ | CreateDividendDistributionAction
+ | CreateDividendDistributionSuccessAction
+ | CreateDividendDistributionFailAction;
diff --git a/src/app/depositAccount/store/dividends/dividends.reducer.ts b/src/app/depositAccount/store/dividends/dividends.reducer.ts
new file mode 100644
index 0000000..c84996e
--- /dev/null
+++ b/src/app/depositAccount/store/dividends/dividends.reducer.ts
@@ -0,0 +1,61 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {DividendDistribution} from '../../../services/depositAccount/domain/definition/dividend-distribution.model';
+import * as dividend from './dividend.actions';
+
+export interface State {
+ entities: DividendDistribution[];
+}
+
+export const initialState: State = {
+ entities: []
+};
+
+export function reducer(state = initialState, action: dividend.Actions): State {
+
+ switch (action.type) {
+
+ case dividend.LOAD_ALL: {
+ return initialState;
+ }
+
+ case dividend.LOAD_ALL_COMPLETE: {
+ const entities: DividendDistribution[] = action.payload;
+
+ return {
+ entities
+ };
+ }
+
+ case dividend.CREATE_SUCCESS: {
+ const entity: DividendDistribution = action.payload.dividendDistribution;
+
+ return {
+ entities: [...state.entities, entity]
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+export const getDividends = (state: State) => state.entities;
+
diff --git a/src/app/depositAccount/store/dividends/effects/notification.effects.ts b/src/app/depositAccount/store/dividends/effects/notification.effects.ts
new file mode 100644
index 0000000..a303e4d
--- /dev/null
+++ b/src/app/depositAccount/store/dividends/effects/notification.effects.ts
@@ -0,0 +1,39 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {NotificationService, NotificationType} from '../../../../services/notification/notification.service';
+import {Action} from '@ngrx/store';
+import {Observable} from 'rxjs/Observable';
+import {Actions, Effect} from '@ngrx/effects';
+import {Injectable} from '@angular/core';
+import * as dividendActions from '../dividend.actions';
+
+@Injectable()
+export class DepositProductDividendNotificationEffects {
+
+ @Effect({dispatch: false})
+ createDividendDistributionSuccess$: Observable<Action> = this.actions$
+ .ofType(dividendActions.CREATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Dividend is going to be distributed'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+
+}
diff --git a/src/app/depositAccount/store/dividends/effects/route.effects.ts b/src/app/depositAccount/store/dividends/effects/route.effects.ts
new file mode 100644
index 0000000..2e24c90
--- /dev/null
+++ b/src/app/depositAccount/store/dividends/effects/route.effects.ts
@@ -0,0 +1,38 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as dividendActions from '../dividend.actions';
+import {Action} from '@ngrx/store';
+import {Observable} from 'rxjs/Observable';
+import {Actions, Effect} from '@ngrx/effects';
+import {Router} from '@angular/router';
+import {Injectable} from '@angular/core';
+
+@Injectable()
+export class DepositProductDividendRouteEffects {
+
+ @Effect({dispatch: false})
+ createDividendDistributionSuccess$: Observable<Action> = this.actions$
+ .ofType(dividendActions.CREATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], {relativeTo: payload.activatedRoute}));
+
+ constructor(private actions$: Actions, private router: Router) {
+ }
+
+}
diff --git a/src/app/depositAccount/store/dividends/effects/service.effects.ts b/src/app/depositAccount/store/dividends/effects/service.effects.ts
new file mode 100644
index 0000000..9b817cb
--- /dev/null
+++ b/src/app/depositAccount/store/dividends/effects/service.effects.ts
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {DepositAccountService} from '../../../../services/depositAccount/deposit-account.service';
+import {Actions, Effect} from '@ngrx/effects';
+import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+import * as dividendActions from '../dividend.actions';
+
+@Injectable()
+export class DepositProductDividendApiEffects {
+
+ @Effect()
+ loadAll$: Observable<Action> = this.actions$
+ .ofType(dividendActions.LOAD_ALL)
+ .switchMap((action) => {
+ return this.depositService.fetchDividendDistributions(action.payload)
+ .map(dividendDistributions => new dividendActions.LoadAllCompleteAction(dividendDistributions))
+ .catch(() => of(new dividendActions.LoadAllCompleteAction([])));
+ });
+
+ @Effect()
+ createDividendDistribution$: Observable<Action> = this.actions$
+ .ofType(dividendActions.CREATE)
+ .map((action: dividendActions.CreateDividendDistributionAction) => action.payload)
+ .mergeMap(payload =>
+ this.depositService.distributeDividend(payload.productDefinitionId, payload.dividendDistribution)
+ .map(() => new dividendActions.CreateDividendDistributionSuccessAction(payload))
+ .catch((error) => of(new dividendActions.CreateDividendDistributionFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private depositService: DepositAccountService) {
+ }
+
+}
diff --git a/src/app/depositAccount/store/effects/notification.effects.ts b/src/app/depositAccount/store/effects/notification.effects.ts
new file mode 100644
index 0000000..ed5fb83
--- /dev/null
+++ b/src/app/depositAccount/store/effects/notification.effects.ts
@@ -0,0 +1,62 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {NotificationService, NotificationType} from '../../../services/notification/notification.service';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as definitionActions from '../product.actions';
+
+@Injectable()
+export class DepositProductDefinitionNotificationEffects {
+
+ @Effect({dispatch: false})
+ createProductDefinitionSuccess$: Observable<Action> = this.actions$
+ .ofType(definitionActions.CREATE_SUCCESS, definitionActions.UPDATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Product is going to be saved'
+ }));
+
+ @Effect({dispatch: false})
+ deleteProductDefinitionSuccess$: Observable<Action> = this.actions$
+ .ofType(definitionActions.DELETE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Product is going to be deleted'
+ }));
+
+ @Effect({dispatch: false})
+ deleteProductDefinitionFail$: Observable<Action> = this.actions$
+ .ofType(definitionActions.DELETE_FAIL)
+ .do(() => this.notificationService.send({
+ type: NotificationType.ALERT,
+ message: 'Product is already assigned to a member.'
+ }));
+
+ @Effect({dispatch: false})
+ executeCommandSuccess$: Observable<Action> = this.actions$
+ .ofType(definitionActions.EXECUTE_COMMAND_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Product is going to be updated'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+}
diff --git a/src/app/depositAccount/store/effects/route.effects.ts b/src/app/depositAccount/store/effects/route.effects.ts
new file mode 100644
index 0000000..ea79dc0
--- /dev/null
+++ b/src/app/depositAccount/store/effects/route.effects.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Action} from '@ngrx/store';
+import {Observable} from 'rxjs/Observable';
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Router} from '@angular/router';
+import * as definitionActions from '../product.actions';
+
+@Injectable()
+export class DepositProductDefinitionRouteEffects {
+
+ @Effect({ dispatch: false })
+ createProductDefinitionSuccess$: Observable<Action> = this.actions$
+ .ofType(definitionActions.CREATE_SUCCESS, definitionActions.UPDATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute }));
+
+ @Effect({ dispatch: false })
+ deleteProductDefinitionSuccess$: Observable<Action> = this.actions$
+ .ofType(definitionActions.DELETE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../../../'], { relativeTo: payload.activatedRoute }));
+
+ constructor(private actions$: Actions, private router: Router) { }
+
+}
diff --git a/src/app/depositAccount/store/effects/service.effects.ts b/src/app/depositAccount/store/effects/service.effects.ts
new file mode 100644
index 0000000..f169348
--- /dev/null
+++ b/src/app/depositAccount/store/effects/service.effects.ts
@@ -0,0 +1,98 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as definitionActions from '../product.actions';
+import {Action} from '@ngrx/store';
+import {Observable} from 'rxjs/Observable';
+import {Actions, Effect} from '@ngrx/effects';
+import {Injectable} from '@angular/core';
+import {DepositAccountService} from '../../../services/depositAccount/deposit-account.service';
+import {of} from 'rxjs/observable/of';
+import {emptySearchResult} from '../../../common/store/search.reducer';
+
+@Injectable()
+export class DepositProductDefinitionApiEffects {
+
+ @Effect()
+ search$: Observable<Action> = this.actions$
+ .ofType(definitionActions.SEARCH)
+ .debounceTime(300)
+ .switchMap(() => {
+ const nextSearch$ = this.actions$.ofType(definitionActions.SEARCH).skip(1);
+
+ return this.depositService.fetchProductDefinitions()
+ .takeUntil(nextSearch$)
+ .map(products => new definitionActions.SearchCompleteAction({
+ elements: products,
+ totalElements: products.length,
+ totalPages: 1
+ }))
+ .catch(() => of(new definitionActions.SearchCompleteAction(emptySearchResult())));
+ });
+
+ @Effect()
+ createProduct$: Observable<Action> = this.actions$
+ .ofType(definitionActions.CREATE)
+ .map((action: definitionActions.CreateProductDefinitionAction) => action.payload)
+ .mergeMap(payload =>
+ this.depositService.createProductDefinition(payload.productDefinition)
+ .map(() => new definitionActions.CreateProductDefinitionSuccessAction({
+ resource: payload.productDefinition,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new definitionActions.CreateProductDefinitionFailAction(error)))
+ );
+
+ @Effect()
+ updateProduct$: Observable<Action> = this.actions$
+ .ofType(definitionActions.UPDATE)
+ .map((action: definitionActions.UpdateProductDefinitionAction) => action.payload)
+ .mergeMap(payload =>
+ this.depositService.updateProductDefinition(payload.productDefinition)
+ .map(() => new definitionActions.UpdateProductDefinitionSuccessAction({
+ resource: payload.productDefinition,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new definitionActions.UpdateProductDefinitionFailAction(error)))
+ );
+
+ @Effect()
+ deleteProduct$: Observable<Action> = this.actions$
+ .ofType(definitionActions.DELETE)
+ .map((action: definitionActions.DeleteProductDefinitionAction) => action.payload)
+ .mergeMap(payload =>
+ this.depositService.deleteProductDefinition(payload.productDefinition.identifier)
+ .map(() => new definitionActions.DeleteProductDefinitionSuccessAction({
+ resource: payload.productDefinition,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new definitionActions.DeleteProductDefinitionFailAction(error)))
+ );
+
+ @Effect()
+ executeCommand$: Observable<Action> = this.actions$
+ .ofType(definitionActions.EXECUTE_COMMAND)
+ .map((action: definitionActions.ExecuteCommandAction) => action.payload)
+ .mergeMap(payload =>
+ this.depositService.processCommand(payload.definitionId, payload.command)
+ .map(() => new definitionActions.ExecuteCommandSuccessAction(payload))
+ .catch((error) => of(new definitionActions.ExecuteCommandFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private depositService: DepositAccountService) { }
+}
diff --git a/src/app/depositAccount/store/index.ts b/src/app/depositAccount/store/index.ts
new file mode 100644
index 0000000..9d1b0f1
--- /dev/null
+++ b/src/app/depositAccount/store/index.ts
@@ -0,0 +1,88 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ActionReducer, Store} from '@ngrx/store';
+import {createReducer} from '../../store/index';
+import {createFormReducer, FormState, getFormError} from '../../common/store/form.reducer';
+import {createResourceReducer, getResourceLoadedAt, getResourceSelected, ResourceState} from '../../common/store/resource.reducer';
+
+import * as fromRoot from '../../store';
+import * as fromProducts from './products.reducer';
+import * as fromDividends from './dividends/dividends.reducer';
+import {createSelector} from 'reselect';
+import {
+ createSearchReducer,
+ getSearchEntities,
+ getSearchTotalElements,
+ getSearchTotalPages,
+ SearchState
+} from '../../common/store/search.reducer';
+
+export interface State extends fromRoot.State {
+ depositProducts: ResourceState;
+ depositProductForm: FormState;
+ depositProductSearch: SearchState;
+ depositProductDividends: fromDividends.State;
+}
+
+const reducers = {
+ depositProducts: createResourceReducer('Deposit Product Definition', fromProducts.reducer),
+ depositProductForm: createFormReducer('Deposit Product Definition'),
+ depositProductSearch: createSearchReducer('Deposit Product Definition'),
+ depositProductDividends: fromDividends.reducer
+};
+
+export const depositAccountModuleReducer: ActionReducer<State> = createReducer(reducers);
+
+export class DepositAccountStore extends Store<State> {}
+
+export function depositAccountStoreFactory(appStore: Store<fromRoot.State>) {
+ appStore.replaceReducer(depositAccountModuleReducer);
+ return appStore;
+}
+
+export const getProductsState = (state: State) => state.depositProducts;
+
+export const getProductFormState = (state: State) => state.depositProductForm;
+export const getProductFormError = createSelector(getProductFormState, getFormError);
+
+export const getProductsLoadedAt = createSelector(getProductsState, getResourceLoadedAt);
+export const getSelectedProduct = createSelector(getProductsState, getResourceSelected);
+
+/**
+ * Product search selector
+ */
+export const getProductSearchState = (state: State) => state.depositProductSearch;
+
+export const getSearchProducts = createSelector(getProductSearchState, getSearchEntities);
+export const getProductSearchTotalElements = createSelector(getProductSearchState, getSearchTotalElements);
+export const getProductSearchTotalPages = createSelector(getProductSearchState, getSearchTotalPages);
+
+export const getProductSearchResults = createSelector(getSearchProducts, getProductSearchTotalPages, getProductSearchTotalElements,
+ (products, totalPages, totalElements) => {
+ return {
+ products: products,
+ totalPages: totalPages,
+ totalElements: totalElements
+ };
+});
+
+
+export const getProductDividendsState = (state: State) => state.depositProductDividends;
+
+export const getDividends = createSelector(getProductDividendsState, fromDividends.getDividends);
diff --git a/src/app/depositAccount/store/product.actions.ts b/src/app/depositAccount/store/product.actions.ts
new file mode 100644
index 0000000..0f19505
--- /dev/null
+++ b/src/app/depositAccount/store/product.actions.ts
@@ -0,0 +1,178 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {type} from '../../store/util';
+import {RoutePayload} from '../../common/store/route-payload';
+import {ProductDefinition} from '../../services/depositAccount/domain/definition/product-definition.model';
+import {Action} from '@ngrx/store';
+import {SearchResult} from '../../common/store/search.reducer';
+import {
+ CreateResourceSuccessPayload,
+ DeleteResourceSuccessPayload,
+ LoadResourcePayload,
+ SelectResourcePayload,
+ UpdateResourceSuccessPayload
+} from '../../common/store/resource.reducer';
+import {ProductDefinitionCommand} from '../../services/depositAccount/domain/definition/product-definition-command.model';
+
+export const SEARCH = type('[Deposit Product Definition] Search');
+export const SEARCH_COMPLETE = type('[Deposit Product Definition] Search Complete');
+
+export const LOAD = type('[Deposit Product Definition] Load');
+export const SELECT = type('[Deposit Product Definition] Select');
+
+export const CREATE = type('[Deposit Product Definition] Create');
+export const CREATE_SUCCESS = type('[Deposit Product Definition] Create Success');
+export const CREATE_FAIL = type('[Deposit Product Definition] Create Fail');
+
+export const UPDATE = type('[Deposit Product Definition] Update');
+export const UPDATE_SUCCESS = type('[Deposit Product Definition] Update Success');
+export const UPDATE_FAIL = type('[Deposit Product Definition] Update Fail');
+
+export const DELETE = type('[Deposit Product Definition] Delete');
+export const DELETE_SUCCESS = type('[Deposit Product Definition] Delete Success');
+export const DELETE_FAIL = type('[Deposit Product Definition] Delete Fail');
+
+export const RESET_FORM = type('[Deposit Product Definition] Reset Form');
+
+export const EXECUTE_COMMAND = type('[Deposit Product Definition] Execute Command');
+export const EXECUTE_COMMAND_SUCCESS = type('[Deposit Product Definition] Execute Command Success');
+export const EXECUTE_COMMAND_FAIL = type('[Deposit Product Definition] Execute Command Fail');
+
+export interface ProductDefinitionRoutePayload extends RoutePayload {
+ productDefinition: ProductDefinition;
+}
+
+export interface ExecuteCommandPayload extends RoutePayload {
+ definitionId: string;
+ command: ProductDefinitionCommand;
+}
+
+export class SearchAction implements Action {
+ readonly type = SEARCH;
+
+ constructor() { }
+}
+
+export class SearchCompleteAction implements Action {
+ readonly type = SEARCH_COMPLETE;
+
+ constructor(public payload: SearchResult) { }
+}
+
+export class LoadAction implements Action {
+ readonly type = LOAD;
+
+ constructor(public payload: LoadResourcePayload) { }
+}
+
+export class SelectAction implements Action {
+ readonly type = SELECT;
+
+ constructor(public payload: SelectResourcePayload) { }
+}
+
+export class CreateProductDefinitionAction implements Action {
+ readonly type = CREATE;
+
+ constructor(public payload: ProductDefinitionRoutePayload) { }
+}
+
+export class CreateProductDefinitionSuccessAction implements Action {
+ readonly type = CREATE_SUCCESS;
+
+ constructor(public payload: CreateResourceSuccessPayload) { }
+}
+
+export class CreateProductDefinitionFailAction implements Action {
+ readonly type = CREATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class UpdateProductDefinitionAction implements Action {
+ readonly type = UPDATE;
+
+ constructor(public payload: ProductDefinitionRoutePayload) { }
+}
+
+export class UpdateProductDefinitionSuccessAction implements Action {
+ readonly type = UPDATE_SUCCESS;
+
+ constructor(public payload: UpdateResourceSuccessPayload) { }
+}
+
+export class UpdateProductDefinitionFailAction implements Action {
+ readonly type = UPDATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class DeleteProductDefinitionAction implements Action {
+ readonly type = DELETE;
+
+ constructor(public payload: ProductDefinitionRoutePayload) { }
+}
+
+export class DeleteProductDefinitionSuccessAction implements Action {
+ readonly type = DELETE_SUCCESS;
+
+ constructor(public payload: DeleteResourceSuccessPayload) { }
+}
+
+export class DeleteProductDefinitionFailAction implements Action {
+ readonly type = DELETE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class ExecuteCommandAction implements Action {
+ readonly type = EXECUTE_COMMAND;
+
+ constructor(public payload: ExecuteCommandPayload) { }
+}
+
+export class ExecuteCommandSuccessAction implements Action {
+ readonly type = EXECUTE_COMMAND_SUCCESS;
+
+ constructor(public payload: ExecuteCommandPayload) { }
+}
+
+export class ExecuteCommandFailAction implements Action {
+ readonly type = EXECUTE_COMMAND_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export type Actions
+ = SearchAction
+ | SearchCompleteAction
+ | LoadAction
+ | SelectAction
+ | CreateProductDefinitionAction
+ | CreateProductDefinitionSuccessAction
+ | CreateProductDefinitionFailAction
+ | UpdateProductDefinitionAction
+ | UpdateProductDefinitionSuccessAction
+ | UpdateProductDefinitionFailAction
+ | DeleteProductDefinitionAction
+ | DeleteProductDefinitionSuccessAction
+ | DeleteProductDefinitionFailAction
+ | ExecuteCommandAction
+ | ExecuteCommandSuccessAction
+ | ExecuteCommandFailAction;
diff --git a/src/app/depositAccount/store/products.reducer.ts b/src/app/depositAccount/store/products.reducer.ts
new file mode 100644
index 0000000..0421c7d
--- /dev/null
+++ b/src/app/depositAccount/store/products.reducer.ts
@@ -0,0 +1,64 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as productActions from '../store/product.actions';
+import {ResourceState} from '../../common/store/resource.reducer';
+import {ProductDefinitionCommand} from '../../services/depositAccount/domain/definition/product-definition-command.model';
+
+export const initialState: ResourceState = {
+ ids: [],
+ entities: {},
+ loadedAt: {},
+ selectedId: null,
+};
+
+export function reducer(state = initialState, action: productActions.Actions): ResourceState {
+
+ switch (action.type) {
+
+ case productActions.EXECUTE_COMMAND_SUCCESS: {
+ const payload = action.payload;
+
+ const definitionId = payload.definitionId;
+ const command: ProductDefinitionCommand = payload.command;
+
+ const definition = state.entities[definitionId];
+
+ let active = false;
+
+ if (command.action === 'ACTIVATE') {
+ active = true;
+ }
+
+ definition.active = active;
+
+ return {
+ ids: [ ...state.ids ],
+ entities: Object.assign({}, state.entities, {
+ [definition.identifier]: definition
+ }),
+ loadedAt: state.loadedAt,
+ selectedId: state.selectedId
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
diff --git a/src/app/employees/detail/employee.detail.component.html b/src/app/employees/detail/employee.detail.component.html
new file mode 100644
index 0000000..f4d6824
--- /dev/null
+++ b/src/app/employees/detail/employee.detail.component.html
@@ -0,0 +1,47 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{employee.givenName}} {{employee.surname}}" [navigateBackTo]="'/employees'">
+ <fims-layout-card-over-header-menu layout="row" layout-align="end center">
+ <td-search-box placeholder="{{'Search' | translate}}" (search)="searchEmployee($event)" [alwaysVisible]="false"></td-search-box>
+ <button mat-icon-button (click)="deleteEmployee()" title="{{'Delete this employee' | translate}}" *hasPermission="{ id: 'office_employees', accessLevel: 'DELETE' }"><mat-icon>delete</mat-icon></button>
+ </fims-layout-card-over-header-menu>
+ <mat-list>
+ <h3 mat-subheader translate>Assigned office</h3>
+ <mat-list-item>
+ <mat-icon matListAvatar>store</mat-icon>
+ <h3 matLine *ngIf="employee.assignedOffice">{{employee.assignedOffice}}</h3>
+ <h3 matLine *ngIf="!employee.assignedOffice" translate>No office assigned</h3>
+ </mat-list-item>
+ <h3 mat-subheader translate>Assigned role</h3>
+ <mat-list-item>
+ <mat-icon matListAvatar>lock</mat-icon>
+ <h3 matLine>{{user.role}}</h3>
+ </mat-list-item>
+ <h3 mat-subheader translate>Contact Information</h3>
+ <mat-list-item [ngSwitch]="detail.type" *ngFor="let detail of employee.contactDetails">
+ <mat-icon *ngSwitchCase="'EMAIL'" matListAvatar>email</mat-icon>
+ <mat-icon *ngSwitchCase="'PHONE'" matListAvatar>phone</mat-icon>
+ <mat-icon *ngSwitchCase="'MOBILE'" matListAvatar>smartphone</mat-icon>
+ <h3 matLine>{{detail.value}}</h3>
+ </mat-list-item>
+ <mat-list-item *ngIf="!employee.contactDetails.length">
+ <h3 matLine translate>No contact details available</h3>
+ </mat-list-item>
+ </mat-list>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Edit member ' | translate}}" icon="mode_edit" [link]="['edit']" [permission]="{ id: 'office_employees', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/employees/detail/employee.detail.component.ts b/src/app/employees/detail/employee.detail.component.ts
new file mode 100644
index 0000000..052020a
--- /dev/null
+++ b/src/app/employees/detail/employee.detail.component.ts
@@ -0,0 +1,96 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ActivatedRoute, Router} from '@angular/router';
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {Employee} from '../../services/office/domain/employee.model';
+import {TdDialogService} from '@covalent/core';
+import {Observable} from 'rxjs/Observable';
+import {Subscription} from 'rxjs/Subscription';
+import {User} from '../../services/identity/domain/user.model';
+import * as fromEmployee from '../store';
+import {DELETE, SelectAction} from '../store/employee.actions';
+import {EmployeesStore} from '../store/index';
+
+@Component({
+ selector: 'fims-employee-detail',
+ templateUrl: './employee.detail.component.html'
+})
+export class EmployeeDetailComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ private employeeSubscription: Subscription;
+
+ employee: Employee;
+
+ user: User;
+
+ constructor(private route: ActivatedRoute, private router: Router, private dialogService: TdDialogService,
+ private store: EmployeesStore) {}
+
+ ngOnInit(): void {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectAction(params['id']))
+ .subscribe(this.store);
+
+ this.employeeSubscription = this.store.select(fromEmployee.getSelectedEmployee)
+ .filter(employee => !!employee)
+ .subscribe(employee => this.employee = employee);
+
+ // TODO load user via store
+ this.route.data.subscribe(( data: { user: User }) => {
+ this.user = data.user;
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ this.employeeSubscription.unsubscribe();
+ }
+
+ searchEmployee(term): void {
+ if (!term) {
+ return;
+ }
+ this.goToOverviewPage(term);
+ }
+
+ confirmDeletion(): Observable<boolean> {
+ return this.dialogService.openConfirm({
+ message: 'Do you want to delete employee "' + this.employee.givenName + ' ' + this.employee.surname + '"?',
+ title: 'Confirm deletion',
+ acceptButton: 'DELETE EMPLOYEE',
+ }).afterClosed();
+ }
+
+ deleteEmployee(): void {
+ this.confirmDeletion()
+ .filter(accept => accept)
+ .subscribe(() => {
+ this.store.dispatch({ type: DELETE, payload: {
+ employee: this.employee,
+ activatedRoute: this.route
+ } });
+ });
+ }
+
+ goToOverviewPage(term?: string): void {
+ this.router.navigate(['../../'], { queryParams: { term: term }, relativeTo: this.route });
+ }
+}
diff --git a/src/app/employees/employee-exists.guard.ts b/src/app/employees/employee-exists.guard.ts
new file mode 100644
index 0000000..98e2b94
--- /dev/null
+++ b/src/app/employees/employee-exists.guard.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import {Injectable} from '@angular/core';
+import {OfficeService} from '../services/office/office.service';
+import * as fromEmployees from './store';
+import {Observable} from 'rxjs/Observable';
+import {LoadAction} from './store/employee.actions';
+import {of} from 'rxjs/observable/of';
+import {EmployeesStore} from './store/index';
+import {ExistsGuardService} from '../common/guards/exists-guard';
+
+@Injectable()
+export class EmployeeExistsGuard implements CanActivate {
+
+ constructor(private store: EmployeesStore,
+ private officeService: OfficeService,
+ private existsGuardService: ExistsGuardService) {}
+
+ hasEmployeeInStore(id: string): Observable<boolean> {
+ const timestamp$ = this.store.select(fromEmployees.getEmployeesLoadedAt)
+ .map(loadedAt => loadedAt[id]);
+
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ hasEmployeeInApi(id: string): Observable<boolean> {
+ const getEmployee$ = this.officeService.getEmployee(id)
+ .map(employeeEntity => new LoadAction({
+ resource: employeeEntity
+ }))
+ .do((action: LoadAction) => this.store.dispatch(action))
+ .map(employee => !!employee);
+
+ return this.existsGuardService.routeTo404OnError(getEmployee$);
+ }
+
+ hasEmployee(id: string): Observable<boolean> {
+ return this.hasEmployeeInStore(id)
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+
+ return this.hasEmployeeInApi(id);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasEmployee(route.params['id']);
+ }
+}
diff --git a/src/app/employees/employee.component.html b/src/app/employees/employee.component.html
new file mode 100644
index 0000000..5c563f5
--- /dev/null
+++ b/src/app/employees/employee.component.html
@@ -0,0 +1,32 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Manage employees' | translate}}">
+ <fims-layout-card-over-header-menu>
+ <td-search-box #searchBox placeholder="{{'Search' | translate}}" (search)="search($event)" [alwaysVisible]="false"></td-search-box>
+ </fims-layout-card-over-header-menu>
+ <fims-data-table flex
+ (onFetch)="fetchEmployees($event)"
+ (onActionCellClick)="rowSelect($event)"
+ [columns]="columns"
+ [data]="employeeData$ | async"
+ [loading]="loading$ | async"
+ [sortable]="true"
+ [pageable]="true">
+ </fims-data-table>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Create new employee' | translate}}" icon="add" [link]="['create']" [permission]="{ id: 'office_employees', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/employees/employee.component.ts b/src/app/employees/employee.component.ts
new file mode 100644
index 0000000..e9c35bf
--- /dev/null
+++ b/src/app/employees/employee.component.ts
@@ -0,0 +1,85 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import {ActivatedRoute, Params, Router} from '@angular/router';
+import {Employee} from '../services/office/domain/employee.model';
+import {FetchRequest} from '../services/domain/paging/fetch-request.model';
+import {TableData} from '../common/data-table/data-table.component';
+import {Store} from '@ngrx/store';
+import * as fromRoot from '../store';
+import {Observable} from 'rxjs/Observable';
+import {SEARCH} from '../store/employee/employee.actions';
+
+@Component({
+ selector: 'fims-employee',
+ templateUrl: './employee.component.html'
+})
+export class EmployeeComponent implements OnInit {
+
+ employeeData$: Observable<TableData>;
+
+ loading$: Observable<boolean>;
+
+ columns: any[] = [
+ { name: 'identifier', label: 'Id' },
+ { name: 'givenName', label: 'First Name' },
+ { name: 'surname', label: 'Last Name' }
+ ];
+
+ searchTerm: string;
+
+ private lastFetchRequest: FetchRequest = {};
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: Store<fromRoot.State>) {}
+
+ ngOnInit(): void {
+
+ this.employeeData$ = this.store.select(fromRoot.getEmployeeSearchResults)
+ .map(employeePage => ({
+ data: employeePage.employees,
+ totalElements: employeePage.totalElements,
+ totalPages: employeePage.totalPages
+ }));
+
+ this.loading$ = this.store.select(fromRoot.getEmployeeSearchLoading);
+
+ this.route.queryParams.subscribe((params: Params) => {
+ this.search(params['term']);
+ });
+ }
+
+ search(searchTerm: string): void {
+ this.searchTerm = searchTerm;
+ this.fetchEmployees();
+ }
+
+ rowSelect(row: Employee): void {
+ this.router.navigate(['detail', row.identifier], { relativeTo: this.route });
+ }
+
+ fetchEmployees(fetchRequest?: FetchRequest) {
+ if (fetchRequest) {
+ this.lastFetchRequest = fetchRequest;
+ }
+
+ this.lastFetchRequest.searchTerm = this.searchTerm;
+
+ this.store.dispatch({ type: SEARCH, payload: this.lastFetchRequest });
+ }
+}
diff --git a/src/app/employees/employee.module.ts b/src/app/employees/employee.module.ts
new file mode 100644
index 0000000..04d9a08
--- /dev/null
+++ b/src/app/employees/employee.module.ts
@@ -0,0 +1,84 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {NgModule} from '@angular/core';
+import {RouterModule} from '@angular/router';
+import {EmployeeComponent} from './employee.component';
+import {EmployeeRoutes} from './employee.routing';
+import {EmployeeFormComponent} from './form/form.component';
+import {CreateEmployeeFormComponent} from './form/create/create.form.component';
+import {EmployeeDetailComponent} from './detail/employee.detail.component';
+import {EditEmployeeFormComponent} from './form/edit/edit.form.component';
+import {UserResolver} from './user.resolver';
+import {FimsSharedModule} from '../common/common.module';
+import {EmployeeExistsGuard} from './employee-exists.guard';
+import {Store} from '@ngrx/store';
+import {EmployeesStore, employeeStoreFactory} from './store/index';
+import {EmployeeNotificationEffects} from './store/effects/notification.effects';
+import {EffectsModule} from '@ngrx/effects';
+import {EmployeeApiEffects} from './store/effects/service.effects';
+import {EmployeeRouteEffects} from './store/effects/route.effects';
+import {
+ MatButtonModule,
+ MatIconModule,
+ MatInputModule,
+ MatListModule,
+ MatOptionModule,
+ MatSelectModule,
+ MatToolbarModule
+} from '@angular/material';
+import {CovalentSearchModule, CovalentStepsModule} from '@covalent/core';
+import {TranslateModule} from '@ngx-translate/core';
+import {CommonModule} from '@angular/common';
+import {ReactiveFormsModule} from '@angular/forms';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(EmployeeRoutes),
+ FimsSharedModule,
+ ReactiveFormsModule,
+ CommonModule,
+ TranslateModule,
+ MatIconModule,
+ MatListModule,
+ MatToolbarModule,
+ MatOptionModule,
+ MatInputModule,
+ MatButtonModule,
+ MatSelectModule,
+ CovalentSearchModule,
+ CovalentStepsModule,
+
+ EffectsModule.run(EmployeeApiEffects),
+ EffectsModule.run(EmployeeRouteEffects),
+ EffectsModule.run(EmployeeNotificationEffects)
+ ],
+ declarations: [
+ EmployeeComponent,
+ EmployeeFormComponent,
+ CreateEmployeeFormComponent,
+ EditEmployeeFormComponent,
+ EmployeeDetailComponent
+ ],
+ providers: [
+ UserResolver,
+ EmployeeExistsGuard,
+ { provide: EmployeesStore, useFactory: employeeStoreFactory, deps: [Store]}
+ ]
+})
+export class EmployeeModule {}
diff --git a/src/app/employees/employee.routing.ts b/src/app/employees/employee.routing.ts
new file mode 100644
index 0000000..9fd1f2d
--- /dev/null
+++ b/src/app/employees/employee.routing.ts
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Routes} from '@angular/router';
+import {EmployeeComponent} from './employee.component';
+import {CreateEmployeeFormComponent} from './form/create/create.form.component';
+import {EmployeeDetailComponent} from './detail/employee.detail.component';
+import {EditEmployeeFormComponent} from './form/edit/edit.form.component';
+import {UserResolver} from './user.resolver';
+import {EmployeeExistsGuard} from './employee-exists.guard';
+
+export const EmployeeRoutes: Routes = [
+ {
+ path: '',
+ component: EmployeeComponent,
+ data: {title: 'Manage Employees', hasPermission: {id: 'office_employees', accessLevel: 'READ'}}
+ },
+ {
+ path: 'create',
+ component: CreateEmployeeFormComponent,
+ data: {title: 'Create Employee', hasPermission: {id: 'office_employees', accessLevel: 'CHANGE'}}
+ },
+ {
+ path: 'detail/:id/edit',
+ component: EditEmployeeFormComponent,
+ canActivate: [EmployeeExistsGuard],
+ resolve: {user: UserResolver},
+ data: {title: 'Edit Employee', hasPermission: {id: 'office_employees', accessLevel: 'CHANGE'}}
+ },
+ {
+ path: 'detail/:id',
+ component: EmployeeDetailComponent,
+ canActivate: [EmployeeExistsGuard],
+ resolve: {user: UserResolver},
+ data: {title: 'View Employee', hasPermission: {id: 'office_employees', accessLevel: 'READ'}}
+ }
+];
diff --git a/src/app/employees/form/create/create.form.component.html b/src/app/employees/form/create/create.form.component.html
new file mode 100644
index 0000000..a2fccd0
--- /dev/null
+++ b/src/app/employees/form/create/create.form.component.html
@@ -0,0 +1,24 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Create new employee' | translate}}">
+ <fims-employee-form-component #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [formData]="employeeFormData">
+ </fims-employee-form-component>
+</fims-layout-card-over>
diff --git a/src/app/employees/form/create/create.form.component.spec.ts b/src/app/employees/form/create/create.form.component.spec.ts
new file mode 100644
index 0000000..b27d416
--- /dev/null
+++ b/src/app/employees/form/create/create.form.component.spec.ts
@@ -0,0 +1,129 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {ReactiveFormsModule} from '@angular/forms';
+import {CovalentStepsModule} from '@covalent/core';
+import {EmployeeFormComponent, EmployeeSaveEvent} from '../form.component';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Observable} from 'rxjs/Observable';
+import {CreateEmployeeFormComponent} from './create.form.component';
+import {mapEmployee, mapUser} from '../form.mapper';
+import {EmployeesStore} from '../../store/index';
+import {CREATE} from '../../store/employee.actions';
+import {Store} from '@ngrx/store';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {MatCardModule, MatInputModule, MatOptionModule, MatSelectModule} from '@angular/material';
+import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
+import {FimsSharedModule} from '../../../common/common.module';
+
+const eventMock: EmployeeSaveEvent = {
+ detailForm: {
+ identifier: 'test',
+ firstName: 'test',
+ middleName: 'test',
+ lastName: 'test',
+ password: 'test',
+ role: 'test'
+ },
+ contactForm: {
+ email: 'test',
+ mobile: 'test',
+ phone: 'test'
+ },
+ officeForm: {
+ assignedOffice: 'test'
+ }
+};
+
+let router: Router;
+
+describe('Test employee form component', () => {
+
+ let fixture: ComponentFixture<CreateEmployeeFormComponent>;
+
+ let testComponent: CreateEmployeeFormComponent;
+
+ beforeEach(() => {
+ router = jasmine.createSpyObj('Router', ['navigate']);
+
+ TestBed.configureTestingModule({
+ declarations: [
+ EmployeeFormComponent,
+ CreateEmployeeFormComponent,
+ ],
+ imports: [
+ TranslateModule.forRoot(),
+ FimsSharedModule,
+ ReactiveFormsModule,
+ MatInputModule,
+ MatCardModule,
+ MatSelectModule,
+ MatOptionModule,
+ CovalentStepsModule,
+ NoopAnimationsModule
+ ],
+ providers: [
+ { provide: Router, useValue: router},
+ { provide: ActivatedRoute, useValue: {} },
+ {
+ provide: Store, useClass: class {
+ dispatch = jasmine.createSpy('dispatch');
+ select = jasmine.createSpy('select').and.returnValue(Observable.empty());
+ }},
+ {
+ provide: EmployeesStore, useClass: class {
+ dispatch = jasmine.createSpy('dispatch');
+ select = jasmine.createSpy('select').and.returnValue(Observable.empty());
+ }
+ }
+ ],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA]
+ });
+
+ fixture = TestBed.createComponent(CreateEmployeeFormComponent);
+ testComponent = fixture.componentInstance;
+ });
+
+ it('should test if employee is created', async(inject([EmployeesStore], (store: EmployeesStore) => {
+ fixture.detectChanges();
+
+ testComponent.onSave(eventMock);
+
+ fixture.whenStable().then(() => {
+ const employee = mapEmployee(eventMock);
+ const user = mapUser(eventMock);
+
+ expect(store.dispatch).toHaveBeenCalledWith({ type: CREATE, payload: {
+ employee: employee,
+ user: user,
+ activatedRoute: {}
+ }});
+ });
+ })));
+
+ xit('should test if error is set on 409', async(() => {
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ expect(testComponent.formComponent.detailForm.get('identifier').errors).toBeDefined();
+ expect(testComponent.formComponent.detailForm.get('identifier').errors['unique']).toBeTruthy();
+ });
+ }));
+});
diff --git a/src/app/employees/form/create/create.form.component.ts b/src/app/employees/form/create/create.form.component.ts
new file mode 100644
index 0000000..22a39c4
--- /dev/null
+++ b/src/app/employees/form/create/create.form.component.ts
@@ -0,0 +1,84 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {EmployeeFormComponent, EmployeeFormData, EmployeeSaveEvent} from '../form.component';
+import {mapEmployee, mapUser} from '../form.mapper';
+import {Employee} from '../../../services/office/domain/employee.model';
+import {UserWithPassword} from '../../../services/identity/domain/user-with-password.model';
+import * as fromEmployees from '../../store';
+import {Subscription} from 'rxjs/Subscription';
+import {CREATE, RESET_FORM} from '../../store/employee.actions';
+import {Error} from '../../../services/domain/error.model';
+import {EmployeesStore} from '../../store/index';
+
+@Component({
+ templateUrl: './create.form.component.html'
+})
+export class CreateEmployeeFormComponent implements OnInit, OnDestroy {
+
+ private formStateSubscription: Subscription;
+
+ @ViewChild('form') formComponent: EmployeeFormComponent;
+
+ employeeFormData: EmployeeFormData = {
+ user: { identifier: '', role: ''},
+ employee: { identifier: '', givenName: '', surname: '', contactDetails: [] }
+ };
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: EmployeesStore) {}
+
+ ngOnInit(): void {
+ this.formStateSubscription = this.store.select(fromEmployees.getEmployeeFormError)
+ .filter((error: Error) => !!error)
+ .subscribe((error: Error) => {
+ const detailForm = this.formComponent.detailForm;
+ const errors = detailForm.get('identifier').errors || {};
+ errors['unique'] = true;
+ detailForm.get('identifier').setErrors(errors);
+ this.formComponent.step.open();
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.formStateSubscription.unsubscribe();
+
+ this.store.dispatch({ type: RESET_FORM });
+ }
+
+ onSave(event: EmployeeSaveEvent): void {
+ const employee: Employee = mapEmployee(event);
+ const user: UserWithPassword = mapUser(event);
+
+ this.store.dispatch({ type: CREATE, payload: {
+ employee: employee,
+ user: user,
+ activatedRoute: this.route
+ }});
+
+ }
+
+ onCancel(): void {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/employees/form/edit/edit.form.component.html b/src/app/employees/form/edit/edit.form.component.html
new file mode 100644
index 0000000..efc62dc
--- /dev/null
+++ b/src/app/employees/form/edit/edit.form.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Edit employee' | translate}}">
+ <fims-employee-form-component #form
+ [editMode]="true"
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [formData]="formData | async">
+ </fims-employee-form-component>
+</fims-layout-card-over>
diff --git a/src/app/employees/form/edit/edit.form.component.spec.ts b/src/app/employees/form/edit/edit.form.component.spec.ts
new file mode 100644
index 0000000..13ca983
--- /dev/null
+++ b/src/app/employees/form/edit/edit.form.component.spec.ts
@@ -0,0 +1,140 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {ReactiveFormsModule} from '@angular/forms';
+import {CovalentStepsModule} from '@covalent/core';
+import {EditEmployeeFormComponent} from './edit.form.component';
+import {EmployeeFormComponent} from '../form.component';
+import {ActivatedRoute, Router} from '@angular/router';
+import {User} from '../../../services/identity/domain/user.model';
+import {Employee} from '../../../services/office/domain/employee.model';
+import {Observable} from 'rxjs/Observable';
+import {EmployeesStore} from '../../store/index';
+import {Store} from '@ngrx/store';
+import {UPDATE} from '../../store/employee.actions';
+import * as fromEmployees from '../../store';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {MatCardModule, MatInputModule, MatOptionModule, MatSelectModule} from '@angular/material';
+import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
+import {FimsSharedModule} from '../../../common/common.module';
+
+const userMock: User = {
+ identifier: 'test',
+ role: 'test'
+};
+
+const employeeMock: Employee = {
+ identifier: 'test',
+ assignedOffice: 'test',
+ givenName: 'test',
+ middleName: 'test',
+ surname: 'test',
+ contactDetails: [
+ {
+ group: 'BUSINESS',
+ type: 'EMAIL',
+ value: 'test',
+ preferenceLevel: 1
+ }
+ ]
+};
+
+const activatedRoute = {
+ data: Observable.of({
+ user: userMock
+ })
+};
+let router: Router;
+
+describe('Test employee form component', () => {
+
+ let fixture: ComponentFixture<EditEmployeeFormComponent>;
+
+ let testComponent: EditEmployeeFormComponent;
+
+ beforeEach(() => {
+ router = jasmine.createSpyObj('Router', ['navigate']);
+
+ TestBed.configureTestingModule({
+ declarations: [
+ EmployeeFormComponent,
+ EditEmployeeFormComponent,
+ ],
+ imports: [
+ TranslateModule.forRoot(),
+ FimsSharedModule,
+ ReactiveFormsModule,
+ MatInputModule,
+ MatCardModule,
+ MatSelectModule,
+ MatOptionModule,
+ CovalentStepsModule,
+ NoopAnimationsModule
+ ],
+ providers: [
+ { provide: Router, useValue: router},
+ { provide: ActivatedRoute, useValue: activatedRoute },
+ {
+ provide: Store, useClass: class {
+ dispatch = jasmine.createSpy('dispatch');
+ select = jasmine.createSpy('select').and.returnValue(Observable.empty());
+ }
+ },
+ {
+ provide: EmployeesStore, useClass: class {
+ dispatch = jasmine.createSpy('dispatch');
+ select = jasmine.createSpy('select').and.callFake(selector => {
+ if (selector === fromEmployees.getSelectedEmployee) {
+ return Observable.of(employeeMock);
+ }
+
+ return Observable.empty();
+ });
+ }
+ }
+ ],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA]
+ });
+
+ fixture = TestBed.createComponent(EditEmployeeFormComponent);
+ testComponent = fixture.componentInstance;
+ });
+
+ it('should test if employee is updated', async(inject([EmployeesStore], (store: EmployeesStore) => {
+ fixture.detectChanges();
+
+ testComponent.formComponent.detailForm.get('password').setValue('newPassword');
+
+ fixture.detectChanges();
+
+ testComponent.formComponent.save();
+
+ fixture.whenStable().then(() => {
+ expect(store.dispatch).toHaveBeenCalledWith({ type: UPDATE, payload: {
+ employee: employeeMock,
+ contactDetails: employeeMock.contactDetails,
+ role: userMock.role,
+ password: 'newPassword',
+ activatedRoute: activatedRoute
+ }});
+ });
+
+ })));
+});
diff --git a/src/app/employees/form/edit/edit.form.component.ts b/src/app/employees/form/edit/edit.form.component.ts
new file mode 100644
index 0000000..dd3e1d7
--- /dev/null
+++ b/src/app/employees/form/edit/edit.form.component.ts
@@ -0,0 +1,76 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {EmployeeFormComponent, EmployeeFormData, EmployeeSaveEvent} from '../form.component';
+import {mapContactDetails, mapEmployee} from '../form.mapper';
+import {Employee} from '../../../services/office/domain/employee.model';
+import {User} from '../../../services/identity/domain/user.model';
+import {UPDATE} from '../../store/employee.actions';
+import {Observable} from 'rxjs/Observable';
+import {EmployeesStore, getSelectedEmployee} from '../../store/index';
+
+@Component({
+ templateUrl: './edit.form.component.html'
+})
+export class EditEmployeeFormComponent implements OnInit {
+
+ @ViewChild('form') formComponent: EmployeeFormComponent;
+
+ formData: Observable<EmployeeFormData>;
+
+ employee: Employee;
+
+ user: User;
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: EmployeesStore) {}
+
+ ngOnInit() {
+ this.formData = Observable.combineLatest(
+ this.store.select(getSelectedEmployee),
+ this.route.data,
+ (employee: Employee, data: { user: User }) => {
+ return {
+ user: data.user,
+ employee: employee
+ };
+ }
+ );
+ }
+
+ onSave(event: EmployeeSaveEvent) {
+ const employee: Employee = mapEmployee(event);
+
+ this.store.dispatch({ type: UPDATE, payload: {
+ employee: employee,
+ contactDetails: mapContactDetails(event.contactForm),
+ role: event.detailForm.role,
+ password: event.detailForm.password,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel() {
+ this.navigateToOffice();
+ }
+
+ private navigateToOffice() {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/employees/form/form.component.html b/src/app/employees/form/form.component.html
new file mode 100644
index 0000000..dd618ee
--- /dev/null
+++ b/src/app/employees/form/form.component.html
@@ -0,0 +1,89 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Employee details' | translate}}" [state]="detailForm.valid ? 'complete' : detailForm.pristine ? 'none' : 'required'">
+ <form [formGroup]="detailForm" layout="column">
+ <fims-id-input [form]="detailForm" placeholder="Username" controlName="identifier" [readonly]="editMode"></fims-id-input>
+ <fims-text-input [form]="detailForm" controlName="firstName" placeholder="{{'First name' | translate}}"></fims-text-input>
+ <fims-text-input [form]="detailForm" controlName="middleName" placeholder="{{'Middle name(optional)' | translate}}"></fims-text-input>
+ <fims-text-input [form]="detailForm" controlName="lastName" placeholder="{{'Last name' | translate}}"></fims-text-input>
+ <mat-form-field layout-margin>
+ <mat-select formControlName="role" placeholder="{{ 'Role' | translate }}">
+ <mat-option *ngFor="let role of roles | async" [value]="role.identifier">
+ {{ role.identifier }}
+ </mat-option>
+ </mat-select>
+ <mat-error *ngIf="detailForm.get('role').hasError('required')" translate>Required</mat-error>
+ </mat-form-field>
+ <div layout="row">
+ <mat-form-field layout-margin flex>
+ <input matInput placeholder="{{'Password' | translate}}" type="password" formControlName="password" autocomplete="new-password" tdAutoTrim/>
+ <mat-error *ngIf="detailForm.get('password').hasError('required')" translate>
+ Required
+ </mat-error>
+ <mat-error *ngIf="detailForm.get('password').hasError('minlength')">
+ {{ 'Must have at least characters.' | translate:{ value: detailForm.get('password').getError('minlength')['requiredLength']} }}
+ </mat-error>
+ </mat-form-field>
+ </div>
+ </form>
+ <ng-template td-step-actions>
+ <fims-form-continue-action (onContinue)="officeStep.open()"></fims-form-continue-action>
+ </ng-template>
+ </td-step>
+
+ <td-step #officeStep label="{{'Assign employee to office(optional)' | translate}}"
+ [state]="officeForm.get('assignedOffice').value ? 'complete' : 'none'">
+
+ <fims-select-list #officeList flex
+ [data]="offices"
+ id="identifier"
+ listIcon="store"
+ [preSelection]="officeForm.get('assignedOffice').value ? [officeForm.get('assignedOffice').value] : []"
+ (onSearch)="searchOffice($event)"
+ (onSelectionChange)="assignOffice($event)"
+ title="{{'Assigned Office' | translate}}"
+ noResultsMessage="{{'No office was found.' | translate}}"
+ noSelectionMessage="{{'No office assigned to employee, yet.' | translate}}">
+ </fims-select-list>
+ <ng-template td-step-actions>
+ <fims-form-continue-action (onContinue)="contactStep.open()"></fims-form-continue-action>
+ </ng-template>
+ </td-step>
+
+ <td-step #contactStep label="{{'Employee contact(optional)' | translate}}" [state]="contactForm.valid && !contactForm.pristine ? 'complete' : contactForm.pristine ? 'none' : 'required'">
+ <form [formGroup]="contactForm" layout="column">
+ <fims-text-input [form]="contactForm" controlName="email" placeholder="{{'Email(optional)' | translate}}" type="email"></fims-text-input>
+ <fims-text-input [form]="contactForm" controlName="phone" placeholder="{{'Phone(optional)' | translate}}" type="tel"></fims-text-input>
+ <fims-text-input [form]="contactForm" controlName="mobile" placeholder="{{'Mobile(optional)' | translate}}" type="tel"></fims-text-input>
+ </form>
+ </td-step>
+
+ <td-step label="{{'Final step' | translate}}" [state]="'complete'">
+ <ng-template td-step-summary>
+ <fims-form-final-action
+ [resourceName]="'EMPLOYEE'"
+ [editMode]="editMode"
+ [disabled]="formsInvalid()"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+
+</td-steps>
diff --git a/src/app/employees/form/form.component.spec.ts b/src/app/employees/form/form.component.spec.ts
new file mode 100644
index 0000000..ef69575
--- /dev/null
+++ b/src/app/employees/form/form.component.spec.ts
@@ -0,0 +1,135 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {Component, EventEmitter, ViewChild} from '@angular/core';
+import {Employee} from '../../services/office/domain/employee.model';
+import {EmployeeFormComponent, EmployeeFormData, EmployeeSaveEvent} from './form.component';
+import {User} from '../../services/identity/domain/user.model';
+import {TranslateModule} from '@ngx-translate/core';
+import {ReactiveFormsModule} from '@angular/forms';
+import {CovalentStepsModule} from '@covalent/core';
+import {Observable} from 'rxjs/Observable';
+import {Store} from '@ngrx/store';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {FimsSharedModule} from '../../common/common.module';
+import {MatIconModule, MatInputModule, MatOptionModule, MatSelectModule} from '@angular/material';
+
+const employeeTemplate: Employee = {
+ identifier: 'test',
+ givenName: 'test',
+ middleName: 'test',
+ surname: 'test',
+ contactDetails: [{
+ type: 'EMAIL',
+ group: 'BUSINESS',
+ value: 'test@test.de',
+ preferenceLevel: 0
+ }],
+ assignedOffice: 'test'
+};
+
+const userTemplate: User = {
+ identifier: 'test',
+ role: 'test'
+};
+
+describe('Test employee form component', () => {
+
+ let fixture: ComponentFixture<TestComponent>;
+
+ let testComponent: TestComponent;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [
+ TestComponent,
+ EmployeeFormComponent
+ ],
+ imports: [
+ TranslateModule.forRoot(),
+ FimsSharedModule,
+ ReactiveFormsModule,
+ MatSelectModule,
+ MatOptionModule,
+ MatIconModule,
+ MatInputModule,
+ CovalentStepsModule,
+ NoopAnimationsModule
+ ],
+ providers: [
+ {
+ provide: Store, useClass: class {
+ dispatch = jasmine.createSpy('dispatch');
+ select = jasmine.createSpy('select').and.returnValue(Observable.empty());
+ }}
+ ]
+ });
+
+ fixture = TestBed.createComponent(TestComponent);
+ testComponent = fixture.componentInstance;
+ });
+
+ it('should test if the form save the original values', () => {
+ fixture.detectChanges();
+
+ testComponent.saveEmitter.subscribe((saveEvent: EmployeeSaveEvent) => {
+ expect(employeeTemplate.identifier).toEqual(saveEvent.detailForm.identifier);
+ expect(employeeTemplate.givenName).toEqual(saveEvent.detailForm.firstName);
+ expect(employeeTemplate.middleName).toEqual(saveEvent.detailForm.middleName);
+ expect(employeeTemplate.surname).toEqual(saveEvent.detailForm.lastName);
+ expect(saveEvent.detailForm.password).toEqual('');
+
+ expect(employeeTemplate.assignedOffice).toEqual(saveEvent.officeForm.assignedOffice);
+
+ expect(employeeTemplate.contactDetails.length).toEqual(1);
+ expect(employeeTemplate.contactDetails[0].value).toEqual(saveEvent.contactForm.email);
+
+ expect(userTemplate.role).toEqual(saveEvent.detailForm.role);
+ });
+
+ testComponent.triggerSave();
+
+ });
+});
+
+@Component({
+ template: `
+ <fims-employee-form-component #form (onSave)="onSave($event)" (onCancel)="onCancel($event)" [formData]="employeeFormData">
+ </fims-employee-form-component>`
+})
+class TestComponent {
+
+ saveEmitter = new EventEmitter<EmployeeSaveEvent>();
+
+ @ViewChild('form') formComponent: EmployeeFormComponent;
+
+ employeeFormData: EmployeeFormData = {
+ employee: employeeTemplate,
+ user: userTemplate
+ };
+
+ triggerSave(): void {
+ this.formComponent.save();
+ }
+
+ onSave(event: EmployeeSaveEvent): void {
+ this.saveEmitter.emit(event);
+ }
+
+}
diff --git a/src/app/employees/form/form.component.ts b/src/app/employees/form/form.component.ts
new file mode 100644
index 0000000..029fa9c
--- /dev/null
+++ b/src/app/employees/form/form.component.ts
@@ -0,0 +1,185 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
+import {FormBuilder, FormGroup, ValidatorFn, Validators} from '@angular/forms';
+import {Observable} from 'rxjs/Observable';
+import {TdStepComponent} from '@covalent/core';
+import {Office} from '../../services/office/domain/office.model';
+import {Employee} from '../../services/office/domain/employee.model';
+import {BUSINESS, ContactDetail, EMAIL, MOBILE, PHONE} from '../../services/domain/contact/contact-detail.model';
+import {FetchRequest} from '../../services/domain/paging/fetch-request.model';
+import {Role} from '../../services/identity/domain/role.model';
+import {User} from '../../services/identity/domain/user.model';
+import {FimsValidators} from '../../common/validator/validators';
+import {Store} from '@ngrx/store';
+import * as fromRoot from '../../store';
+import {SEARCH as SEARCH_OFFICE} from '../../store/office/office.actions';
+import {SEARCH as SEARCH_ROLE} from '../../store/role/role.actions';
+
+export interface EmployeeFormData {
+ user: User;
+ employee: Employee;
+}
+
+export interface EmployeeSaveEvent {
+ detailForm: {
+ identifier: string;
+ firstName: string;
+ middleName: string;
+ lastName: string;
+ password: string;
+ role: string;
+ };
+ contactForm: {
+ email: string;
+ phone: string;
+ mobile: string;
+ };
+ officeForm: {
+ assignedOffice: string;
+ };
+}
+
+@Component({
+ selector: 'fims-employee-form-component',
+ templateUrl: './form.component.html'
+})
+export class EmployeeFormComponent implements OnInit {
+
+ offices: Observable<Office[]>;
+
+ roles: Observable<Role[]>;
+
+ detailForm: FormGroup;
+ contactForm: FormGroup;
+ officeForm: FormGroup;
+
+ @ViewChild('detailsStep') step: TdStepComponent;
+
+ @Input('editMode') editMode: boolean;
+
+ @Input('formData') set formData(formData: EmployeeFormData) {
+ this.prepareDetailForm(formData.employee, formData.user);
+ this.prepareOfficeForm(formData.employee);
+ this.prepareContactForm(formData.employee.contactDetails);
+ }
+
+ @Output('onSave') onSave = new EventEmitter<EmployeeSaveEvent>();
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder, private store: Store<fromRoot.State>) {}
+
+ ngOnInit(): void {
+ this.offices = this.store.select(fromRoot.getOfficeSearchResults)
+ .map(officePage => officePage.offices);
+
+ this.roles = this.store.select(fromRoot.getRoleSearchResults)
+ .map(rolesPage => rolesPage.roles);
+
+ this.fetchRoles();
+
+ this.step.open();
+ }
+
+ prepareDetailForm(employee: Employee, user: User): void {
+ const passwordValidators: ValidatorFn[] = [Validators.minLength(8)];
+
+ if (!this.editMode) {
+ passwordValidators.push(Validators.required);
+ }
+
+ this.detailForm = this.formBuilder.group({
+ identifier: [employee.identifier, [Validators.required, Validators.minLength(3), Validators.maxLength(32), FimsValidators.urlSafe]],
+ firstName: [employee.givenName, [Validators.required, Validators.maxLength(256)]],
+ middleName: [employee.middleName, Validators.maxLength(256)],
+ lastName: [employee.surname, [Validators.required, Validators.maxLength(256)]],
+ password: ['', passwordValidators],
+ role: [user ? user.role : '', Validators.required]
+ });
+ }
+
+ private prepareOfficeForm(employee: Employee) {
+ this.officeForm = this.formBuilder.group({
+ assignedOffice: [employee.assignedOffice]
+ });
+ }
+
+ private prepareContactForm(contactDetails: ContactDetail[]): void {
+ let phone = '';
+ let mobile = '';
+ let email = '';
+
+ const businessContacts: ContactDetail[] = contactDetails.filter(contactDetail => contactDetail.group === BUSINESS);
+
+ if (businessContacts.length) {
+ phone = this.getFirstItemByType(businessContacts, PHONE);
+ mobile = this.getFirstItemByType(businessContacts, MOBILE);
+ email = this.getFirstItemByType(businessContacts, EMAIL);
+ }
+
+ this.contactForm = this.formBuilder.group({
+ email: [email, [Validators.maxLength(256), FimsValidators.email]],
+ phone: [phone, Validators.maxLength(256)],
+ mobile: [mobile, Validators.maxLength(256)]
+ });
+ }
+
+ getFirstItemByType(contactDetails: ContactDetail[], type: string): string {
+ const items = contactDetails.filter(contact => contact.type === type);
+ return items.length ? items[0].value : '';
+ }
+
+ formsInvalid(): boolean {
+ return (!this.officeForm.pristine && this.officeForm.invalid) ||
+ (!this.contactForm.pristine && this.contactForm.invalid)
+ || this.detailForm.invalid;
+ }
+
+ save(): void {
+ this.onSave.emit({
+ detailForm: this.detailForm.value,
+ contactForm: this.contactForm.value,
+ officeForm: this.officeForm.value
+ });
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+ searchOffice(searchTerm: string): void {
+ const fetchRequest: FetchRequest = {
+ searchTerm
+ };
+ this.store.dispatch({ type: SEARCH_OFFICE, payload: fetchRequest });
+ }
+
+ assignOffice(selections: string[]): void {
+ this.setFormValue(this.officeForm, {'assignedOffice': selections && selections.length > 0 ? selections[0] : undefined});
+ }
+
+ fetchRoles(): void {
+ this.store.dispatch({ type: SEARCH_ROLE });
+ }
+
+ private setFormValue(form: FormGroup, value: any) {
+ form.setValue(value);
+ form.markAsDirty();
+ }
+}
diff --git a/src/app/employees/form/form.mapper.ts b/src/app/employees/form/form.mapper.ts
new file mode 100644
index 0000000..eb6339f
--- /dev/null
+++ b/src/app/employees/form/form.mapper.ts
@@ -0,0 +1,77 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {EmployeeSaveEvent} from './form.component';
+import {ContactDetail, ContactDetailType} from '../../services/domain/contact/contact-detail.model';
+import {Employee} from '../../services/office/domain/employee.model';
+import {UserWithPassword} from '../../services/identity/domain/user-with-password.model';
+
+function buildContactDetail(type: ContactDetailType, value: string): ContactDetail {
+ return {
+ group: 'BUSINESS',
+ type: type,
+ value: value,
+ preferenceLevel: 1
+ };
+}
+
+export function mapContactDetails(contactForm: any): ContactDetail[] {
+ const contactDetails: ContactDetail[] = [];
+
+ if (contactForm.phone) {
+ contactDetails.push(buildContactDetail('PHONE', contactForm.phone));
+ }
+
+ if (contactForm.mobile) {
+ contactDetails.push(buildContactDetail('MOBILE', contactForm.mobile));
+ }
+
+ if (contactForm.email) {
+ contactDetails.push(buildContactDetail('EMAIL', contactForm.email));
+ }
+
+ return contactDetails;
+}
+
+export function mapEmployee(event: EmployeeSaveEvent): Employee {
+ const assignedOffice = event.officeForm.assignedOffice;
+
+ const contactDetails: ContactDetail[] = mapContactDetails(event.contactForm);
+
+ const employee: Employee = {
+ identifier: event.detailForm.identifier,
+ givenName: event.detailForm.firstName,
+ middleName: event.detailForm.middleName,
+ surname: event.detailForm.lastName,
+ contactDetails: contactDetails,
+ assignedOffice: assignedOffice ? assignedOffice : undefined
+ };
+
+ return employee;
+}
+
+export function mapUser(event: EmployeeSaveEvent): UserWithPassword {
+ const userWithPassword: UserWithPassword = {
+ identifier: event.detailForm.identifier,
+ password: event.detailForm.password,
+ role: event.detailForm.role
+ };
+
+ return userWithPassword;
+}
diff --git a/src/app/employees/store/effects/notification.effects.ts b/src/app/employees/store/effects/notification.effects.ts
new file mode 100644
index 0000000..3de745e
--- /dev/null
+++ b/src/app/employees/store/effects/notification.effects.ts
@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as employeeActions from '../employee.actions';
+import {NotificationService, NotificationType} from '../../../services/notification/notification.service';
+
+@Injectable()
+export class EmployeeNotificationEffects {
+
+ @Effect({ dispatch: false })
+ createEmployeeSuccess$: Observable<Action> = this.actions$
+ .ofType(employeeActions.CREATE_SUCCESS, employeeActions.UPDATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Employee is going to be saved'
+ }));
+
+ @Effect({ dispatch: false })
+ deleteEmployeeSuccess$: Observable<Action> = this.actions$
+ .ofType(employeeActions.DELETE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Employee is going to be deleted'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+}
+
diff --git a/src/app/employees/store/effects/route.effects.ts b/src/app/employees/store/effects/route.effects.ts
new file mode 100644
index 0000000..3e35dd1
--- /dev/null
+++ b/src/app/employees/store/effects/route.effects.ts
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as employeeActions from '../employee.actions';
+import {Router} from '@angular/router';
+
+@Injectable()
+export class EmployeeRouteEffects {
+
+ @Effect({ dispatch: false })
+ createEmployeeSuccess$: Observable<Action> = this.actions$
+ .ofType(employeeActions.CREATE_SUCCESS, employeeActions.UPDATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute} ));
+
+ @Effect({ dispatch: false })
+ deleteEmployeeSuccess$: Observable<Action> = this.actions$
+ .ofType(employeeActions.DELETE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../../'], { relativeTo: payload.activatedRoute }));
+
+ constructor(private actions$: Actions, private router: Router) { }
+}
diff --git a/src/app/employees/store/effects/service.effects.spec.ts b/src/app/employees/store/effects/service.effects.spec.ts
new file mode 100644
index 0000000..5afcd87
--- /dev/null
+++ b/src/app/employees/store/effects/service.effects.spec.ts
@@ -0,0 +1,201 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {fakeAsync, TestBed, tick} from '@angular/core/testing';
+import {EffectsRunner, EffectsTestingModule} from '@ngrx/effects/testing';
+import {EmployeeApiEffects} from './service.effects';
+import {OfficeService} from '../../../services/office/office.service';
+import {IdentityService} from '../../../services/identity/identity.service';
+import {Observable} from 'rxjs/Observable';
+import {UpdateEmployeeAction, UpdateEmployeeSuccessAction} from '../employee.actions';
+import {Employee} from '../../../services/office/domain/employee.model';
+
+describe('Account Search Api Effects', () => {
+ beforeEach(() => {
+
+ TestBed.configureTestingModule({
+ imports: [
+ EffectsTestingModule
+ ],
+ providers: [
+ EmployeeApiEffects,
+ {
+ provide: OfficeService,
+ useValue: jasmine.createSpyObj('officeService', ['createEmployee', 'updateEmployee', 'setContactDetails'])
+ },
+ {
+ provide: IdentityService,
+ useValue: jasmine.createSpyObj('identityService', ['createUser', 'changeUserRole', 'changePassword'])
+ }
+ ]
+ });
+
+ });
+
+ describe('updateEmployee$', () => {
+
+ function setup() {
+ const officeService = TestBed.get(OfficeService);
+
+ officeService.updateEmployee.and.returnValue(Observable.of({}));
+
+ return {
+ runner: TestBed.get(EffectsRunner),
+ officeService: officeService,
+ identityService: TestBed.get(IdentityService),
+ employeeEffects: TestBed.get(EmployeeApiEffects)
+ };
+ }
+
+ it('should update employee', fakeAsync(() => {
+ const { runner, officeService, employeeEffects } = setup();
+
+ const employee: Employee = {
+ identifier: '',
+ givenName: '',
+ surname: '',
+ contactDetails: []
+ };
+
+ const expectedResult = new UpdateEmployeeSuccessAction({
+ resource: employee,
+ activatedRoute: null
+ });
+
+ runner.queue(new UpdateEmployeeAction({
+ employee: employee,
+ activatedRoute: null
+ }));
+
+ let result = null;
+ employeeEffects.updateEmployee$.subscribe(_result => result = _result);
+
+ tick();
+
+ expect(result).toEqual(expectedResult);
+
+ expect(officeService.updateEmployee).toHaveBeenCalled();
+ }));
+
+ it('should update contact details', fakeAsync(() => {
+ const { runner, officeService, employeeEffects } = setup();
+
+ officeService.setContactDetails.and.returnValue(Observable.of({}));
+
+ const employee: Employee = {
+ identifier: '',
+ givenName: '',
+ surname: '',
+ contactDetails: []
+ };
+
+ const expectedResult = new UpdateEmployeeSuccessAction({
+ resource: employee,
+ activatedRoute: null
+ });
+
+ runner.queue(new UpdateEmployeeAction({
+ employee: employee,
+ contactDetails: [
+ {
+ type: 'EMAIL',
+ group: 'BUSINESS',
+ value: 'dont@call.me',
+ preferenceLevel: 0
+ }
+ ],
+ activatedRoute: null
+ }));
+
+ let result = null;
+ employeeEffects.updateEmployee$.subscribe(_result => result = _result);
+
+ tick();
+
+ expect(result).toEqual(expectedResult);
+
+ expect(officeService.setContactDetails).toHaveBeenCalled();
+ }));
+
+ it('should update password', fakeAsync(() => {
+ const { runner, identityService, employeeEffects } = setup();
+
+ identityService.changePassword.and.returnValue(Observable.of({}));
+
+ const employee: Employee = {
+ identifier: '',
+ givenName: '',
+ surname: '',
+ contactDetails: []
+ };
+
+ const expectedResult = new UpdateEmployeeSuccessAction({
+ resource: employee,
+ activatedRoute: null
+ });
+
+ runner.queue(new UpdateEmployeeAction({
+ employee: employee,
+ password: 'test',
+ activatedRoute: null
+ }));
+
+ let result = null;
+ employeeEffects.updateEmployee$.subscribe(_result => result = _result);
+
+ tick();
+
+ expect(result).toEqual(expectedResult);
+
+ expect(identityService.changePassword).toHaveBeenCalled();
+ }));
+
+ it('should update role', fakeAsync(() => {
+ const { runner, identityService, employeeEffects } = setup();
+
+ identityService.changeUserRole.and.returnValue(Observable.of({}));
+
+ const employee: Employee = {
+ identifier: '',
+ givenName: '',
+ surname: '',
+ contactDetails: []
+ };
+
+ const expectedResult = new UpdateEmployeeSuccessAction({
+ resource: employee,
+ activatedRoute: null
+ });
+
+ runner.queue(new UpdateEmployeeAction({
+ employee: employee,
+ role: 'test',
+ activatedRoute: null
+ }));
+
+ let result = null;
+ employeeEffects.updateEmployee$.subscribe(_result => result = _result);
+
+ tick();
+
+ expect(result).toEqual(expectedResult);
+
+ expect(identityService.changeUserRole).toHaveBeenCalled();
+ }));
+ });
+});
diff --git a/src/app/employees/store/effects/service.effects.ts b/src/app/employees/store/effects/service.effects.ts
new file mode 100644
index 0000000..0a17834
--- /dev/null
+++ b/src/app/employees/store/effects/service.effects.ts
@@ -0,0 +1,95 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {OfficeService} from '../../../services/office/office.service';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+import * as employeeActions from '../employee.actions';
+import {IdentityService} from '../../../services/identity/identity.service';
+import {RoleIdentifier} from '../../../services/identity/domain/role-identifier.model';
+import {Password} from '../../../services/identity/domain/password.model';
+
+@Injectable()
+export class EmployeeApiEffects {
+
+ @Effect()
+ createEmployee$: Observable<Action> = this.actions$
+ .ofType(employeeActions.CREATE)
+ .map((action: employeeActions.CreateEmployeeAction) => action.payload)
+ .mergeMap(payload =>
+ this.identityService.createUser(payload.user)
+ .mergeMap(() => this.officeService.createEmployee(payload.employee) )
+ .map(() => new employeeActions.CreateEmployeeSuccessAction({
+ resource: payload.employee,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new employeeActions.CreateEmployeeFailAction(error)))
+ );
+
+ @Effect()
+ updateEmployee$: Observable<Action> = this.actions$
+ .ofType(employeeActions.UPDATE)
+ .map((action: employeeActions.UpdateEmployeeAction) => action.payload)
+ .mergeMap(payload => {
+ const employee = payload.employee;
+ const httpCalls: Observable<any>[] = [];
+
+ httpCalls.push(this.officeService.updateEmployee(employee));
+
+ if (payload.contactDetails) {
+ httpCalls.push(this.officeService.setContactDetails(employee.identifier, payload.contactDetails));
+ }
+
+ if (payload.role) {
+ httpCalls.push(this.identityService.changeUserRole(employee.identifier, new RoleIdentifier(payload.role)));
+ }
+
+ if (payload.password) {
+ httpCalls.push(this.identityService.changePassword(employee.identifier, new Password(payload.password)));
+ }
+
+ return Observable.forkJoin(httpCalls)
+ .map(() => new employeeActions.UpdateEmployeeSuccessAction({
+ resource: payload.employee,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new employeeActions.UpdateEmployeeFailAction(error)));
+ });
+
+ @Effect()
+ deleteEmployee$: Observable<Action> = this.actions$
+ .ofType(employeeActions.DELETE)
+ .map((action: employeeActions.DeleteEmployeeAction) => action.payload)
+ .mergeMap(payload => {
+ return Observable.forkJoin(
+ this.officeService.deleteEmployee(payload.employee.identifier),
+ this.identityService.changeUserRole(payload.employee.identifier, new RoleIdentifier('deactivated'))
+ )
+ .map(() => new employeeActions.DeleteEmployeeSuccessAction({
+ resource: payload.employee,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new employeeActions.DeleteEmployeeFailAction(error)));
+ }
+ );
+
+ constructor(private actions$: Actions, private officeService: OfficeService, private identityService: IdentityService) { }
+}
diff --git a/src/app/employees/store/employee.actions.ts b/src/app/employees/store/employee.actions.ts
new file mode 100644
index 0000000..9083240
--- /dev/null
+++ b/src/app/employees/store/employee.actions.ts
@@ -0,0 +1,149 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Action} from '@ngrx/store';
+import {type} from '../../store/util';
+import {Employee} from '../../services/office/domain/employee.model';
+import {Error} from '../../services/domain/error.model';
+import {ContactDetail} from '../../services/domain/contact/contact-detail.model';
+import {UserWithPassword} from '../../services/identity/domain/user-with-password.model';
+import {RoutePayload} from '../../common/store/route-payload';
+import {
+ CreateResourceSuccessPayload,
+ DeleteResourceSuccessPayload,
+ LoadResourcePayload,
+ SelectResourcePayload,
+ UpdateResourceSuccessPayload
+} from '../../common/store/resource.reducer';
+
+export const LOAD = type('[Employee] Load');
+export const SELECT = type('[Employee] Select');
+
+export const CREATE = type('[Employee] Create');
+export const CREATE_SUCCESS = type('[Employee] Create Success');
+export const CREATE_FAIL = type('[Employee] Create Fail');
+
+export const UPDATE = type('[Employee] Update');
+export const UPDATE_SUCCESS = type('[Employee] Update Success');
+export const UPDATE_FAIL = type('[Employee] Update Fail');
+
+export const DELETE = type('[Employee] Delete');
+export const DELETE_SUCCESS = type('[Employee] Delete Success');
+export const DELETE_FAIL = type('[Employee] Delete Fail');
+
+export const RESET_FORM = type('[Employee] Reset Form');
+
+export interface EmployeeRoutePayload extends RoutePayload {
+ employee: Employee;
+}
+
+export interface CreateEmployeePayload extends EmployeeRoutePayload {
+ user: UserWithPassword;
+}
+
+export interface UpdateEmployeePayload extends EmployeeRoutePayload {
+ contactDetails?: ContactDetail[];
+ password?: string;
+ role?: string;
+}
+
+export class LoadAction implements Action {
+ readonly type = LOAD;
+
+ constructor(public payload: LoadResourcePayload) { }
+}
+
+export class SelectAction implements Action {
+ readonly type = SELECT;
+
+ constructor(public payload: SelectResourcePayload) { }
+}
+
+export class CreateEmployeeAction implements Action {
+ readonly type = CREATE;
+
+ constructor(public payload: CreateEmployeePayload) { }
+}
+
+export class CreateEmployeeSuccessAction implements Action {
+ readonly type = CREATE_SUCCESS;
+
+ constructor(public payload: CreateResourceSuccessPayload) { }
+}
+
+export class CreateEmployeeFailAction implements Action {
+ readonly type = CREATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class UpdateEmployeeAction implements Action {
+ readonly type = UPDATE;
+
+ constructor(public payload: UpdateEmployeePayload) { }
+}
+
+export class UpdateEmployeeSuccessAction implements Action {
+ readonly type = UPDATE_SUCCESS;
+
+ constructor(public payload: UpdateResourceSuccessPayload) { }
+}
+
+export class UpdateEmployeeFailAction implements Action {
+ readonly type = UPDATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class DeleteEmployeeAction implements Action {
+ readonly type = DELETE;
+
+ constructor(public payload: EmployeeRoutePayload) { }
+}
+
+export class DeleteEmployeeSuccessAction implements Action {
+ readonly type = DELETE_SUCCESS;
+
+ constructor(public payload: DeleteResourceSuccessPayload) { }
+}
+
+export class DeleteEmployeeFailAction implements Action {
+ readonly type = DELETE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class ResetEmployeeFormAction implements Action {
+ readonly type = RESET_FORM;
+
+ constructor() { }
+}
+
+export type Actions
+ = LoadAction
+ | SelectAction
+ | CreateEmployeeAction
+ | CreateEmployeeSuccessAction
+ | CreateEmployeeFailAction
+ | UpdateEmployeeAction
+ | UpdateEmployeeSuccessAction
+ | UpdateEmployeeFailAction
+ | DeleteEmployeeAction
+ | DeleteEmployeeSuccessAction
+ | DeleteEmployeeFailAction
+ | ResetEmployeeFormAction;
diff --git a/src/app/employees/store/index.ts b/src/app/employees/store/index.ts
new file mode 100644
index 0000000..6b4ea4e
--- /dev/null
+++ b/src/app/employees/store/index.ts
@@ -0,0 +1,51 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as fromRoot from '../../store';
+import {ActionReducer, Store} from '@ngrx/store';
+import {createReducer} from '../../store/index';
+import {createSelector} from 'reselect';
+import {createResourceReducer, getResourceLoadedAt, getResourceSelected, ResourceState} from '../../common/store/resource.reducer';
+import {createFormReducer, FormState, getFormError} from '../../common/store/form.reducer';
+
+export interface State extends fromRoot.State {
+ employees: ResourceState;
+ employeeForm: FormState;
+}
+
+const reducers = {
+ employees: createResourceReducer('Employee'),
+ employeeForm: createFormReducer('Employee')
+};
+
+export const employeeModuleReducer: ActionReducer<State> = createReducer(reducers);
+
+export const getEmployeesState = (state: State) => state.employees;
+
+export const getEmployeeFormState = (state: State) => state.employeeForm;
+export const getEmployeeFormError = createSelector(getEmployeeFormState, getFormError);
+
+export const getEmployeesLoadedAt = createSelector(getEmployeesState, getResourceLoadedAt);
+export const getSelectedEmployee = createSelector(getEmployeesState, getResourceSelected);
+
+export class EmployeesStore extends Store<State> {}
+
+export function employeeStoreFactory(appStore: Store<fromRoot.State>) {
+ appStore.replaceReducer(employeeModuleReducer);
+ return appStore;
+}
diff --git a/src/app/employees/user.resolver.ts b/src/app/employees/user.resolver.ts
new file mode 100644
index 0000000..34f29cd
--- /dev/null
+++ b/src/app/employees/user.resolver.ts
@@ -0,0 +1,33 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {ActivatedRouteSnapshot, Resolve} from '@angular/router';
+import {Observable} from 'rxjs/Observable';
+import {IdentityService} from '../services/identity/identity.service';
+import {User} from '../services/identity/domain/user.model';
+
+@Injectable()
+export class UserResolver implements Resolve<User> {
+
+ constructor(private identityService: IdentityService) {}
+
+ resolve(route: ActivatedRouteSnapshot): Observable<User> {
+ return this.identityService.getUser(route.params['id']);
+ }
+}
diff --git a/src/app/environment.ts b/src/app/environment.ts
new file mode 100644
index 0000000..9a16843
--- /dev/null
+++ b/src/app/environment.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// The file for the current environment will overwrite this one during build
+// Different environments can be found in config/environment.{dev|prod}.ts
+// The build system defaults to the dev environment
+
+export const environment: any = {
+ production: false,
+};
diff --git a/src/app/index.ts b/src/app/index.ts
new file mode 100644
index 0000000..2df8576
--- /dev/null
+++ b/src/app/index.ts
@@ -0,0 +1,20 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export {environment} from './environment';
+export {AppModule} from './app.module';
diff --git a/src/app/loans/products/charges/charge-exists.guard.ts b/src/app/loans/products/charges/charge-exists.guard.ts
new file mode 100644
index 0000000..68ae830
--- /dev/null
+++ b/src/app/loans/products/charges/charge-exists.guard.ts
@@ -0,0 +1,69 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import {Injectable} from '@angular/core';
+import * as fromProducts from '../store';
+import {Observable} from 'rxjs/Observable';
+import {LoadAction} from '../store/charges/charge.actions';
+import {of} from 'rxjs/observable/of';
+import {PortfolioStore} from '../store/index';
+import {PortfolioService} from '../../../services/portfolio/portfolio.service';
+import {ExistsGuardService} from '../../../common/guards/exists-guard';
+
+@Injectable()
+export class ProductChargeExistsGuard implements CanActivate {
+
+ constructor(private store: PortfolioStore,
+ private portfolioService: PortfolioService,
+ private existsGuardService: ExistsGuardService) {}
+
+ hasChargeInStore(id: string): Observable<boolean> {
+ const timestamp$ = this.store.select(fromProducts.getProductChargesLoadedAt)
+ .map(loadedAt => loadedAt[id]);
+
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ hasChargeInApi(productId: string, chargeId: string): Observable<boolean> {
+ const getChargeDefinition = this.portfolioService.getChargeDefinition(productId, chargeId)
+ .map(chargeEntity => new LoadAction({
+ resource: chargeEntity
+ }))
+ .do((action: LoadAction) => this.store.dispatch(action))
+ .map(charge => !!charge);
+
+ return this.existsGuardService.routeTo404OnError(getChargeDefinition);
+ }
+
+ hasCharge(productId: string, chargeId: string): Observable<boolean> {
+ return this.hasChargeInStore(chargeId)
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+
+ return this.hasChargeInApi(productId, chargeId);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasCharge(route.parent.params['productId'], route.params['chargeId']);
+ }
+}
diff --git a/src/app/loans/products/charges/charge.detail.component.html b/src/app/loans/products/charges/charge.detail.component.html
new file mode 100644
index 0000000..158185e
--- /dev/null
+++ b/src/app/loans/products/charges/charge.detail.component.html
@@ -0,0 +1,49 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over [title]="charge.name" [subTitle]="charge.description" [navigateBackTo]="['../../']">
+ <fims-layout-card-over-header-menu>
+ <button mat-icon-button (click)="deleteCharge()" title="{{'Delete this fee' | translate}}" *hasPermission="{ id: 'portfolio_products', accessLevel: 'CHANGE'}"><mat-icon>delete</mat-icon></button>
+ </fims-layout-card-over-header-menu>
+ <div class="mat-content inset" flex>
+ <div layout="row">
+ <mat-list>
+ <mat-list-item>
+ <h3 matLine translate>Action</h3>
+ <p matLine>{{charge.chargeAction}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Amount</h3>
+ <p matLine>{{charge.amount | number}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Charge method</h3>
+ <p matLine>{{charge.chargeMethod}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Proportional to</h3>
+ <p matLine>{{charge.proportionalTo}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Cycle</h3>
+ <p matLine>{{charge.forCycleSizeUnit}}</p>
+ </mat-list-item>
+ </mat-list>
+ </div>
+ </div>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Edit fee' | translate}}" icon="mode_edit" [link]="['edit']" [permission]="{ id: 'portfolio_products', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/loans/products/charges/charge.detail.component.ts b/src/app/loans/products/charges/charge.detail.component.ts
new file mode 100644
index 0000000..aa72d0d
--- /dev/null
+++ b/src/app/loans/products/charges/charge.detail.component.ts
@@ -0,0 +1,87 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {ChargeDefinition} from '../../../services/portfolio/domain/charge-definition.model';
+import {Observable} from 'rxjs/Observable';
+import {Subscription} from 'rxjs/Subscription';
+import {TdDialogService} from '@covalent/core';
+import {DELETE, SelectAction} from '../store/charges/charge.actions';
+import {PortfolioStore} from '../store/index';
+import * as fromPortfolio from '../store';
+import {FimsProduct} from '../store/model/fims-product.model';
+
+@Component({
+ templateUrl: './charge.detail.component.html'
+})
+export class ProductChargeDetailComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ private productSubscription: Subscription;
+
+ private chargeSubscription: Subscription;
+
+ private product: FimsProduct;
+
+ charge: ChargeDefinition;
+
+ constructor(private route: ActivatedRoute, private dialogService: TdDialogService, private portfolioStore: PortfolioStore) {}
+
+ ngOnInit(): void {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectAction(params['chargeId']))
+ .subscribe(this.portfolioStore);
+
+ this.productSubscription = this.portfolioStore.select(fromPortfolio.getSelectedProduct)
+ .filter(product => !!product)
+ .subscribe(product => this.product = product);
+
+ this.chargeSubscription = this.portfolioStore.select(fromPortfolio.getSelectedProductCharge)
+ .filter(charge => !!charge)
+ .subscribe(charge => this.charge = charge);
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ this.productSubscription.unsubscribe();
+ this.chargeSubscription.unsubscribe();
+ }
+
+ confirmDeletion(): Observable<boolean> {
+ return this.dialogService.openConfirm({
+ message: 'Do you want to delete fee "' + this.charge.identifier + '"?',
+ title: 'Confirm deletion',
+ acceptButton: 'DELETE FEE',
+ }).afterClosed();
+ }
+
+ deleteCharge(): void {
+ this.confirmDeletion()
+ .filter(accept => accept)
+ .subscribe(() => {
+ this.portfolioStore.dispatch({ type: DELETE, payload: {
+ productId: this.product.identifier,
+ charge: this.charge,
+ activatedRoute: this.route
+ }});
+ });
+ }
+
+}
diff --git a/src/app/loans/products/charges/charge.list.component.html b/src/app/loans/products/charges/charge.list.component.html
new file mode 100644
index 0000000..610037c
--- /dev/null
+++ b/src/app/loans/products/charges/charge.list.component.html
@@ -0,0 +1,31 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Manage loan product fees' | translate}}" [navigateBackTo]="['../']">
+ <fims-two-column-layout>
+ <mat-nav-list left>
+ <h3 mat-subheader translate>Management</h3>
+ <a mat-list-item [routerLink]="['./ranges']">
+ <mat-icon matListAvatar>assignment</mat-icon>
+ <h3 matLine translate>Ranges</h3>
+ <p matLine translate>Manage ranges</p>
+ </a>
+ </mat-nav-list>
+ <fims-data-table right flex (onFetch)="fetchCharges($event)" (onActionCellClick)="rowSelect($event)" [columns]="columns" [data]="chargesData$ | async" [sortable]="false"></fims-data-table>
+ </fims-two-column-layout>
+
+</fims-layout-card-over>
diff --git a/src/app/loans/products/charges/charge.list.component.ts b/src/app/loans/products/charges/charge.list.component.ts
new file mode 100644
index 0000000..bdcce6d
--- /dev/null
+++ b/src/app/loans/products/charges/charge.list.component.ts
@@ -0,0 +1,88 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {TableData, TableFetchRequest} from '../../../common/data-table/data-table.component';
+import {ChargeDefinition} from '../../../services/portfolio/domain/charge-definition.model';
+import {ITdDataTableColumn} from '@covalent/core';
+import {ActionOption, ActionOptions} from '../../../common/domain/action-option.model';
+import {PortfolioStore} from '../store/index';
+import * as fromPortfolio from '../store';
+import {Observable} from 'rxjs/Observable';
+import {Subscription} from 'rxjs/Subscription';
+import {LOAD_ALL} from '../store/charges/charge.actions';
+import {FimsProduct} from '../store/model/fims-product.model';
+
+@Component({
+ templateUrl: './charge.list.component.html'
+})
+export class ProductChargeListComponent implements OnInit, OnDestroy {
+
+ private productSubscription: Subscription;
+
+ private product: FimsProduct;
+
+ chargesData$: Observable<TableData>;
+
+ columns: ITdDataTableColumn[] = [
+ { name: 'identifier', label: 'Id' },
+ { name: 'name', label: 'Name' },
+ { name: 'amount', label: 'Amount', numeric: true, format: value => value.toFixed(2) },
+ { name: 'chargeAction', label: 'Applied when', format: value => {
+ const result: ActionOption = ActionOptions.find((option) => {
+ return option.type === value;
+ });
+ return result.label;
+ } }
+ ];
+
+ constructor(private router: Router, private route: ActivatedRoute, private portfolioStore: PortfolioStore) {}
+
+ ngOnInit(): void {
+ this.productSubscription = this.portfolioStore.select(fromPortfolio.getSelectedProduct)
+ .filter(product => !!product)
+ .subscribe(product => {
+ this.product = product;
+ this.fetchCharges();
+ });
+
+ this.chargesData$ = this.portfolioStore.select(fromPortfolio.getAllProductChargeEntities)
+ .map(charges => {
+ const data = charges.filter(charge => !charge.readOnly);
+
+ return {
+ totalElements: data.length,
+ totalPages: 1,
+ data
+ };
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.productSubscription.unsubscribe();
+ }
+
+ fetchCharges(event?: TableFetchRequest): void {
+ this.portfolioStore.dispatch({ type: LOAD_ALL, payload: this.product.identifier });
+ }
+
+ rowSelect(chargeDefinition: ChargeDefinition): void {
+ this.router.navigate(['detail', chargeDefinition.identifier], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/loans/products/charges/form/create.component.html b/src/app/loans/products/charges/form/create.component.html
new file mode 100644
index 0000000..bdf8ab2
--- /dev/null
+++ b/src/app/loans/products/charges/form/create.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Add new fee' | translate}}">
+ <fims-product-charge-form-component
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [charge]="charge"
+ [ranges]="ranges$ | async">
+ </fims-product-charge-form-component>
+</fims-layout-card-over>
diff --git a/src/app/loans/products/charges/form/create.component.ts b/src/app/loans/products/charges/form/create.component.ts
new file mode 100644
index 0000000..19db22a
--- /dev/null
+++ b/src/app/loans/products/charges/form/create.component.ts
@@ -0,0 +1,84 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {ChargeDefinition} from '../../../../services/portfolio/domain/charge-definition.model';
+import {ActivatedRoute, Router} from '@angular/router';
+import * as fromPortfolio from '../../store/index';
+import {PortfolioStore} from '../../store/index';
+import {Subscription} from 'rxjs/Subscription';
+import {CREATE} from '../../store/charges/charge.actions';
+import {FimsProduct} from '../../store/model/fims-product.model';
+import {Observable} from 'rxjs/Observable';
+import {RangeActions} from '../../store/ranges/range.actions';
+import {FimsRange} from '../../../../services/portfolio/domain/range-model';
+
+@Component({
+ templateUrl: './create.component.html'
+})
+export class ProductChargeCreateFormComponent implements OnInit, OnDestroy {
+
+ private productSubscription: Subscription;
+
+ private product: FimsProduct;
+
+ ranges$: Observable<FimsRange[]>;
+
+ charge: ChargeDefinition = {
+ identifier: '',
+ name: '',
+ description: '',
+ chargeAction: 'OPEN',
+ amount: 0,
+ chargeMethod: 'FIXED',
+ fromAccountDesignator: '',
+ toAccountDesignator: '',
+ forCycleSizeUnit: 'WEEKS',
+ proportionalTo: ''
+ };
+
+ constructor(private router: Router, private route: ActivatedRoute, private portfolioStore: PortfolioStore) {}
+
+ ngOnInit(): void {
+ this.productSubscription = this.portfolioStore.select(fromPortfolio.getSelectedProduct)
+ .do(product => this.portfolioStore.dispatch(RangeActions.loadAllAction(product.identifier)))
+ .subscribe(product => this.product = product);
+
+ this.ranges$ = this.portfolioStore.select(fromPortfolio.getAllProductChargeRangeEntities);
+ }
+
+ ngOnDestroy(): void {
+ this.productSubscription.unsubscribe();
+ }
+
+ onSave(charge: ChargeDefinition): void {
+ this.portfolioStore.dispatch({ type: CREATE, payload: {
+ productId: this.product.identifier,
+ charge: charge,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel(): void {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/loans/products/charges/form/edit.component.html b/src/app/loans/products/charges/form/edit.component.html
new file mode 100644
index 0000000..26efa77
--- /dev/null
+++ b/src/app/loans/products/charges/form/edit.component.html
@@ -0,0 +1,26 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Edit fee' | translate}}">
+ <fims-product-charge-form-component
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [charge]="charge$ | async"
+ [ranges]="ranges$ | async"
+ [editMode]="true">
+ </fims-product-charge-form-component>
+</fims-layout-card-over>
diff --git a/src/app/loans/products/charges/form/edit.component.ts b/src/app/loans/products/charges/form/edit.component.ts
new file mode 100644
index 0000000..32625f6
--- /dev/null
+++ b/src/app/loans/products/charges/form/edit.component.ts
@@ -0,0 +1,77 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {ChargeDefinition} from '../../../../services/portfolio/domain/charge-definition.model';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Subscription} from 'rxjs/Subscription';
+import {UPDATE} from '../../store/charges/charge.actions';
+import * as fromPortfolio from '../../store';
+import {PortfolioStore} from '../../store/index';
+import {FimsProduct} from '../../store/model/fims-product.model';
+import {Observable} from 'rxjs/Observable';
+import {RangeActions} from '../../store/ranges/range.actions';
+import {FimsRange} from '../../../../services/portfolio/domain/range-model';
+
+@Component({
+ templateUrl: './edit.component.html'
+})
+export class ProductChargeEditFormComponent implements OnInit, OnDestroy {
+
+ private productSubscription: Subscription;
+
+ private product: FimsProduct;
+
+ charge$: Observable<ChargeDefinition>;
+
+ ranges$: Observable<FimsRange[]>;
+
+ constructor(private router: Router, private route: ActivatedRoute, private portfolioStore: PortfolioStore) {}
+
+ ngOnInit(): void {
+ this.productSubscription = this.portfolioStore.select(fromPortfolio.getSelectedProduct)
+ .filter(product => !!product)
+ .do(product => this.portfolioStore.dispatch(RangeActions.loadAllAction(product.identifier)))
+ .subscribe(product => this.product = product);
+
+ this.charge$ = this.portfolioStore.select(fromPortfolio.getSelectedProductCharge)
+ .filter(charge => !!charge);
+
+ this.ranges$ = this.portfolioStore.select(fromPortfolio.getAllProductChargeRangeEntities);
+ }
+
+ ngOnDestroy(): void {
+ this.productSubscription.unsubscribe();
+ }
+
+ onSave(charge: ChargeDefinition): void {
+ this.portfolioStore.dispatch({ type: UPDATE, payload: {
+ productId: this.product.identifier,
+ charge: charge,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel(): void {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/loans/products/charges/form/form.component.html b/src/app/loans/products/charges/form/form.component.html
new file mode 100644
index 0000000..83f43e1
--- /dev/null
+++ b/src/app/loans/products/charges/form/form.component.html
@@ -0,0 +1,74 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Fee details' | translate}}" [state]="detailForm.valid ? 'complete' : detailForm.pristine ? 'none' : 'required'" [active]="true">
+ <form [formGroup]="detailForm" layout="column">
+ <fims-id-input [form]="detailForm" controlName="identifier" [readonly]="editMode"></fims-id-input>
+ <fims-text-input [form]="detailForm" controlName="name" placeholder="{{'Name' | translate}}"></fims-text-input>
+ <mat-form-field layout-margin flex>
+ <textarea matInput placeholder="{{'Description' | translate}}" formControlName="description"></textarea>
+ <mat-error *ngIf="detailForm.get('description').hasError('required')" translate>
+ Required
+ </mat-error>
+ </mat-form-field>
+ <div layout="row">
+ <fims-text-input type="number" [form]="detailForm" controlName="amount" placeholder="{{'Amount' | translate}}"></fims-text-input>
+ <mat-radio-group formControlName="chargeMethod">
+ <mat-radio-button *ngFor="let basis of chargeMethodOptions" [value]="basis.type" layout-margin>
+ {{basis.label}}
+ </mat-radio-button>
+ </mat-radio-group>
+ <mat-form-field layout-margin>
+ <mat-select formControlName="proportionalTo" placeholder="{{ 'Proportional to' | translate }}">
+ <mat-option *ngFor="let proportionalTo of proportionalToOptions" [value]="proportionalTo.designator">
+ {{proportionalTo.label}}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ </div>
+ <mat-slide-toggle formControlName="rangeEnabled" layout-margin translate layout-margin>
+ Apply amount only within range?
+ </mat-slide-toggle>
+ <ng-container *ngIf="detailForm.get('rangeEnabled').value === true">
+ <mat-form-field layout-margin>
+ <mat-select formControlName="rangeIdentifier" placeholder="{{ 'Select range' | translate }}">
+ <mat-option *ngFor="let range of ranges" [value]="range.identifier">
+ {{range.identifier}}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ <mat-form-field layout-margin>
+ <mat-select formControlName="rangeSegmentIdentifier" placeholder="{{ 'Select range segment' | translate }}">
+ <mat-option *ngFor="let segment of selectedRange?.segments" [value]="segment.identifier">
+ {{segment.identifier}}({{segment.start | number:numberFormat}} - {{segment.end | number:numberFormat}})
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ </ng-container>
+ </form>
+ <ng-template td-step-actions>
+ <fims-form-final-action
+ [resourceName]="'FEE'"
+ [editMode]="editMode"
+ [disabled]="!detailForm.valid"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/loans/products/charges/form/form.component.ts b/src/app/loans/products/charges/form/form.component.ts
new file mode 100644
index 0000000..96e51c6
--- /dev/null
+++ b/src/app/loans/products/charges/form/form.component.ts
@@ -0,0 +1,181 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnChanges, Output, SimpleChanges} from '@angular/core';
+import {ChargeDefinition} from '../../../../services/portfolio/domain/charge-definition.model';
+import {FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {ChargeMethod} from '../../../../services/portfolio/domain/charge-method.model';
+import {temporalOptionList} from '../../../../common/domain/temporal.domain';
+import {FimsValidators} from '../../../../common/validator/validators';
+import {ChargeProportionalDesignators} from '../../../../services/portfolio/domain/individuallending/charge-proportional-designators.model';
+import {FimsRange} from '../../../../services/portfolio/domain/range-model';
+
+interface ChargeMethodOption {
+ type: ChargeMethod;
+ label: string;
+}
+
+@Component({
+ selector: 'fims-product-charge-form-component',
+ templateUrl: './form.component.html'
+})
+export class ProductChargeFormComponent implements OnChanges {
+
+ numberFormat = '1.2-2';
+
+ chargeMethodOptions: ChargeMethodOption[] = [
+ { type: 'FIXED', label: 'Fixed'},
+ { type: 'PROPORTIONAL', label: 'Proportional'}
+ ];
+
+ proportionalToOptions: any[] = [
+ { label: 'Maximum balance', designator: ChargeProportionalDesignators.MAXIMUM_BALANCE_DESIGNATOR },
+ { label: 'Repayment', designator: ChargeProportionalDesignators.REQUESTED_DISBURSEMENT_DESIGNATOR },
+ { label: 'Running balance', designator: ChargeProportionalDesignators.RUNNING_BALANCE_DESIGNATOR }
+ ];
+
+ temporalOptions = temporalOptionList;
+
+ detailForm: FormGroup;
+
+ selectedRange: FimsRange;
+
+ @Input() editMode: boolean;
+
+ @Input() charge: ChargeDefinition;
+
+ @Input() ranges: FimsRange[];
+
+ @Output('onSave') onSave = new EventEmitter<ChargeDefinition>();
+
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder) {
+ this.detailForm = this.formBuilder.group({
+ identifier: ['', [Validators.required, Validators.minLength(3), Validators.maxLength(32), FimsValidators.urlSafe]],
+ name: ['', [Validators.required]],
+ description: ['', [Validators.required]],
+ chargeMethod: ['', [Validators.required]],
+ proportionalTo: ['', [Validators.required]],
+ amount: [0, [Validators.required]],
+ rangeEnabled: [false],
+ rangeIdentifier: ['', Validators.required],
+ rangeSegmentIdentifier: ['', Validators.required]
+ });
+
+ this.detailForm.get('chargeMethod').valueChanges
+ .subscribe(value => this.toggleChargeMethod(value));
+
+ this.detailForm.get('rangeIdentifier').valueChanges
+ .subscribe(identifier => this.toggleRange(identifier));
+
+ this.detailForm.get('rangeEnabled').valueChanges
+ .subscribe(enabled => this.toggleRangeEnabled(enabled));
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes.charge) {
+ const chargeData: any = {
+ identifier: this.charge.identifier,
+ name: this.charge.name,
+ description: this.charge.description,
+ chargeMethod: this.charge.chargeMethod,
+ proportionalTo: this.charge.proportionalTo,
+ amount: this.charge.amount
+ };
+
+ if (this.charge.forSegmentSet) {
+ chargeData.rangeEnabled = true;
+ chargeData.rangeIdentifier = this.charge.forSegmentSet;
+ chargeData.rangeSegmentIdentifier = this.charge.fromSegment ? [this.charge.fromSegment] : [];
+ }
+
+ this.detailForm.reset(chargeData);
+ }
+
+ if (changes.ranges) {
+ this.toggleRange(this.detailForm.get('rangeIdentifier').value);
+ }
+ }
+
+ private toggleChargeMethod(chargeMethod: ChargeMethod): void {
+ const proportionalTo = this.detailForm.get('proportionalTo');
+
+ if (chargeMethod === 'PROPORTIONAL') {
+ proportionalTo.enable();
+ proportionalTo.setValidators(Validators.required);
+ } else {
+ proportionalTo.disable();
+ proportionalTo.clearValidators();
+ }
+ proportionalTo.updateValueAndValidity();
+ }
+
+ toggleRange(identifier: string): void {
+ if (this.ranges) {
+ this.selectedRange = this.ranges.find(range => range.identifier === identifier);
+ }
+ }
+
+ toggleRangeEnabled(enabled: boolean): void {
+ const rangeIdentifier = this.detailForm.get('rangeIdentifier');
+ const rangeSegmentIdentifier = this.detailForm.get('rangeSegmentIdentifier');
+
+ if (enabled) {
+ rangeIdentifier.setValidators(Validators.required);
+ rangeSegmentIdentifier.setValidators(Validators.required);
+ } else {
+ rangeIdentifier.clearValidators();
+ rangeSegmentIdentifier.clearValidators();
+ }
+
+ rangeIdentifier.updateValueAndValidity();
+ rangeSegmentIdentifier.updateValueAndValidity();
+ }
+
+ save(): void {
+ const chargeMethod: ChargeMethod = this.detailForm.get('chargeMethod').value;
+
+ const charge: ChargeDefinition = Object.assign({}, this.charge, {
+ identifier: this.detailForm.get('identifier').value,
+ name: this.detailForm.get('name').value,
+ description: this.detailForm.get('description').value,
+ chargeMethod,
+ amount: this.detailForm.get('amount').value,
+ proportionalTo: chargeMethod === 'PROPORTIONAL' ? this.detailForm.get('proportionalTo').value : undefined
+ });
+
+ if (this.detailForm.get('rangeEnabled').value) {
+ charge.forSegmentSet = this.detailForm.get('rangeIdentifier').value;
+ const segmentIdentifier = this.detailForm.get('rangeSegmentIdentifier').value;
+ charge.fromSegment = segmentIdentifier;
+ charge.toSegment = segmentIdentifier;
+ } else {
+ delete charge.forSegmentSet;
+ delete charge.fromSegment;
+ delete charge.toSegment;
+ }
+
+ this.onSave.emit(charge);
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+}
diff --git a/src/app/loans/products/charges/ranges/form/create.component.html b/src/app/loans/products/charges/ranges/form/create.component.html
new file mode 100644
index 0000000..2f352cb
--- /dev/null
+++ b/src/app/loans/products/charges/ranges/form/create.component.html
@@ -0,0 +1,24 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Create range' | translate}}">
+ <fims-product-charge-range-form-component
+ [range]="range"
+ (onCancel)="cancel()"
+ (onSave)="save($event)">
+ </fims-product-charge-range-form-component>
+</fims-layout-card-over>
diff --git a/src/app/loans/products/charges/ranges/form/create.component.ts b/src/app/loans/products/charges/ranges/form/create.component.ts
new file mode 100644
index 0000000..40a66e5
--- /dev/null
+++ b/src/app/loans/products/charges/ranges/form/create.component.ts
@@ -0,0 +1,70 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import * as fromPortfolio from '../../../store/index';
+import {PortfolioStore} from '../../../store/index';
+import {RangeActions} from '../../../store/ranges/range.actions';
+import {FimsProduct} from '../../../store/model/fims-product.model';
+import {Subscription} from 'rxjs/Subscription';
+import {FimsRange} from '../../../../../services/portfolio/domain/range-model';
+
+@Component({
+ templateUrl: './create.component.html'
+})
+export class CreateProductChargeRangeFormComponent implements OnInit, OnDestroy {
+
+ private productSubscription: Subscription;
+
+ private product: FimsProduct;
+
+ range: FimsRange = {
+ identifier: '',
+ segments: [
+ { identifier: 'Start', start: 0 }
+ ]
+ };
+
+ constructor(private router: Router, private route: ActivatedRoute, private portfolioStore: PortfolioStore) {}
+
+ ngOnInit(): void {
+ this.productSubscription = this.portfolioStore.select(fromPortfolio.getSelectedProduct)
+ .filter(product => !!product)
+ .subscribe(product => this.product = product);
+ }
+
+ ngOnDestroy(): void {
+ this.productSubscription.unsubscribe();
+ }
+
+ save(resource: FimsRange): void {
+ this.portfolioStore.dispatch(RangeActions.createAction({
+ resource,
+ data: {
+ productIdentifier: this.product.identifier,
+ activatedRoute: this.route
+ }
+ }));
+ }
+
+ cancel(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+
+}
diff --git a/src/app/loans/products/charges/ranges/form/edit.component.html b/src/app/loans/products/charges/ranges/form/edit.component.html
new file mode 100644
index 0000000..475a26a
--- /dev/null
+++ b/src/app/loans/products/charges/ranges/form/edit.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Edit range' | translate}}">
+ <fims-product-charge-range-form-component
+ [range]="range$ | async"
+ (onCancel)="cancel()"
+ (onSave)="save($event)"
+ [editMode]="true">
+ </fims-product-charge-range-form-component>
+</fims-layout-card-over>
diff --git a/src/app/loans/products/charges/ranges/form/edit.component.ts b/src/app/loans/products/charges/ranges/form/edit.component.ts
new file mode 100644
index 0000000..f15fe47
--- /dev/null
+++ b/src/app/loans/products/charges/ranges/form/edit.component.ts
@@ -0,0 +1,69 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import * as fromPortfolio from '../../../store/index';
+import {PortfolioStore} from '../../../store/index';
+import {ActivatedRoute, Router} from '@angular/router';
+import {RangeActions} from '../../../store/ranges/range.actions';
+import {Subscription} from 'rxjs/Subscription';
+import {FimsProduct} from '../../../store/model/fims-product.model';
+import {Observable} from 'rxjs/Observable';
+import {FimsRange} from '../../../../../services/portfolio/domain/range-model';
+
+@Component({
+ templateUrl: './edit.component.html'
+})
+export class EditProductChargeRangeFormComponent implements OnInit, OnDestroy {
+
+ private productSubscription: Subscription;
+
+ private product: FimsProduct;
+
+ range$: Observable<FimsRange[]>;
+
+ constructor(private router: Router, private route: ActivatedRoute, private portfolioStore: PortfolioStore) {}
+
+ ngOnInit(): void {
+ this.productSubscription = this.portfolioStore.select(fromPortfolio.getSelectedProduct)
+ .filter(product => !!product)
+ .subscribe(product => this.product = product);
+
+ this.range$ = this.portfolioStore.select(fromPortfolio.getSelectedProductChargeRange);
+ }
+
+ ngOnDestroy(): void {
+ this.productSubscription.unsubscribe();
+ }
+
+ save(resource: FimsRange): void {
+ this.portfolioStore.dispatch(RangeActions.updateAction({
+ resource,
+ data: {
+ productIdentifier: this.product.identifier,
+ activatedRoute: this.route
+ }
+ }));
+ }
+
+
+ cancel(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+
+}
diff --git a/src/app/loans/products/charges/ranges/form/form.component.html b/src/app/loans/products/charges/ranges/form/form.component.html
new file mode 100644
index 0000000..f42cfc9
--- /dev/null
+++ b/src/app/loans/products/charges/ranges/form/form.component.html
@@ -0,0 +1,48 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Range details' | translate}}" [state]="form.valid ? 'complete' : form.pristine ? 'none' : 'required'" [active]="true">
+ <form [formGroup]="form">
+ <fims-id-input [form]="form" controlName="identifier" [readonly]="editMode"></fims-id-input>
+ <ng-container formArrayName="rangeSegments">
+ <div layout="row" *ngFor="let range of rangeSegmentControls; let i=index" [formGroupName]="i">
+ <fims-id-input [form]="getFormGroup(i)" controlName="identifier" [readonly]="editMode"></fims-id-input>
+ <mat-form-field layout-margin>
+ <input matInput type="number" formControlName="start" placeholder="Range start"/>
+ </mat-form-field>
+ <mat-form-field layout-margin>
+ <input matInput formControlName="end" placeholder="Range end" [value]="getNextSegmentValue(i)"/>
+ </mat-form-field>
+ <button mat-button (click)="removeRangeSegment(i)" *ngIf="i > 0">{{'REMOVE RANGE' | translate}}</button>
+ </div>
+ </ng-container>
+ <div layout="row">
+ <button flex mat-button mat-raised-button (click)="addRangeSegment()">{{'ADD RANGE' | translate}}</button>
+ </div>
+ </form>
+ <ng-template td-step-actions>
+ <fims-form-final-action
+ [resourceName]="'RANGE'"
+ [editMode]="editMode"
+ [disabled]="!form.valid"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/loans/products/charges/ranges/form/form.component.spec.ts b/src/app/loans/products/charges/ranges/form/form.component.spec.ts
new file mode 100644
index 0000000..51da6c0
--- /dev/null
+++ b/src/app/loans/products/charges/ranges/form/form.component.spec.ts
@@ -0,0 +1,18 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
diff --git a/src/app/loans/products/charges/ranges/form/form.component.ts b/src/app/loans/products/charges/ranges/form/form.component.ts
new file mode 100644
index 0000000..c6f06e9
--- /dev/null
+++ b/src/app/loans/products/charges/ranges/form/form.component.ts
@@ -0,0 +1,116 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, Output} from '@angular/core';
+import {AbstractControl, FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {FimsValidators} from '../../../../../common/validator/validators';
+import {FimsRange} from '../../../../../services/portfolio/domain/range-model';
+
+@Component({
+ selector: 'fims-product-charge-range-form-component',
+ templateUrl: './form.component.html'
+})
+export class ProductChargeRangeFormComponent {
+
+ form: FormGroup;
+
+ @Input() set range(range: FimsRange) {
+ this.form = this.formBuilder.group({
+ identifier: [range.identifier, [Validators.required, Validators.minLength(3), Validators.maxLength(32), FimsValidators.urlSafe]],
+ rangeSegments: this.initRangeSegments(range)
+ });
+
+ // TODO: Validate unique identifier and ranges across range segments
+ };
+
+ @Input() editMode: boolean;
+
+ @Output('onSave') onSave = new EventEmitter<FimsRange>();
+
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder) {}
+
+ private initRangeSegments(range: FimsRange): FormArray {
+ const formControls: FormGroup[] = [];
+
+ range.segments.forEach((segment, index) => {
+ formControls.push(this.initRange(segment.identifier, segment.start, index === 0));
+ });
+
+ return this.formBuilder.array(formControls);
+ }
+
+ private initRange(identifier: string = '', start: number = 0, disabled: boolean = false): FormGroup {
+ return this.formBuilder.group({
+ identifier: [identifier, [Validators.required, Validators.minLength(3), Validators.maxLength(32), FimsValidators.urlSafe]],
+ start: [{ value: start, disabled }, [Validators.required, FimsValidators.minValue(0)]],
+ end: [{ value: 0, disabled: true }, [Validators.required, FimsValidators.minValue(0)]],
+ });
+ }
+
+ get rangeSegments(): FormArray {
+ return this.form.get('rangeSegments') as FormArray;
+ }
+
+ addRangeSegment(): void {
+ this.rangeSegments.push(this.initRange());
+ }
+
+ removeRangeSegment(index: number): void {
+ this.rangeSegments.removeAt(index);
+ }
+
+ get rangeSegmentControls(): AbstractControl[] {
+ return this.rangeSegments.controls;
+ }
+
+ getFormGroup(index: number): FormGroup {
+ return this.rangeSegments.at(index) as FormGroup;
+ }
+
+ getNextSegmentValue(index: number): string {
+ const nextIndex = index + 1;
+
+ if (nextIndex >= this.rangeSegments.length) {
+ return '-';
+ }
+
+ const formGroup: FormGroup = this.getFormGroup(nextIndex);
+ return formGroup.get('start').value;
+ }
+
+ save(): void {
+ const formValue = this.form.getRawValue();
+
+ const range: FimsRange = {
+ identifier: formValue.identifier,
+ segments: formValue.rangeSegments.map(segment => ({
+ identifier: segment.identifier,
+ start: segment.start,
+ end: segment.end
+ }))
+ };
+
+ this.onSave.emit(range);
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+}
diff --git a/src/app/loans/products/charges/ranges/range-exists.guard.ts b/src/app/loans/products/charges/ranges/range-exists.guard.ts
new file mode 100644
index 0000000..6d52903
--- /dev/null
+++ b/src/app/loans/products/charges/ranges/range-exists.guard.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import {Injectable} from '@angular/core';
+import * as fromProducts from '../../store';
+import {Observable} from 'rxjs/Observable';
+import {of} from 'rxjs/observable/of';
+import {ExistsGuardService} from '../../../../common/guards/exists-guard';
+import {PortfolioService} from '../../../../services/portfolio/portfolio.service';
+import {PortfolioStore} from '../../store/index';
+import {RangeActions} from '../../store/ranges/range.actions';
+import {FimsRange} from '../../../../services/portfolio/domain/range-model';
+
+@Injectable()
+export class ProductChargeRangeExistsGuard implements CanActivate {
+
+ constructor(private store: PortfolioStore,
+ private portfolioService: PortfolioService,
+ private existsGuardService: ExistsGuardService) {}
+
+ hasRangeInStore(id: string): Observable<boolean> {
+ const timestamp$ = this.store.select(fromProducts.getProductChargeRangesLoadedAt)
+ .map(loadedAt => loadedAt[id]);
+
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ hasRangeInApi(productId: string, rangeId: string): Observable<boolean> {
+ const getRange = this.portfolioService.getRange(productId, rangeId)
+ .do((resource: FimsRange) => this.store.dispatch(RangeActions.loadAction({
+ resource
+ })))
+ .map(resource => !!resource);
+
+ return this.existsGuardService.routeTo404OnError(getRange);
+ }
+
+ hasRange(productId: string, rangeId: string): Observable<boolean> {
+ return this.hasRangeInStore(rangeId)
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+
+ return this.hasRangeInApi(productId, rangeId);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasRange(route.parent.params['productId'], route.params['rangeId']);
+ }
+}
diff --git a/src/app/loans/products/charges/ranges/range.detail.component.html b/src/app/loans/products/charges/ranges/range.detail.component.html
new file mode 100644
index 0000000..ad17279
--- /dev/null
+++ b/src/app/loans/products/charges/ranges/range.detail.component.html
@@ -0,0 +1,36 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over [title]="range.identifier" [navigateBackTo]="['../../../../../../../']" *ngIf="(range$ | async) as range">
+ <fims-layout-card-over-header-menu>
+ <button mat-icon-button (click)="deleteRange(range)" title="{{'Delete this range' | translate}}" *hasPermission="{ id: 'portfolio_products', accessLevel: 'CHANGE'}"><mat-icon>delete</mat-icon></button>
+ </fims-layout-card-over-header-menu>
+ <div class="mat-content inset" flex>
+ <div layout="row">
+ <td-data-table
+ flex
+ [data]="range?.segments"
+ [columns]="rangeColumns"
+ [selectable]="false"
+ [clickable]="false"
+ [multiple]="false"
+ [sortable]="false">
+ </td-data-table>
+ </div>
+ </div>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Edit range' | translate}}" icon="mode_edit" [link]="['edit']" [permission]="{ id: 'portfolio_products', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/loans/products/charges/ranges/range.detail.component.ts b/src/app/loans/products/charges/ranges/range.detail.component.ts
new file mode 100644
index 0000000..71eb006
--- /dev/null
+++ b/src/app/loans/products/charges/ranges/range.detail.component.ts
@@ -0,0 +1,79 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import {PortfolioStore} from '../../store/index';
+import * as fromPortfolio from '../../store';
+import {Observable} from 'rxjs/Observable';
+import {RangeActions} from '../../store/ranges/range.actions';
+import {ActivatedRoute} from '@angular/router';
+import {ITdDataTableColumn, TdDialogService} from '@covalent/core';
+import {FimsProduct} from '../../store/model/fims-product.model';
+import {Subscription} from 'rxjs/Subscription';
+import {FimsRange} from '../../../../services/portfolio/domain/range-model';
+
+@Component({
+ templateUrl: './range.detail.component.html'
+})
+export class ProductChargeRangeDetailComponent implements OnInit {
+
+ private productSubscription: Subscription;
+
+ private product: FimsProduct;
+
+ range$: Observable<FimsRange>;
+
+ rangeColumns: ITdDataTableColumn[] = [
+ { name: 'identifier', label: 'Identifier' },
+ { name: 'start', label: 'Start', numeric: true, format: value => value ? value.toFixed(2) : '-' },
+ { name: 'end', label: 'End', numeric: true, format: value => value ? value.toFixed(2) : '-' },
+ ];
+
+ constructor(private portfolioStore: PortfolioStore, private route: ActivatedRoute, private dialogService: TdDialogService) {}
+
+ ngOnInit(): void {
+ this.productSubscription = this.portfolioStore.select(fromPortfolio.getSelectedProduct)
+ .filter(product => !!product)
+ .subscribe(product => this.product = product);
+
+ this.range$ = this.portfolioStore.select(fromPortfolio.getSelectedProductChargeRange)
+ .filter(range => !!range);
+ }
+
+ confirmDeletion(): Observable<boolean> {
+ return this.dialogService.openConfirm({
+ message: 'Do you want to this range?',
+ title: 'Confirm deletion',
+ acceptButton: 'DELETE RANGE',
+ }).afterClosed();
+ }
+
+ deleteRange(resource: FimsRange): void {
+ this.confirmDeletion()
+ .filter(accept => accept)
+ .subscribe(() => {
+ this.portfolioStore.dispatch(RangeActions.deleteAction({
+ resource,
+ data: {
+ productIdentifier: this.product.identifier,
+ activatedRoute: this.route
+ }
+ }));
+ });
+ }
+}
diff --git a/src/app/loans/products/charges/ranges/range.index.component.html b/src/app/loans/products/charges/ranges/range.index.component.html
new file mode 100644
index 0000000..ca721b3
--- /dev/null
+++ b/src/app/loans/products/charges/ranges/range.index.component.html
@@ -0,0 +1,18 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<router-outlet></router-outlet>
diff --git a/src/app/loans/products/charges/ranges/range.index.component.ts b/src/app/loans/products/charges/ranges/range.index.component.ts
new file mode 100644
index 0000000..898c73e
--- /dev/null
+++ b/src/app/loans/products/charges/ranges/range.index.component.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {Subscription} from 'rxjs/Subscription';
+import {ActivatedRoute} from '@angular/router';
+import {RangeActions} from '../../store/ranges/range.actions';
+import {PortfolioStore} from '../../store/index';
+
+@Component({
+ templateUrl: './range.index.component.html'
+})
+export class ProductChargeRangeIndexComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ constructor(private route: ActivatedRoute, private store: PortfolioStore) {}
+
+ ngOnInit(): void {
+ this.actionsSubscription = this.route.params
+ .map(params => RangeActions.selectAction(params['rangeId']))
+ .subscribe(this.store);
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ }
+}
diff --git a/src/app/loans/products/charges/ranges/range.list.component.html b/src/app/loans/products/charges/ranges/range.list.component.html
new file mode 100644
index 0000000..1fac59a
--- /dev/null
+++ b/src/app/loans/products/charges/ranges/range.list.component.html
@@ -0,0 +1,27 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{ 'Manage ranges' | translate }}" [navigateBackTo]="['../../../../']">
+ <fims-data-table right flex
+ (onFetch)="fetchRanges($event)"
+ (onActionCellClick)="rowSelect($event)"
+ [columns]="columns"
+ [data]="rangesData$ | async"
+ [sortable]="false">
+ </fims-data-table>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Create new range' | translate}}" icon="add" [link]="['create']" [permission]="{ id: 'portfolio_products', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/loans/products/charges/ranges/range.list.component.ts b/src/app/loans/products/charges/ranges/range.list.component.ts
new file mode 100644
index 0000000..1059b44
--- /dev/null
+++ b/src/app/loans/products/charges/ranges/range.list.component.ts
@@ -0,0 +1,75 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {ITdDataTableColumn} from '@covalent/core';
+import {TableData, TableFetchRequest} from '../../../../common/data-table/data-table.component';
+import {Observable} from 'rxjs/Observable';
+import {PortfolioStore} from '../../store/index';
+import {RangeActions} from '../../store/ranges/range.actions';
+import {ActivatedRoute, Router} from '@angular/router';
+import * as fromPortfolio from '../../store';
+import {FimsRange} from '../../../../services/portfolio/domain/range-model';
+import {Subscription} from 'rxjs/Subscription';
+import {FimsProduct} from '../../store/model/fims-product.model';
+
+@Component({
+ templateUrl: './range.list.component.html'
+})
+export class ProductChargeRangeListComponent implements OnInit, OnDestroy {
+
+ private productSubscription: Subscription;
+
+ private product: FimsProduct;
+
+ rangesData$: Observable<TableData>;
+
+ columns: ITdDataTableColumn[] = [
+ { name: 'identifier', label: 'Id' }
+ ];
+
+ constructor(private router: Router, private route: ActivatedRoute, private portfolioStore: PortfolioStore) {}
+
+ ngOnInit(): void {
+ this.rangesData$ = this.portfolioStore.select(fromPortfolio.getAllProductChargeRangeEntities)
+ .map(data => ({
+ totalElements: data.length,
+ totalPages: 1,
+ data
+ }));
+
+ this.productSubscription = this.portfolioStore.select(fromPortfolio.getSelectedProduct)
+ .filter(product => !!product)
+ .subscribe(product => {
+ this.product = product;
+ this.fetchRanges();
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.productSubscription.unsubscribe();
+ }
+
+ fetchRanges(event?: TableFetchRequest): void {
+ this.portfolioStore.dispatch(RangeActions.loadAllAction(this.product.identifier));
+ }
+
+ rowSelect(range: FimsRange): void {
+ this.router.navigate(['detail', range.identifier], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/loans/products/components/term/term.component.html b/src/app/loans/products/components/term/term.component.html
new file mode 100644
index 0000000..bbd7198
--- /dev/null
+++ b/src/app/loans/products/components/term/term.component.html
@@ -0,0 +1,27 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<form [formGroup]="form">
+ <div layout-gt-xs="row">
+ <fims-text-input type="number" [form]="form" controlName="term" placeholder="{{'Term' | translate}}"></fims-text-input>
+ <mat-radio-group formControlName="temporalUnit">
+ <mat-radio-button *ngFor="let basis of temporalOptions" [value]="basis.type" layout-margin>
+ {{basis.label}}
+ </mat-radio-button>
+ </mat-radio-group>
+ </div>
+</form>
diff --git a/src/app/loans/products/components/term/term.component.ts b/src/app/loans/products/components/term/term.component.ts
new file mode 100644
index 0000000..11b1b8b
--- /dev/null
+++ b/src/app/loans/products/components/term/term.component.ts
@@ -0,0 +1,54 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input} from '@angular/core';
+import {FormComponent} from '../../../../common/forms/form.component';
+import {FormBuilder, Validators} from '@angular/forms';
+import {ChronoUnit} from '../../../../services/portfolio/domain/chrono-unit.model';
+import {temporalOptionList} from '../../../../common/domain/temporal.domain';
+import {FimsValidators} from '../../../../common/validator/validators';
+
+export interface TermRangeFormData {
+ temporalUnit: ChronoUnit;
+ term: number;
+}
+
+@Component({
+ selector: 'fims-product-term-form',
+ templateUrl: './term.component.html'
+})
+export class ProductTermFormComponent extends FormComponent<TermRangeFormData> {
+
+ temporalOptions = temporalOptionList;
+
+ @Input() set formData(termRange: TermRangeFormData) {
+ this.form = this.formBuilder.group({
+ term: [termRange.term, [ Validators.required, FimsValidators.minValue(0) ]],
+ temporalUnit: [termRange.temporalUnit, Validators.required],
+ });
+ };
+
+ constructor(private formBuilder: FormBuilder) {
+ super();
+ }
+
+ get formData(): TermRangeFormData {
+ return this.form.getRawValue();
+ }
+
+}
diff --git a/src/app/loans/products/form/create.component.html b/src/app/loans/products/form/create.component.html
new file mode 100644
index 0000000..7dfe563
--- /dev/null
+++ b/src/app/loans/products/form/create.component.html
@@ -0,0 +1,26 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Create new product' | translate}}">
+ <fims-product-form-component #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [currencies]="currencies$ | async"
+ [product]="product"
+ [error]="error$ | async">
+ </fims-product-form-component>
+</fims-layout-card-over>
diff --git a/src/app/loans/products/form/create.component.ts b/src/app/loans/products/form/create.component.ts
new file mode 100644
index 0000000..d28b445
--- /dev/null
+++ b/src/app/loans/products/form/create.component.ts
@@ -0,0 +1,101 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {ProductFormComponent} from './form.component';
+import {PortfolioStore} from '../store/index';
+import {CREATE, RESET_FORM} from '../store/product.actions';
+import * as fromPortfolio from '../store';
+import {Error} from '../../../services/domain/error.model';
+import {FimsProduct} from '../store/model/fims-product.model';
+import {Currency} from '../../../services/currency/domain/currency.model';
+import {CurrencyService} from '../../../services/currency/currency.service';
+import {Observable} from 'rxjs/Observable';
+
+@Component({
+ templateUrl: './create.component.html'
+})
+export class ProductCreateComponent implements OnInit, OnDestroy {
+
+ @ViewChild('form') formComponent: ProductFormComponent;
+
+ currencies$: Observable<Currency[]>;
+
+ error$: Observable<Error>;
+
+ product: FimsProduct = {
+ identifier: '',
+ name: '',
+ termRange: {
+ temporalUnit: 'MONTHS',
+ maximum: 1
+ },
+ balanceRange: {
+ minimum: 0,
+ maximum: 0
+ },
+ interestRange: {
+ minimum: 0,
+ maximum: 0
+ },
+ interestBasis: 'CURRENT_BALANCE',
+ patternPackage: 'org.apache.fineract.cn.portfolio.individuallending.v1',
+ description: '',
+ accountAssignments: [],
+ currencyCode: 'USD',
+ minorCurrencyUnitDigits: 2,
+ enabled: false,
+ parameters: {
+ minimumDispersalAmount: 0,
+ maximumDispersalAmount: 0,
+ maximumDispersalCount: 0,
+ moratoriums: []
+ }
+ };
+
+ constructor(private router: Router, private route: ActivatedRoute, private portfolioStore: PortfolioStore,
+ private currencyService: CurrencyService) {}
+
+ ngOnInit(): void {
+ this.error$ = this.portfolioStore.select(fromPortfolio.getProductFormError)
+ .filter(error => !!error);
+
+ this.currencies$ = this.currencyService.fetchCurrencies();
+ }
+
+ ngOnDestroy(): void {
+ this.portfolioStore.dispatch({ type: RESET_FORM });
+ }
+
+ onSave(product: FimsProduct): void {
+ this.portfolioStore.dispatch({ type: CREATE, payload: {
+ product: product,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel() {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+
+}
diff --git a/src/app/loans/products/form/detail/detail.component.html b/src/app/loans/products/form/detail/detail.component.html
new file mode 100644
index 0000000..20b2001
--- /dev/null
+++ b/src/app/loans/products/form/detail/detail.component.html
@@ -0,0 +1,54 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<form [formGroup]="form" layout="column">
+ <fims-id-input [form]="form" [placeholder]="'Short name'" controlName="identifier" [readonly]="editMode"></fims-id-input>
+ <fims-text-input [form]="form" controlName="name" placeholder="{{'Name' | translate}}"></fims-text-input>
+ <mat-form-field layout-margin flex>
+ <textarea matInput placeholder="{{'Description' | translate}}" formControlName="description"></textarea>
+ <mat-error *ngIf="form.get('description').hasError('required')" translate>
+ Required
+ </mat-error>
+ <mat-error *ngIf="form.get('description').hasError('maxlength')">
+ {{ 'Only characters allowed.' | translate:{ value: form.get('description').getError('maxlength')['requiredLength']} }}
+ </mat-error>
+ </mat-form-field>
+ <mat-form-field layout-margin>
+ <mat-select formControlName="currencyCode" placeholder="{{ 'Currency' | translate }}">
+ <mat-option *ngFor="let currency of currencies" [value]="currency.code">
+ {{currency.code}}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ <fims-min-max flex
+ minPlaceholder="{{'Minimum principal amount' | translate}}"
+ maxPlaceholder="{{'Maximum principal amount' | translate}}"
+ [form]="form"
+ minControlName="minimumBalance"
+ maxControlName="maximumBalance"
+ [requireDecimal]="false"
+ [decimalLimit]="2">
+ </fims-min-max>
+ <div layout="row">
+ <fims-text-input type="number" [form]="form" controlName="term" placeholder="{{'Maximum Term' | translate}}"></fims-text-input>
+ <mat-radio-group formControlName="temporalUnit">
+ <mat-radio-button *ngFor="let basis of temporalOptions" [value]="basis.type" layout-margin>
+ {{basis.label}}
+ </mat-radio-button>
+ </mat-radio-group>
+ </div>
+</form>
diff --git a/src/app/loans/products/form/detail/detail.component.spec.ts b/src/app/loans/products/form/detail/detail.component.spec.ts
new file mode 100644
index 0000000..95cc5f7
--- /dev/null
+++ b/src/app/loans/products/form/detail/detail.component.spec.ts
@@ -0,0 +1,105 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {Component, ViewChild} from '@angular/core';
+import {ReactiveFormsModule} from '@angular/forms';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {DetailFormData, ProductDetailFormComponent} from './detail.component';
+import {FimsSharedModule} from '../../../../common/common.module';
+import {MatInputModule, MatRadioModule, MatSelectModule} from '@angular/material';
+
+describe('Test product detail component', () => {
+
+ const validFormData: DetailFormData = {
+ identifier: 'test',
+ minimumBalance: '1000',
+ maximumBalance: '2000',
+ temporalUnit: 'WEEKS',
+ description: 'description',
+ currencyCode: 'USD',
+ name: 'test',
+ term: 12
+ };
+
+ const invalidFormData: DetailFormData = {
+ identifier: 'test',
+ minimumBalance: '2000',
+ maximumBalance: '1000',
+ temporalUnit: 'WEEKS',
+ description: 'description',
+ currencyCode: 'USD',
+ name: 'test',
+ term: 12
+ };
+
+ let component: TestComponent;
+ let fixture: ComponentFixture<TestComponent>;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot(),
+ ReactiveFormsModule,
+ MatRadioModule,
+ MatInputModule,
+ MatSelectModule,
+ FimsSharedModule,
+ NoopAnimationsModule
+ ],
+ declarations: [
+ TestComponent,
+ ProductDetailFormComponent
+ ]
+ });
+
+ fixture = TestBed.createComponent(TestComponent);
+
+ component = fixture.componentInstance;
+ });
+
+ it('should return same interest form data', () => {
+ component.formData = validFormData;
+ fixture.detectChanges();
+ expect(component.form.formData).toEqual(component.formData);
+ });
+
+ it('should mark form as valid when range is valid', () => {
+ component.formData = validFormData;
+ fixture.detectChanges();
+ expect(component.form.valid).toBeTruthy();
+ });
+
+ it('should mark form as invalid when range is invalid', () => {
+ component.formData = invalidFormData;
+ fixture.detectChanges();
+ expect(component.form.valid).toBeFalsy();
+ });
+
+});
+
+@Component({
+ template: '<fims-product-detail-form #form [formData]="formData"></fims-product-detail-form>'
+})
+class TestComponent {
+
+ @ViewChild('form') form: ProductDetailFormComponent;
+
+ formData: DetailFormData;
+}
diff --git a/src/app/loans/products/form/detail/detail.component.ts b/src/app/loans/products/form/detail/detail.component.ts
new file mode 100644
index 0000000..bf01bea
--- /dev/null
+++ b/src/app/loans/products/form/detail/detail.component.ts
@@ -0,0 +1,96 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input, OnChanges, SimpleChanges} from '@angular/core';
+import {FormComponent} from '../../../../common/forms/form.component';
+import {FormBuilder, Validators} from '@angular/forms';
+import {FimsValidators} from '../../../../common/validator/validators';
+import {ChronoUnit} from '../../../../services/portfolio/domain/chrono-unit.model';
+import {Currency} from '../../../../services/currency/domain/currency.model';
+import {Error} from '../../../../services/domain/error.model';
+import {TemporalOption} from '../../../../common/domain/temporal.domain';
+
+export interface DetailFormData {
+ identifier: string;
+ name: string;
+ description: string;
+ currencyCode: string;
+ minimumBalance: string;
+ maximumBalance: string;
+ term: number;
+ temporalUnit: ChronoUnit;
+}
+
+@Component({
+ selector: 'fims-product-detail-form',
+ templateUrl: './detail.component.html'
+})
+export class ProductDetailFormComponent extends FormComponent<DetailFormData> implements OnChanges {
+
+ private _formData: DetailFormData;
+
+ @Input('formData') set formData(formData: DetailFormData) {
+ this._formData = formData;
+ };
+
+ @Input('editMode') editMode: boolean;
+
+ @Input('temporalOptions') temporalOptions: TemporalOption[];
+
+ @Input('currencies') currencies: Currency[];
+
+ @Input('error') error: Error;
+
+ constructor(private formBuilder: FormBuilder) {
+ super();
+
+ this.form = this.formBuilder.group({
+ identifier: ['', [Validators.required, Validators.minLength(3), Validators.maxLength(32), FimsValidators.urlSafe]],
+ name: ['', [Validators.required, Validators.maxLength(256)]],
+ description: ['', [Validators.required, Validators.maxLength(4096)]],
+ currencyCode: ['', [Validators.required]],
+ minimumBalance: ['', [Validators.required, FimsValidators.minValue(0)]],
+ maximumBalance: ['', [Validators.required, FimsValidators.minValue(0)]],
+ term: ['', [ Validators.required, FimsValidators.minValue(1), FimsValidators.maxScale(0)]],
+ temporalUnit: ['', Validators.required]
+ }, { validator: FimsValidators.greaterThanEquals('minimumBalance', 'maximumBalance') });
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes.formData) {
+ this.form.reset({
+ identifier: this._formData.identifier,
+ name: this._formData.name,
+ description: this._formData.description,
+ currencyCode: this._formData.currencyCode,
+ minimumBalance: this._formData.minimumBalance,
+ maximumBalance: this._formData.maximumBalance,
+ term: this._formData.term,
+ temporalUnit: this._formData.temporalUnit
+ });
+ }
+
+ if (changes.error) {
+ this.setError('identifier', 'unique', true);
+ }
+ }
+
+ get formData(): DetailFormData {
+ return this.form.getRawValue();
+ }
+}
diff --git a/src/app/loans/products/form/edit.component.html b/src/app/loans/products/form/edit.component.html
new file mode 100644
index 0000000..76bb79e
--- /dev/null
+++ b/src/app/loans/products/form/edit.component.html
@@ -0,0 +1,26 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Edit product' | translate}}">
+ <fims-product-form-component #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [currencies]="currencies$ | async"
+ [product]="product"
+ [editMode]="true">
+ </fims-product-form-component>
+</fims-layout-card-over>
diff --git a/src/app/loans/products/form/edit.component.ts b/src/app/loans/products/form/edit.component.ts
new file mode 100644
index 0000000..acb7311
--- /dev/null
+++ b/src/app/loans/products/form/edit.component.ts
@@ -0,0 +1,72 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {Product} from '../../../services/portfolio/domain/product.model';
+import {ActivatedRoute, Router} from '@angular/router';
+import {PortfolioStore} from '../store/index';
+import {RESET_FORM, UPDATE} from '../store/product.actions';
+import {Subscription} from 'rxjs/Subscription';
+import * as fromPortfolio from '../store';
+import {FimsProduct} from '../store/model/fims-product.model';
+import {Currency} from '../../../services/currency/domain/currency.model';
+import {CurrencyService} from '../../../services/currency/currency.service';
+import {Observable} from 'rxjs/Observable';
+
+@Component({
+ templateUrl: './edit.component.html'
+})
+export class ProductEditComponent implements OnInit, OnDestroy {
+
+ private productSubscription: Subscription;
+
+ currencies$: Observable<Currency[]>;
+
+ product: FimsProduct;
+
+ constructor(private router: Router, private route: ActivatedRoute, private portfolioStore: PortfolioStore,
+ private currencyService: CurrencyService) {}
+
+ ngOnInit() {
+ this.productSubscription = this.portfolioStore.select(fromPortfolio.getSelectedProduct)
+ .subscribe(product => this.product = product);
+
+ this.currencies$ = this.currencyService.fetchCurrencies();
+ }
+
+ ngOnDestroy(): void {
+ this.productSubscription.unsubscribe();
+
+ this.portfolioStore.dispatch({ type: RESET_FORM });
+ }
+
+ onSave(product: Product) {
+ this.portfolioStore.dispatch({ type: UPDATE, payload: {
+ product: product,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel() {
+ this.navigateAway();
+ }
+
+ private navigateAway() {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/loans/products/form/fees/fee.component.html b/src/app/loans/products/form/fees/fee.component.html
new file mode 100644
index 0000000..821a0b5
--- /dev/null
+++ b/src/app/loans/products/form/fees/fee.component.html
@@ -0,0 +1,59 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<form [formGroup]="form">
+ <fims-account-select title="Processing fee income account(Revenue accounts only)" formControlName="processingFeeAccount" [type]="'REVENUE'">
+ <ng-container *ngIf="!form.get('processingFeeAccount').pristine && form.get('processingFeeAccount').hasError('required')" translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="form.get('processingFeeAccount').hasError('invalidAccount')" translate>
+ Invalid account
+ </ng-container>
+ </fims-account-select>
+ <fims-account-select title="Loan origination fee income account(Revenue accounts only)" formControlName="originationFeeAccount" [type]="'REVENUE'">
+ <ng-container *ngIf="!form.get('originationFeeAccount').pristine && form.get('originationFeeAccount').hasError('required')" translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="form.get('originationFeeAccount').hasError('invalidAccount')" translate>
+ Invalid account
+ </ng-container>
+ </fims-account-select>
+ <fims-account-select title="Disbursement fee income account(Revenue accounts only)" formControlName="disbursementFeeAccount" [type]="'REVENUE'">
+ <ng-container *ngIf="!form.get('disbursementFeeAccount').pristine && form.get('disbursementFeeAccount').hasError('required')" translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="form.get('disbursementFeeAccount').hasError('invalidAccount')" translate>
+ Invalid account
+ </ng-container>
+ </fims-account-select>
+ <fims-account-select title="Late fee income account(Revenue accounts only)" formControlName="lateFeeIncomeAccount" [type]="'REVENUE'">
+ <ng-container *ngIf="!form.get('lateFeeIncomeAccount').pristine && form.get('lateFeeIncomeAccount').hasError('required')" translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="form.get('lateFeeIncomeAccount').hasError('invalidAccount')" translate>
+ Invalid account
+ </ng-container>
+ </fims-account-select>
+ <fims-account-select title="Late fee accrual account(Asset accounts only)" formControlName="lateFeeAccrualAccount" [type]="'ASSET'">
+ <ng-container *ngIf="!form.get('lateFeeAccrualAccount').pristine && form.get('lateFeeAccrualAccount').hasError('required')" translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="form.get('lateFeeAccrualAccount').hasError('invalidAccount')" translate>
+ Invalid account
+ </ng-container>
+ </fims-account-select>
+</form>
diff --git a/src/app/loans/products/form/fees/fee.component.ts b/src/app/loans/products/form/fees/fee.component.ts
new file mode 100644
index 0000000..35c0e12
--- /dev/null
+++ b/src/app/loans/products/form/fees/fee.component.ts
@@ -0,0 +1,57 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input} from '@angular/core';
+import {FormComponent} from '../../../../common/forms/form.component';
+import {FormBuilder, Validators} from '@angular/forms';
+import {AccountingService} from '../../../../services/accounting/accounting.service';
+import {accountExists} from '../../../../common/validator/account-exists.validator';
+
+export interface FeeFormData {
+ processingFeeAccount: string;
+ originationFeeAccount: string;
+ disbursementFeeAccount: string;
+ lateFeeIncomeAccount: string;
+ lateFeeAccrualAccount: string;
+}
+
+@Component({
+ selector: 'fims-product-fee-form',
+ templateUrl: './fee.component.html'
+})
+export class ProductFeeFormComponent extends FormComponent<FeeFormData> {
+
+ @Input() set formData(feeFormData: FeeFormData) {
+ this.form = this.formBuilder.group({
+ processingFeeAccount: [feeFormData.processingFeeAccount, [Validators.required], accountExists(this.accountingService)],
+ originationFeeAccount: [feeFormData.originationFeeAccount, [Validators.required], accountExists(this.accountingService)],
+ disbursementFeeAccount: [feeFormData.disbursementFeeAccount, [Validators.required], accountExists(this.accountingService)],
+ lateFeeIncomeAccount: [feeFormData.lateFeeIncomeAccount, [Validators.required], accountExists(this.accountingService)],
+ lateFeeAccrualAccount: [feeFormData.lateFeeAccrualAccount, [Validators.required], accountExists(this.accountingService)]
+ });
+ }
+
+ constructor(private formBuilder: FormBuilder, private accountingService: AccountingService) {
+ super();
+ }
+
+ get formData(): FeeFormData {
+ return this.form.getRawValue();
+ }
+
+}
diff --git a/src/app/loans/products/form/form.component.html b/src/app/loans/products/form/form.component.html
new file mode 100644
index 0000000..3287d74
--- /dev/null
+++ b/src/app/loans/products/form/form.component.html
@@ -0,0 +1,67 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Product details' | translate}}" [state]="detailForm.valid ? 'complete' : detailForm.pristine ? 'none' : 'required'">
+ <fims-product-detail-form #detailForm [formData]="detailFormData" [currencies]="currencies" [temporalOptions]="temporalOptions" [error]="error" [editMode]="editMode"></fims-product-detail-form>
+ <ng-template td-step-actions>
+ <fims-form-continue-action (onContinue)="settingsStep.open()"></fims-form-continue-action>
+ </ng-template>
+ </td-step>
+ <td-step #settingsStep label="{{'Ledger and account settings' | translate}}" [state]="settingsForm.valid ? 'complete' : settingsForm.pristine ? 'none' : 'required'">
+ <fims-product-settings-form #settingsForm [formData]="settingsFormData"></fims-product-settings-form>
+ <ng-template td-step-actions>
+ <fims-form-continue-action (onContinue)="interestStep.open()"></fims-form-continue-action>
+ </ng-template>
+ </td-step>
+ <td-step #interestStep label="{{'Interest settings' | translate}}" [state]="interestForm.valid ? 'complete' : interestForm.pristine ? 'none' : 'required'">
+ <fims-product-interests-form #interestForm [formData]="interestFormData"></fims-product-interests-form>
+ <ng-template td-step-actions>
+ <fims-form-continue-action (onContinue)="feesStep.open()"></fims-form-continue-action>
+ </ng-template>
+ </td-step>
+ <td-step #feesStep label="{{'Fee income accounts' | translate}}" [state]="feeForm.valid ? 'complete' : feeForm.pristine ? 'none' : 'required'">
+ <fims-product-fee-form #feeForm [formData]="feeFormData"></fims-product-fee-form>
+ <ng-template td-step-actions>
+ <fims-form-continue-action (onContinue)="arrearsStep.open()"></fims-form-continue-action>
+ </ng-template>
+ </td-step>
+ <td-step #arrearsStep label="{{'Arrears allowance reserve account' | translate}}" [state]="arrearsAllowanceForm.valid ? 'complete' : arrearsAllowanceForm.pristine ? 'none' : 'required'">
+ <form [formGroup]="arrearsAllowanceForm">
+ <fims-account-select title="{{'Arrears allowance(Expense accounts only)' | translate}}" formControlName="account" [type]="'EXPENSE'">
+ <ng-container *ngIf="!arrearsAllowanceForm.get('account').pristine && arrearsAllowanceForm.get('account').hasError('required')" translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="arrearsAllowanceForm.get('account').hasError('invalidAccount')" translate>
+ Invalid account
+ </ng-container>
+ </fims-account-select>
+ </form>
+ </td-step>
+
+ <td-step label="{{'Final step' | translate}}" [state]="'complete'">
+ <ng-template td-step-summary>
+ <fims-form-final-action
+ [resourceName]="'PRODUCT'"
+ [editMode]="editMode"
+ [disabled]="!isValid"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/loans/products/form/form.component.ts b/src/app/loans/products/form/form.component.ts
new file mode 100644
index 0000000..4cb49e0
--- /dev/null
+++ b/src/app/loans/products/form/form.component.ts
@@ -0,0 +1,232 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
+import {FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {TdStepComponent} from '@covalent/core';
+import {InterestFormData, ProductInterestFormComponent} from './interests/interests.component';
+import {AccountAssignment} from '../../../services/portfolio/domain/account-assignment.model';
+import {AccountDesignators} from '../../../services/portfolio/domain/individuallending/account-designators.model';
+import {FeeFormData, ProductFeeFormComponent} from './fees/fee.component';
+import {ProductParameters} from '../../../services/portfolio/domain/individuallending/product-parameters.model';
+import {AccountingService} from '../../../services/accounting/accounting.service';
+import {FimsProduct} from '../store/model/fims-product.model';
+import {accountExists} from '../../../common/validator/account-exists.validator';
+import {ProductSettingsFormComponent, SettingsFormData} from './settings/settings.component';
+import {temporalOptionList} from '../../../common/domain/temporal.domain';
+import {Currency} from '../../../services/currency/domain/currency.model';
+import {
+ accountIdentifier,
+ createAccountAssignment,
+ createLedgerAssignment,
+ findAccountDesignator,
+ ledgerIdentifier
+} from '../../../common/util/account-assignments';
+import {DetailFormData, ProductDetailFormComponent} from './detail/detail.component';
+import {Error} from '../../../services/domain/error.model';
+
+@Component({
+ selector: 'fims-product-form-component',
+ templateUrl: './form.component.html'
+})
+export class ProductFormComponent implements OnInit {
+
+ private _error: Error;
+
+ temporalOptions = temporalOptionList;
+
+ arrearsAllowanceForm: FormGroup;
+
+ @ViewChild('detailsStep') step: TdStepComponent;
+
+ @Input('editMode') editMode: boolean;
+
+ @Input('product') set product(product: FimsProduct) {
+ this.prepareDetailForm(product);
+ this.prepareSettingsForm(product);
+ this.prepareInterestForm(product);
+ this.prepareFeeForm(product);
+ this.prepareAllowanceForm(product);
+ };
+
+ @Input('currencies') currencies: Currency[];
+
+ @Input('error') set error(error: Error) {
+ this._error = error;
+ this.step.open();
+ };
+
+ @Output('onSave') onSave = new EventEmitter<FimsProduct>();
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ @ViewChild('detailForm') detailForm: ProductDetailFormComponent;
+ detailFormData: DetailFormData;
+
+ @ViewChild('settingsForm') settingsForm: ProductSettingsFormComponent;
+ settingsFormData: SettingsFormData;
+
+ @ViewChild('interestForm') interestForm: ProductInterestFormComponent;
+ interestFormData: InterestFormData;
+
+ @ViewChild('feeForm') feeForm: ProductFeeFormComponent;
+ feeFormData: FeeFormData;
+
+ constructor(private formBuilder: FormBuilder, private accountingService: AccountingService) {}
+
+ ngOnInit(): void {
+ this.step.open();
+ }
+
+ get isValid(): boolean {
+ return this.detailForm.valid &&
+ this.settingsForm.valid &&
+ this.interestForm.valid &&
+ this.feeForm.valid &&
+ this.arrearsAllowanceForm.valid;
+ }
+
+ save(): void {
+ const parameters: ProductParameters = {
+ minimumDispersalAmount: 0,
+ maximumDispersalAmount: 0,
+ maximumDispersalCount: 0,
+ moratoriums: []
+ };
+
+ const foundCurrency = this.currencies.find(currency => currency.code === this.detailForm.formData.currencyCode);
+
+ const product: FimsProduct = {
+ identifier: this.detailForm.formData.identifier,
+ name: this.detailForm.formData.name,
+ description: this.detailForm.formData.description,
+ minorCurrencyUnitDigits: foundCurrency.digits,
+ currencyCode: foundCurrency.code,
+ interestBasis: 'CURRENT_BALANCE',
+ interestRange: {
+ minimum: parseFloat(this.interestForm.formData.minimum),
+ maximum: parseFloat(this.interestForm.formData.maximum),
+ },
+ termRange: {
+ maximum: this.detailForm.formData.term,
+ temporalUnit: this.detailForm.formData.temporalUnit
+ },
+ balanceRange: {
+ minimum: parseFloat(this.detailForm.formData.minimumBalance),
+ maximum: parseFloat(this.detailForm.formData.maximumBalance)
+ },
+ parameters,
+ patternPackage: 'org.apache.fineract.cn.individuallending.api.v1',
+ accountAssignments: this.collectAccountAssignments()
+ };
+
+ this.onSave.emit(product);
+ }
+
+ private collectAccountAssignments(): AccountAssignment[] {
+ const assignments: AccountAssignment[] = [];
+
+ assignments.push(createAccountAssignment(this.settingsForm.formData.loanFundAccount, AccountDesignators.LOAN_FUNDS_SOURCE));
+
+ assignments.push(createLedgerAssignment(this.settingsForm.formData.customerLoanLedger, AccountDesignators.CUSTOMER_LOAN_PRINCIPAL));
+ assignments.push(createLedgerAssignment(this.settingsForm.formData.customerLoanLedger, AccountDesignators.CUSTOMER_LOAN_INTEREST));
+ assignments.push(createLedgerAssignment(this.settingsForm.formData.customerLoanLedger, AccountDesignators.CUSTOMER_LOAN_FEES));
+
+ assignments.push(createLedgerAssignment(this.settingsForm.formData.customerLoanLedger, AccountDesignators.CUSTOMER_LOAN_FEES));
+
+ assignments.push(createAccountAssignment(this.feeForm.formData.processingFeeAccount, AccountDesignators.PROCESSING_FEE_INCOME));
+ assignments.push(createAccountAssignment(this.feeForm.formData.disbursementFeeAccount, AccountDesignators.DISBURSEMENT_FEE_INCOME));
+ assignments.push(createAccountAssignment(this.feeForm.formData.lateFeeIncomeAccount, AccountDesignators.LATE_FEE_INCOME));
+ assignments.push(createAccountAssignment(this.feeForm.formData.lateFeeAccrualAccount, AccountDesignators.LATE_FEE_ACCRUAL));
+ assignments.push(createAccountAssignment(this.feeForm.formData.originationFeeAccount, AccountDesignators.ORIGINATION_FEE_INCOME));
+
+ assignments.push(createAccountAssignment(this.interestForm.formData.incomeAccount, AccountDesignators.INTEREST_INCOME));
+ assignments.push(createAccountAssignment(this.interestForm.formData.accrualAccount, AccountDesignators.INTEREST_ACCRUAL));
+
+ assignments.push(createAccountAssignment(this.arrearsAllowanceForm.get('account').value, AccountDesignators.GENERAL_LOSS_ALLOWANCE));
+
+ return assignments;
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+ prepareDetailForm(product: FimsProduct): void {
+ this.detailFormData = {
+ identifier: product.identifier,
+ name: product.name,
+ description: product.description,
+ currencyCode: product.currencyCode,
+ minimumBalance: product.balanceRange.minimum.toFixed(2),
+ maximumBalance: product.balanceRange.maximum.toFixed(2),
+ term: product.termRange.maximum,
+ temporalUnit: product.termRange.temporalUnit
+ };
+ }
+
+ prepareSettingsForm(product: FimsProduct) {
+ const loanFoundAccount = findAccountDesignator(product.accountAssignments, AccountDesignators.LOAN_FUNDS_SOURCE);
+ const customerLoanLedger = findAccountDesignator(product.accountAssignments, AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
+
+ this.settingsFormData = {
+ loanFundAccount: accountIdentifier(loanFoundAccount),
+ customerLoanLedger: ledgerIdentifier(customerLoanLedger),
+ };
+ }
+
+ private prepareInterestForm(product: FimsProduct) {
+ const interestIncome = findAccountDesignator(product.accountAssignments, AccountDesignators.INTEREST_INCOME);
+ const interestAccrual = findAccountDesignator(product.accountAssignments, AccountDesignators.INTEREST_ACCRUAL);
+ const interestRange = product.interestRange;
+
+ this.interestFormData = {
+ minimum: interestRange.minimum.toFixed(2),
+ maximum: interestRange.maximum.toFixed(2),
+ incomeAccount: accountIdentifier(interestIncome),
+ accrualAccount: accountIdentifier(interestAccrual)
+ };
+ }
+
+ private prepareFeeForm(product: FimsProduct) {
+ const processingFeeDesignator = findAccountDesignator(product.accountAssignments, AccountDesignators.PROCESSING_FEE_INCOME);
+ const disbursementFeeDesignator = findAccountDesignator(product.accountAssignments, AccountDesignators.DISBURSEMENT_FEE_INCOME);
+ const lateFeeIncomeDesignator = findAccountDesignator(product.accountAssignments, AccountDesignators.LATE_FEE_INCOME);
+ const lateFeeAccrualDesignator = findAccountDesignator(product.accountAssignments, AccountDesignators.LATE_FEE_ACCRUAL);
+ const loanOriginationFeeDesignator = findAccountDesignator(product.accountAssignments, AccountDesignators.ORIGINATION_FEE_INCOME);
+
+ this.feeFormData = {
+ disbursementFeeAccount: accountIdentifier(disbursementFeeDesignator),
+ lateFeeIncomeAccount: accountIdentifier(lateFeeIncomeDesignator),
+ lateFeeAccrualAccount: accountIdentifier(lateFeeAccrualDesignator),
+ processingFeeAccount: accountIdentifier(processingFeeDesignator),
+ originationFeeAccount: accountIdentifier(loanOriginationFeeDesignator)
+ };
+ }
+
+ private prepareAllowanceForm(product: FimsProduct) {
+ const allowanceDesignator = findAccountDesignator(product.accountAssignments, AccountDesignators.GENERAL_LOSS_ALLOWANCE);
+ this.arrearsAllowanceForm = this.formBuilder.group({
+ account: [accountIdentifier(allowanceDesignator), [Validators.required], accountExists(this.accountingService)],
+ });
+ }
+
+ get error(): Error {
+ return this._error;
+ }
+
+}
diff --git a/src/app/loans/products/form/interests/interest.component.spec.ts b/src/app/loans/products/form/interests/interest.component.spec.ts
new file mode 100644
index 0000000..cea60a1
--- /dev/null
+++ b/src/app/loans/products/form/interests/interest.component.spec.ts
@@ -0,0 +1,102 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {InterestFormData, ProductInterestFormComponent} from './interests.component';
+import {Component, CUSTOM_ELEMENTS_SCHEMA, ViewChild} from '@angular/core';
+import {ReactiveFormsModule} from '@angular/forms';
+import {AccountingService} from '../../../../services/accounting/accounting.service';
+import {FimsSharedModule} from '../../../../common/common.module';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {MatInputModule, MatRadioModule, MatSlideToggleModule} from '@angular/material';
+
+describe('Test product interest component', () => {
+
+ const validFormData: InterestFormData = {
+ minimum: '1.23',
+ maximum: '4.56',
+ accrualAccount: 'accrualAccount',
+ incomeAccount: 'incomeAccount'
+ };
+
+ const invalidFormData: InterestFormData = {
+ minimum: '4.56',
+ maximum: '1.23',
+ accrualAccount: 'accrualAccount',
+ incomeAccount: 'incomeAccount'
+ };
+
+ let component: TestComponent;
+ let fixture: ComponentFixture<TestComponent>;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot(),
+ ReactiveFormsModule,
+ MatInputModule,
+ MatRadioModule,
+ MatSlideToggleModule,
+ FimsSharedModule,
+ NoopAnimationsModule
+ ],
+ declarations: [
+ TestComponent,
+ ProductInterestFormComponent
+ ],
+ providers: [
+ { provide: AccountingService, useValue: jasmine.createSpyObj('accountingService', ['findAccount', 'fetchAccounts']) }
+ ],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA]
+ });
+
+ fixture = TestBed.createComponent(TestComponent);
+
+ component = fixture.componentInstance;
+ });
+
+ it('should return same interest form data', () => {
+ component.formData = validFormData;
+ fixture.detectChanges();
+ expect(component.form.formData).toEqual(component.formData);
+ });
+
+ it('should mark form as valid when range is valid', () => {
+ component.formData = validFormData;
+ fixture.detectChanges();
+ expect(component.form.valid).toBeTruthy();
+ });
+
+ it('should mark form as invalid when range is invalid', () => {
+ component.formData = invalidFormData;
+ fixture.detectChanges();
+ expect(component.form.valid).toBeFalsy();
+ });
+
+});
+
+@Component({
+ template: '<fims-product-interests-form #form [formData]="formData"></fims-product-interests-form>'
+})
+class TestComponent {
+
+ @ViewChild('form') form: ProductInterestFormComponent;
+
+ formData: InterestFormData;
+}
diff --git a/src/app/loans/products/form/interests/interests.component.html b/src/app/loans/products/form/interests/interests.component.html
new file mode 100644
index 0000000..1a786ad
--- /dev/null
+++ b/src/app/loans/products/form/interests/interests.component.html
@@ -0,0 +1,47 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<form [formGroup]="form">
+ <div layout="row">
+ <mat-slide-toggle formControlName="interestRangeEnabled" layout-margin translate>
+ Interest range?
+ </mat-slide-toggle>
+ </div>
+ <div layout="row" *ngIf="form.get('interestRangeEnabled').value === false">
+ <fims-number-input [form]="form" controlName="minimum" placeholder="{{'Interest rate' | translate}}"></fims-number-input>
+ </div>
+ <div layout="row" *ngIf="form.get('interestRangeEnabled').value === true">
+ <fims-min-max minPlaceholder="{{'Minimum interest rate' | translate}}" maxPlaceholder="{{'Maximum interest rate' | translate}}" [form]="form" minControlName="minimum" maxControlName="maximum"></fims-min-max>
+ </div>
+ <fims-account-select title="{{'Interest income account(Revenue accounts only)' | translate}}" formControlName="incomeAccount" [type]="'REVENUE'">
+ <ng-container *ngIf="!form.get('incomeAccount').pristine && form.get('incomeAccount').hasError('required')">
+ Required
+ </ng-container>
+ <ng-container *ngIf="form.get('incomeAccount').hasError('invalidAccount')">
+ Invalid account
+ </ng-container>
+ </fims-account-select>
+ <fims-account-select title="{{'Interest accrual account(Asset accounts only)' | translate}}" formControlName="accrualAccount" [type]="'ASSET'">
+ <ng-container *ngIf="!form.get('accrualAccount').pristine && form.get('accrualAccount').hasError('required')">
+ Required
+ </ng-container>
+ <ng-container *ngIf="form.get('accrualAccount').hasError('invalidAccount')">
+ Invalid account
+ </ng-container>
+ </fims-account-select>
+ <p class="text-md" translate>Interests will be calculated on a daily basis</p>
+</form>
diff --git a/src/app/loans/products/form/interests/interests.component.ts b/src/app/loans/products/form/interests/interests.component.ts
new file mode 100644
index 0000000..6d75f84
--- /dev/null
+++ b/src/app/loans/products/form/interests/interests.component.ts
@@ -0,0 +1,110 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {Component, Input, OnChanges, SimpleChanges} from '@angular/core';
+import {FormComponent} from '../../../../common/forms/form.component';
+import {FormBuilder, FormControl, ValidatorFn, Validators} from '@angular/forms';
+import {FimsValidators} from '../../../../common/validator/validators';
+import {AccountingService} from '../../../../services/accounting/accounting.service';
+import {accountExists} from '../../../../common/validator/account-exists.validator';
+
+export interface InterestFormData {
+ minimum: string;
+ maximum: string;
+ incomeAccount: string;
+ accrualAccount: string;
+}
+
+@Component({
+ selector: 'fims-product-interests-form',
+ templateUrl: './interests.component.html'
+})
+export class ProductInterestFormComponent extends FormComponent<InterestFormData> implements OnChanges {
+
+ private _formData: InterestFormData;
+
+ private minMaxValidators: ValidatorFn[] = [Validators.required, FimsValidators.minValue(0), FimsValidators.scale(2)];
+
+ @Input() set formData(formData: InterestFormData) {
+ this._formData = formData;
+ };
+
+ constructor(private formBuilder: FormBuilder, private accountingService: AccountingService) {
+ super();
+
+ this.form = this.formBuilder.group({
+ interestRangeEnabled: [false],
+ minimum: ['', this.minMaxValidators],
+ maximum: [''],
+ incomeAccount: ['', [Validators.required], accountExists(this.accountingService)],
+ accrualAccount: ['', [Validators.required], accountExists(this.accountingService)]
+ });
+
+ this.form.get('interestRangeEnabled').valueChanges
+ .subscribe(enabled => this.toggleInterestRange(enabled));
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ const interestRangeEnabled: boolean = this.hasInterestRange(this._formData.minimum, this._formData.maximum);
+
+ this.form.reset({
+ interestRangeEnabled: interestRangeEnabled,
+ minimum: this._formData.minimum,
+ maximum: this._formData.maximum,
+ incomeAccount: this._formData.incomeAccount,
+ accrualAccount: this._formData.accrualAccount
+ });
+ }
+
+ get formData(): InterestFormData {
+ return {
+ minimum: this.form.get('minimum').value,
+ maximum: this.form.get('interestRangeEnabled').value ? this.form.get('maximum').value : this.form.get('minimum').value,
+ incomeAccount: this.form.get('incomeAccount').value,
+ accrualAccount: this.form.get('accrualAccount').value
+ };
+ }
+
+ private toggleInterestRange(enabled: boolean): void {
+ const maximumControl: FormControl = this.form.get('maximum') as FormControl;
+
+ if (enabled) {
+ maximumControl.setValidators(this.minMaxValidators);
+ this.form.setValidators(FimsValidators.greaterThan('minimum', 'maximum'));
+ } else {
+ maximumControl.clearValidators();
+ this.form.clearValidators();
+ }
+
+ maximumControl.updateValueAndValidity();
+ this.form.updateValueAndValidity();
+ }
+
+ private hasInterestRange(min: string, max: string): boolean {
+ return this.hasValue(min) &&
+ this.hasValue(max) &&
+ min !== max;
+ }
+
+ private hasValue(value: string): boolean {
+ return value !== undefined;
+ }
+
+
+}
diff --git a/src/app/loans/products/form/moratorium/moratorium.component.html b/src/app/loans/products/form/moratorium/moratorium.component.html
new file mode 100644
index 0000000..0477b07
--- /dev/null
+++ b/src/app/loans/products/form/moratorium/moratorium.component.html
@@ -0,0 +1,36 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<form [formGroup]="form">
+ <div layout-gt-xs="column" layout-margin formArrayName="moratoriums" flex="30">
+ <div *ngFor="let moratorium of moratoriums; let i=index" layout="row" [formGroupName]="i">
+ <fims-text-input flex="20" type="number" [form]="form" controlName="period" placeholder="{{'Period' | translate}}"></fims-text-input>
+ <mat-form-field layout-margin>
+ <mat-select formControlName="temporalUnit">
+ <mat-option *ngFor="let basis of temporalOptions" [value]="basis.type">
+ {{basis.label}}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ <fims-text-input [form]="moratorium" controlName="chargeTask" placeholder="{{'Fee task' | translate}}"></fims-text-input>
+ <button mat-button mat-raised-button (click)="removeMoratorium(i)">{{'Remove' | translate}}</button>
+ </div>
+ <div layout="row">
+ <button flex mat-button mat-raised-button (click)="addMoratorium()">{{'Add moratorium' | translate}}</button>
+ </div>
+ </div>
+</form>
diff --git a/src/app/loans/products/form/moratorium/moratorium.component.ts b/src/app/loans/products/form/moratorium/moratorium.component.ts
new file mode 100644
index 0000000..54397a5
--- /dev/null
+++ b/src/app/loans/products/form/moratorium/moratorium.component.ts
@@ -0,0 +1,76 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {FormComponent} from '../../../../common/forms/form.component';
+import {Component, Input} from '@angular/core';
+import {Moratorium} from '../../../../services/portfolio/domain/individuallending/moratorium.model';
+import {AbstractControl, FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {temporalOptionList} from '../../../../common/domain/temporal.domain';
+
+@Component({
+ selector: 'fims-product-moratorium-form',
+ templateUrl: './moratorium.component.html'
+})
+export class ProductMoratoriumFormComponent extends FormComponent<Moratorium[]> {
+
+ temporalOptions = temporalOptionList;
+
+ @Input('formData') set formData(moratoriums: Moratorium[]){
+ moratoriums = moratoriums || [];
+ this.form = this.formBuilder.group({
+ moratoriums: this.initMoratoriums(moratoriums),
+ });
+ }
+
+ constructor(private formBuilder: FormBuilder) {
+ super();
+ }
+
+ get formData(): Moratorium[] {
+ return null;
+ }
+
+ private initMoratoriums(moratoriums: Moratorium[]): FormArray {
+ const formControls: FormGroup[] = [];
+ moratoriums.forEach(value => formControls.push(this.initMoratorium(value)));
+ return this.formBuilder.array(formControls);
+ }
+
+ private initMoratorium(moratorium?: Moratorium): FormGroup {
+ return this.formBuilder.group({
+ period: [moratorium ? moratorium.period : '1', Validators.required],
+ chargeTask: [moratorium ? moratorium.chargeTask : '', Validators.required],
+ temporalUnit: [moratorium ? moratorium.temporalUnit : 'WEEKS', Validators.required]
+ });
+ }
+
+ addMoratorium(): void {
+ const moratoriums: FormArray = this.form.get('moratoriums') as FormArray;
+ moratoriums.push(this.initMoratorium());
+ }
+
+ removeMoratorium(index: number): void {
+ const moratoriums: FormArray = this.form.get('moratoriums') as FormArray;
+ moratoriums.removeAt(index);
+ }
+
+ get moratoriums(): AbstractControl[] {
+ const moratoriums: FormArray = this.form.get('moratoriums') as FormArray;
+ return moratoriums.controls;
+ }
+}
diff --git a/src/app/loans/products/form/settings/settings.component.html b/src/app/loans/products/form/settings/settings.component.html
new file mode 100644
index 0000000..afa7983
--- /dev/null
+++ b/src/app/loans/products/form/settings/settings.component.html
@@ -0,0 +1,38 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<form [formGroup]="form">
+ <fims-account-select title="{{'Cash/fund account(Asset accounts only)' | translate}}" formControlName="loanFundAccount" [type]="'ASSET'">
+ <ng-container *ngIf="!form.get('loanFundAccount').pristine && form.get('loanFundAccount').hasError('required')" translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="form.get('loanFundAccount').hasError('invalidAccount')" translate>
+ Invalid account
+ </ng-container>
+ </fims-account-select>
+ <fims-ledger-select title="{{'Member loan ledger(Asset ledgers only)' | translate}}" formControlName="customerLoanLedger" [type]="'ASSET'">
+ <ng-container *ngIf="!form.get('customerLoanLedger').pristine && form.get('customerLoanLedger').hasError('required')" translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="form.get('customerLoanLedger').hasError('invalidLedger')" translate>
+ Invalid ledger
+ </ng-container>
+ </fims-ledger-select>
+ <span layout-margin class="text-md" translate>
+ Ledger used to create member loan account.
+ </span>
+</form>
diff --git a/src/app/loans/products/form/settings/settings.component.ts b/src/app/loans/products/form/settings/settings.component.ts
new file mode 100644
index 0000000..b4edac9
--- /dev/null
+++ b/src/app/loans/products/form/settings/settings.component.ts
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input} from '@angular/core';
+import {FormComponent} from '../../../../common/forms/form.component';
+import {accountExists} from '../../../../common/validator/account-exists.validator';
+import {FormBuilder, Validators} from '@angular/forms';
+import {AccountingService} from '../../../../services/accounting/accounting.service';
+import {ledgerExists} from '../../../../common/validator/ledger-exists.validator';
+
+export interface SettingsFormData {
+ loanFundAccount: string;
+ customerLoanLedger: string;
+}
+
+@Component({
+ selector: 'fims-product-settings-form',
+ templateUrl: './settings.component.html'
+})
+export class ProductSettingsFormComponent extends FormComponent<SettingsFormData> {
+
+ @Input() set formData(settingsFormData: SettingsFormData) {
+ this.form = this.formBuilder.group({
+ loanFundAccount: [settingsFormData.loanFundAccount, [Validators.required], accountExists(this.accountingService)],
+ customerLoanLedger: [settingsFormData.customerLoanLedger, [Validators.required], ledgerExists(this.accountingService)],
+ });
+ }
+
+ constructor(private formBuilder: FormBuilder, private accountingService: AccountingService) {
+ super();
+ }
+
+ get formData(): SettingsFormData {
+ return this.form.getRawValue();
+ }
+
+}
diff --git a/src/app/loans/products/lossProvision/form/create.component.html b/src/app/loans/products/lossProvision/form/create.component.html
new file mode 100644
index 0000000..9c548bb
--- /dev/null
+++ b/src/app/loans/products/lossProvision/form/create.component.html
@@ -0,0 +1,26 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Loss provision configuration' | translate}}" *ngIf="product$ | async as product">
+ <fims-product-loss-provision-form
+ [lossProvisionSteps]="lossProvisionSteps$ | async"
+ (onSave)="save(product.identifier, $event)"
+ (onCancel)="cancel()"
+ [editMode]="true">
+ </fims-product-loss-provision-form>
+</fims-layout-card-over>
+
diff --git a/src/app/loans/products/lossProvision/form/create.component.ts b/src/app/loans/products/lossProvision/form/create.component.ts
new file mode 100644
index 0000000..00452b8
--- /dev/null
+++ b/src/app/loans/products/lossProvision/form/create.component.ts
@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component} from '@angular/core';
+import {LossProvisionStep} from '../../../../services/portfolio/domain/loss-provision-step.model';
+import * as fromPortfolio from '../../store/index';
+import {PortfolioStore} from '../../store/index';
+import {ActivatedRoute, Router} from '@angular/router';
+import {UPDATE} from '../../store/lossProvision/loss-provision.actions';
+import {Observable} from 'rxjs/Observable';
+import {FimsProduct} from '../../store/model/fims-product.model';
+
+@Component({
+ templateUrl: './create.component.html'
+})
+export class CreateProductLossProvisionFormComponent {
+
+ product$: Observable<FimsProduct>;
+
+ lossProvisionSteps$: Observable<LossProvisionStep[]>;
+
+ constructor(private store: PortfolioStore, private router: Router, private route: ActivatedRoute) {
+ this.product$ = store.select(fromPortfolio.getSelectedProduct);
+
+ this.lossProvisionSteps$ = store.select(fromPortfolio.getProductLossProvisionConfiguration)
+ .map(configuration => configuration.lossProvisionSteps);
+ }
+
+ save(productIdentifier: string, lossProvisionSteps: LossProvisionStep[]): void {
+ this.store.dispatch({
+ type: UPDATE,
+ payload: {
+ productIdentifier,
+ configuration: {
+ lossProvisionSteps
+ },
+ activatedRoute: this.route
+ }
+ });
+ }
+
+ cancel(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/loans/products/lossProvision/form/form.component.html b/src/app/loans/products/lossProvision/form/form.component.html
new file mode 100644
index 0000000..76c4072
--- /dev/null
+++ b/src/app/loans/products/lossProvision/form/form.component.html
@@ -0,0 +1,45 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Loss provision' | translate}}" [state]="form.valid ? 'complete' : form.pristine ? 'none' : 'required'" [active]="true">
+ <form [formGroup]="form">
+ <ng-container formArrayName="steps">
+ <div layout="row" *ngFor="let step of steps; let i=index" [formGroupName]="i">
+ <fims-text-input type="number" [form]="step" controlName="daysLate" placeholder="{{'Days late' | translate}}"></fims-text-input>
+ <fims-text-input type="number" [form]="step" controlName="percentProvision" placeholder="{{'Percent provision' | translate}}"></fims-text-input>
+ <button mat-button (click)="removeStep(i)" *ngIf="i > 0">{{'REMOVE STEP' | translate}}</button>
+ </div>
+ </ng-container>
+ <p *ngIf="form.get('steps').hasError('daysLateUnique')" class="tc-red-600" translate>
+ Days late can't overlap with other days late.
+ </p>
+ <div layout="row">
+ <button flex mat-button mat-raised-button (click)="addStep()">{{'ADD STEP' | translate}}</button>
+ </div>
+ </form>
+ <ng-template td-step-actions>
+ <fims-form-final-action
+ [resourceName]="'LOSS PROVISION'"
+ [editMode]="editMode"
+ [disabled]="!form.valid"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/loans/products/lossProvision/form/form.component.ts b/src/app/loans/products/lossProvision/form/form.component.ts
new file mode 100644
index 0000000..797966f
--- /dev/null
+++ b/src/app/loans/products/lossProvision/form/form.component.ts
@@ -0,0 +1,99 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnChanges, Output, SimpleChanges} from '@angular/core';
+import {AbstractControl, FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {FimsValidators} from '../../../../common/validator/validators';
+import {LossProvisionStep} from '../../../../services/portfolio/domain/loss-provision-step.model';
+import {daysLateUnique} from './validator/days-late-unique.validator';
+
+@Component({
+ selector: 'fims-product-loss-provision-form',
+ templateUrl: './form.component.html'
+})
+export class ProductLossProvisionFormComponent implements OnChanges {
+
+ form: FormGroup;
+
+ @Input() lossProvisionSteps: LossProvisionStep[];
+
+ @Input() editMode: boolean;
+
+ @Output('onSave') onSave = new EventEmitter<LossProvisionStep[]>();
+
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder) {
+ this.form = formBuilder.group({
+ steps: this.initSteps([])
+ });
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes.lossProvisionSteps) {
+ this.lossProvisionSteps.forEach(step => this.addStep(step));
+ }
+ }
+
+ private initSteps(steps: LossProvisionStep[]): FormArray {
+ const formControls: FormGroup[] = [];
+
+ steps.forEach(step => {
+ formControls.push(this.initStep(step));
+ });
+
+ return this.formBuilder.array(formControls, daysLateUnique);
+ }
+
+ private initStep(step?: LossProvisionStep): FormGroup {
+ return this.formBuilder.group({
+ daysLate: [step ? step.daysLate : 0, [Validators.required, FimsValidators.minValue(0)]],
+ percentProvision: [step ? step.percentProvision : 0, [Validators.required, FimsValidators.minValue(0), FimsValidators.maxValue(100)]]
+ });
+ }
+
+ addStep(step?: LossProvisionStep): void {
+ const steps: FormArray = this.form.get('steps') as FormArray;
+ steps.push(this.initStep(step));
+ }
+
+ removeStep(index: number): void {
+ const steps: FormArray = this.form.get('steps') as FormArray;
+ steps.removeAt(index);
+ }
+
+ get steps(): AbstractControl[] {
+ const steps: FormArray = this.form.get('steps') as FormArray;
+ return steps.controls;
+ }
+
+ save(): void {
+ const formValue = this.form.getRawValue();
+
+ const steps = formValue.steps.map(step => ({
+ daysLate: parseInt(step.daysLate, 10),
+ percentProvision: parseInt(step.percentProvision, 10)
+ }));
+
+ this.onSave.emit(steps);
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+}
diff --git a/src/app/loans/products/lossProvision/form/validator/days-late-unique.validator.spec.ts b/src/app/loans/products/lossProvision/form/validator/days-late-unique.validator.spec.ts
new file mode 100644
index 0000000..c781388
--- /dev/null
+++ b/src/app/loans/products/lossProvision/form/validator/days-late-unique.validator.spec.ts
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {FormArray, FormControl, FormGroup} from '@angular/forms';
+import {daysLateUnique} from './days-late-unique.validator';
+
+describe('days late unique validator', () => {
+
+ function setup(daysLate: number[]): FormArray {
+ const steps = new FormArray([]);
+ daysLate.forEach(number => steps.push(
+ new FormGroup({
+ daysLate: new FormControl(number),
+ })));
+
+ return steps;
+ }
+
+ it('should not return error when no days late overlap', () => {
+ const group = setup([1, 2, 3]);
+
+ const result = daysLateUnique(group);
+
+ expect(result).toBeNull();
+ });
+
+ it('should return error when days late overlap', () => {
+ const group = setup([1, 2, 2]);
+
+ const result = daysLateUnique(group);
+
+ expect(result).toEqual({
+ daysLateUnique: true
+ });
+ });
+
+});
diff --git a/src/app/loans/products/lossProvision/form/validator/days-late-unique.validator.ts b/src/app/loans/products/lossProvision/form/validator/days-late-unique.validator.ts
new file mode 100644
index 0000000..9f513ed
--- /dev/null
+++ b/src/app/loans/products/lossProvision/form/validator/days-late-unique.validator.ts
@@ -0,0 +1,38 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {FormArray, FormGroup, ValidationErrors} from '@angular/forms';
+
+export function daysLateUnique(array: FormArray): ValidationErrors | null {
+ const steps: FormGroup[] = array.controls as FormGroup[];
+
+ const values = steps
+ .map(optionGroup => parseInt(optionGroup.get('daysLate').value, 10));
+
+ const set = new Set();
+
+ values.forEach(daysLate => set.add(daysLate));
+
+ if (set.size !== values.length) {
+ return {
+ daysLateUnique: true
+ };
+ }
+
+ return null;
+}
diff --git a/src/app/loans/products/lossProvision/loss-provision-exists.guard.ts b/src/app/loans/products/lossProvision/loss-provision-exists.guard.ts
new file mode 100644
index 0000000..0923cb7
--- /dev/null
+++ b/src/app/loans/products/lossProvision/loss-provision-exists.guard.ts
@@ -0,0 +1,63 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import {PortfolioService} from '../../../services/portfolio/portfolio.service';
+import {ExistsGuardService} from '../../../common/guards/exists-guard';
+import * as fromPortfolio from '../store/index';
+import {PortfolioStore} from '../store/index';
+import {Observable} from 'rxjs/Observable';
+import {LoadAction} from '../store/lossProvision/loss-provision.actions';
+import {of} from 'rxjs/observable/of';
+
+@Injectable()
+export class LoanLossProvisionExistsGuard implements CanActivate {
+
+ constructor(private store: PortfolioStore,
+ private portfolioService: PortfolioService,
+ private existsGuardService: ExistsGuardService) {
+ }
+
+ hasConfigurationInStore(): Observable<boolean> {
+ const timestamp$: Observable<number> = this.store.select(fromPortfolio.getProductLossProvisionConfigurationLoadedAt);
+
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ hasConfigurationInApi(id: string): Observable<boolean> {
+ return this.portfolioService.getLossProvisionConfiguration(id)
+ .map(configuration => new LoadAction(configuration))
+ .do((action: LoadAction) => this.store.dispatch(action))
+ .map(catalog => !!catalog);
+ }
+
+ hasConfiguration(id: string): Observable<boolean> {
+ return this.hasConfigurationInStore()
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+ return this.hasConfigurationInApi(id);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasConfiguration(route.parent.params['productId']);
+ }
+}
diff --git a/src/app/loans/products/lossProvision/loss-provision.detail.component.html b/src/app/loans/products/lossProvision/loss-provision.detail.component.html
new file mode 100644
index 0000000..90b2750
--- /dev/null
+++ b/src/app/loans/products/lossProvision/loss-provision.detail.component.html
@@ -0,0 +1,30 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Loss provision configuration' | translate}}" [navigateBackTo]="['../../../../']">
+ <fims-data-table flex
+ [columns]="columns"
+ [data]="stepData$ | async"
+ [actionColumn]="false">
+ </fims-data-table>
+</fims-layout-card-over>
+<fims-fab-button
+ title="{{'Edit configuration' | translate}}"
+ icon="mode_edit"
+ [link]="['edit']"
+ [permission]="{ id: 'portfolio_loss_provision', accessLevel: 'CHANGE' }">
+</fims-fab-button>
diff --git a/src/app/loans/products/lossProvision/loss-provision.detail.component.ts b/src/app/loans/products/lossProvision/loss-provision.detail.component.ts
new file mode 100644
index 0000000..e9d3981
--- /dev/null
+++ b/src/app/loans/products/lossProvision/loss-provision.detail.component.ts
@@ -0,0 +1,46 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component} from '@angular/core';
+import * as fromPortfolio from '../store/index';
+import {PortfolioStore} from '../store/index';
+import {Observable} from 'rxjs/Observable';
+import {TableData} from '../../../common/data-table/data-table.component';
+
+@Component({
+ templateUrl: './loss-provision.detail.component.html'
+})
+export class LossProvisionDetailComponent {
+
+ stepData$: Observable<TableData>;
+
+ columns: any[] = [
+ { name: 'daysLate', label: 'Days late' },
+ { name: 'percentProvision', label: 'Percent', format: v => `${v}%`}
+ ];
+
+ constructor(private store: PortfolioStore) {
+ this.stepData$ = store.select(fromPortfolio.getProductLossProvisionConfiguration)
+ .map(configuration => ({
+ data: configuration.lossProvisionSteps,
+ totalElements: configuration.lossProvisionSteps.length,
+ totalPages: 1
+ }));
+ }
+
+}
diff --git a/src/app/loans/products/product-exists.guard.ts b/src/app/loans/products/product-exists.guard.ts
new file mode 100644
index 0000000..2740a49
--- /dev/null
+++ b/src/app/loans/products/product-exists.guard.ts
@@ -0,0 +1,69 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import {Injectable} from '@angular/core';
+import * as fromProducts from './store';
+import {Observable} from 'rxjs/Observable';
+import {LoadAction} from './store/product.actions';
+import {of} from 'rxjs/observable/of';
+import {PortfolioStore} from './store/index';
+import {PortfolioService} from '../../services/portfolio/portfolio.service';
+import {mapToFimsProduct} from './store/model/fims-product.mapper';
+import {ExistsGuardService} from '../../common/guards/exists-guard';
+
+@Injectable()
+export class ProductExistsGuard implements CanActivate {
+
+ constructor(private store: PortfolioStore,
+ private portfolioService: PortfolioService,
+ private existsGuardService: ExistsGuardService) {}
+
+ hasProductInStore(id: string): Observable<boolean> {
+ const timestamp$ = this.store.select(fromProducts.getProductsLoadedAt)
+ .map(loadedAt => loadedAt[id]);
+
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ hasProductInApi(id: string): Observable<boolean> {
+ const getProduct = this.portfolioService.getProduct(id)
+ .map(productEntity => new LoadAction({
+ resource: mapToFimsProduct(productEntity)
+ }))
+ .do((action: LoadAction) => this.store.dispatch(action))
+ .map(product => !!product);
+
+ return this.existsGuardService.routeTo404OnError(getProduct);
+ }
+
+ hasProduct(id: string): Observable<boolean> {
+ return this.hasProductInStore(id)
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+
+ return this.hasProductInApi(id);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasProduct(route.params['productId']);
+ }
+}
diff --git a/src/app/loans/products/product.detail.component.html b/src/app/loans/products/product.detail.component.html
new file mode 100644
index 0000000..8e9f86a
--- /dev/null
+++ b/src/app/loans/products/product.detail.component.html
@@ -0,0 +1,75 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over [title]="product.name" [subTitle]="product.description" [navigateBackTo]="['../../../']">
+ <fims-layout-card-over-header-menu>
+ <button mat-icon-button (click)="deleteProduct()" title="{{'Delete this product' | translate}}" *ngIf="canDelete$ | async"><mat-icon>delete</mat-icon></button>
+ </fims-layout-card-over-header-menu>
+ <td-message *ngIf="!product.enabled" label="{{'Product not enabled' | translate }}" sublabel="{{'To assign this product to a member it needs to be enabled first' | translate }}" color="warn" icon="error">
+ <button td-message-actions mat-button (click)="enableProduct()" *hasPermission="{ id: 'portfolio_product_operations', accessLevel: 'CHANGE'}" translate>ENABLE PRODUCT</button>
+ </td-message>
+ <td-message *ngIf="product.enabled" label="{{'Product enabled' | translate }}" sublabel="{{'This product can be assigned to a member ' | translate }}" color="accent" icon="check">
+ <button td-message-actions mat-button (click)="disableProduct()" *hasPermission="{ id: 'portfolio_product_operations', accessLevel: 'CHANGE'}" translate>DISABLE PRODUCT</button>
+ </td-message>
+ <fims-two-column-layout>
+ <mat-nav-list left>
+ <h3 mat-subheader translate>Management</h3>
+ <a mat-list-item [routerLink]="['./charges']">
+ <mat-icon matListAvatar>assignment</mat-icon>
+ <h3 matLine translate>Fees</h3>
+ <p matLine translate>Manage fees</p>
+ </a>
+ <a mat-list-item [routerLink]="['./tasks']">
+ <mat-icon matListAvatar>playlist_add_check</mat-icon>
+ <h3 matLine translate>Tasks</h3>
+ <p matLine translate>Manage tasks of this product</p>
+ </a>
+ <a mat-list-item [routerLink]="['./lossProvision']" *hasPermission="{ id: 'portfolio_loss_provision', accessLevel: 'READ' }">
+ <mat-icon matListAvatar>trending_down</mat-icon>
+ <h3 matLine translate>Loss provision configuration</h3>
+ </a>
+ </mat-nav-list>
+ <mat-list right>
+ <mat-list-item>
+ <h3 matLine translate>Balance range</h3>
+ <p matLine>{{product.balanceRange?.minimum | number:numberFormat}} - {{product.balanceRange?.maximum | number:numberFormat}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Interest range</h3>
+ <p matLine>{{product.interestBasis | translate}}</p>
+ <p matLine>{{product.interestRange?.minimum | number:numberFormat}} - {{product.interestRange?.maximum | number:numberFormat}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Term</h3>
+ <p matLine>{{product.termRange?.maximum | number}} {{product.termRange?.temporalUnit | translate}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Currency</h3>
+ <p matLine>{{product.currencyCode}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Created by</h3>
+ <p matLine>{{product.createdBy}} - {{product.createdOn | date:'medium'}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Last modified by</h3>
+ <p matLine>{{product.lastModifiedBy}} - {{product.lastModifiedOn | date:'medium'}}</p>
+ </mat-list-item>
+ </mat-list>
+ </fims-two-column-layout>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Edit product' | translate}}" icon="mode_edit" [link]="['edit']" *ngIf="canEdit$ | async"></fims-fab-button>
diff --git a/src/app/loans/products/product.detail.component.spec.ts b/src/app/loans/products/product.detail.component.spec.ts
new file mode 100644
index 0000000..9f8746e
--- /dev/null
+++ b/src/app/loans/products/product.detail.component.spec.ts
@@ -0,0 +1,144 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {ProductDetailComponent} from './product.detail.component';
+import {ActivatedRouteStub, RouterLinkStubDirective} from '../../common/testing/router-stubs';
+import {TranslateModule} from '@ngx-translate/core';
+import {ActivatedRoute} from '@angular/router';
+import {PortfolioStore} from './store/index';
+import {CUSTOM_ELEMENTS_SCHEMA, DebugElement} from '@angular/core';
+import {TdDialogService} from '@covalent/core';
+import {FimsProduct} from './store/model/fims-product.model';
+import {FimsPermission} from '../../services/security/authz/fims-permission.model';
+import * as fromPortfolio from './store';
+import * as fromRoot from '../../store';
+import {Observable} from 'rxjs/Observable';
+import {By} from '@angular/platform-browser';
+import {FimsPermissionStubDirective} from '../../common/testing/permission-stubs';
+import {MatDialogModule} from '@angular/material';
+
+describe('Test product list component', () => {
+
+ let component: ProductDetailComponent;
+ let fixture: ComponentFixture<ProductDetailComponent>;
+
+ beforeEach(() => {
+ const activatedRoute = new ActivatedRouteStub();
+
+ TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot(),
+ MatDialogModule
+ ],
+ declarations: [
+ FimsPermissionStubDirective,
+ RouterLinkStubDirective,
+ ProductDetailComponent
+ ],
+ providers: [
+ TdDialogService,
+ { provide: ActivatedRoute, useValue: activatedRoute },
+ { provide: PortfolioStore, useValue: jasmine.createSpyObj('portfolioStore', ['select', 'dispatch']) }
+ ],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA]
+ });
+
+ fixture = TestBed.createComponent(ProductDetailComponent);
+
+ component = fixture.componentInstance;
+ });
+
+ function setup(enabled: boolean, hasChangePermission: boolean) {
+ const product: FimsProduct = {
+ identifier: 'test',
+ name: 'test',
+ termRange: { temporalUnit: 'MONTHS', maximum: 1 },
+ balanceRange: { minimum: 1, maximum: 2 },
+ interestRange: { minimum: 1, maximum: 2 },
+ interestBasis: 'BEGINNING_BALANCE',
+ patternPackage: 'test',
+ description: '',
+ accountAssignments: [],
+ parameters: {
+ moratoriums: [],
+ minimumDispersalAmount: 0,
+ maximumDispersalAmount: 1,
+ maximumDispersalCount: 1
+ },
+ currencyCode: 'USD',
+ minorCurrencyUnitDigits: 1,
+ enabled
+ };
+
+ const permissions: FimsPermission[] = [];
+
+ if (hasChangePermission) {
+ permissions.push({
+ id: 'portfolio_products',
+ accessLevel: 'CHANGE'
+ });
+ }
+
+ const portfolioStore = TestBed.get(PortfolioStore);
+
+ portfolioStore.select.and.callFake(selector => {
+ if (selector === fromPortfolio.getSelectedProduct) {
+ return Observable.of(product);
+ }
+ if (selector === fromRoot.getPermissions) {
+ return Observable.of(permissions);
+ }
+ });
+ }
+
+ function getCreateButton(): DebugElement {
+ return fixture.debugElement.query(By.css('fims-fab-button'));
+ }
+
+ it('should display edit button when product is not enabled and has change permission', () => {
+ setup(false, true);
+
+ fixture.detectChanges();
+
+ const button = getCreateButton();
+
+ expect(button).not.toBeNull();
+ });
+
+ it('should not display edit button when product is enabled and has no change permission', () => {
+ setup(true, false);
+
+ fixture.detectChanges();
+
+ const button = getCreateButton();
+
+ expect(button).toBeNull();
+ });
+
+ it('should not display edit button when product is enabled and has change permission', () => {
+ setup(true, true);
+
+ fixture.detectChanges();
+
+ const button = getCreateButton();
+
+ expect(button).toBeNull();
+ });
+
+});
diff --git a/src/app/loans/products/product.detail.component.ts b/src/app/loans/products/product.detail.component.ts
new file mode 100644
index 0000000..d87298a
--- /dev/null
+++ b/src/app/loans/products/product.detail.component.ts
@@ -0,0 +1,133 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {PortfolioStore} from './store/index';
+import {DELETE, ENABLE} from './store/product.actions';
+import {Subscription} from 'rxjs/Subscription';
+import * as fromPortfolio from './store';
+import * as fromRoot from '../../store';
+import {FimsProduct} from './store/model/fims-product.model';
+import {FimsPermission} from '../../services/security/authz/fims-permission.model';
+import {Observable} from 'rxjs/Observable';
+import {TdDialogService} from '@covalent/core';
+
+@Component({
+ templateUrl: './product.detail.component.html'
+})
+export class ProductDetailComponent implements OnInit, OnDestroy {
+
+ private productSubscription: Subscription;
+
+ product: FimsProduct;
+
+ canEdit$: Observable<boolean>;
+
+ canDelete$: Observable<boolean>;
+
+ constructor(private route: ActivatedRoute, private portfolioStore: PortfolioStore, private dialogService: TdDialogService) {}
+
+ ngOnInit(): void {
+ const product$: Observable<FimsProduct> = this.portfolioStore.select(fromPortfolio.getSelectedProduct)
+ .filter(product => !!product);
+
+ this.productSubscription = product$
+ .subscribe(product => this.product = product);
+
+ const permissions$ = this.portfolioStore.select(fromRoot.getPermissions);
+
+ this.canEdit$ = Observable.combineLatest(
+ permissions$,
+ product$,
+ (permissions, product) => ({
+ hasPermission: this.hasChangePermission(permissions),
+ isEnabled: product.enabled
+ }))
+ .map(result => result.hasPermission && !result.isEnabled);
+
+ this.canDelete$ = Observable.combineLatest(
+ permissions$,
+ product$,
+ (permissions, product) => ({
+ hasPermission: this.hasDeletePermission(permissions),
+ isEnabled: product.enabled
+ }))
+ .map(result => result.hasPermission && !result.isEnabled);
+ }
+
+ ngOnDestroy(): void {
+ this.productSubscription.unsubscribe();
+ }
+
+ enableProduct(): void {
+ this.portfolioStore.dispatch({ type: ENABLE, payload: {
+ product: this.product,
+ enable: true
+ } });
+ }
+
+ disableProduct(): void {
+ this.portfolioStore.dispatch({ type: ENABLE, payload: {
+ product: this.product,
+ enable: false
+ } });
+ }
+
+ confirmDeletion(): Observable<boolean> {
+ return this.dialogService.openConfirm({
+ message: 'Do you want to delete this product?',
+ title: 'Confirm deletion',
+ acceptButton: 'DELETE PRODUCT',
+ }).afterClosed();
+ }
+
+ deleteProduct(): void {
+ this.confirmDeletion()
+ .filter(accept => accept)
+ .subscribe(() => this.portfolioStore.dispatch({
+ type: DELETE, payload: {
+ product: this.product,
+ activatedRoute: this.route
+ }
+ }));
+ }
+
+ get numberFormat(): string {
+ let digits = 2;
+ if (this.product) {
+ digits = this.product.minorCurrencyUnitDigits;
+ }
+ return `1.${digits}-${digits}`;
+ }
+
+ private hasChangePermission(permissions: FimsPermission[]): boolean {
+ return permissions.filter(permission =>
+ permission.id === 'portfolio_products' &&
+ permission.accessLevel === 'CHANGE'
+ ).length > 0;
+ }
+
+ private hasDeletePermission(permissions: FimsPermission[]): boolean {
+ return permissions.filter(permission =>
+ permission.id === 'portfolio_products' &&
+ permission.accessLevel === 'DELETE'
+ ).length > 0;
+ }
+
+}
diff --git a/src/app/loans/products/product.index.component.html b/src/app/loans/products/product.index.component.html
new file mode 100644
index 0000000..ca721b3
--- /dev/null
+++ b/src/app/loans/products/product.index.component.html
@@ -0,0 +1,18 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<router-outlet></router-outlet>
diff --git a/src/app/loans/products/product.index.component.ts b/src/app/loans/products/product.index.component.ts
new file mode 100644
index 0000000..787159b
--- /dev/null
+++ b/src/app/loans/products/product.index.component.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {Subscription} from 'rxjs/Subscription';
+import {ActivatedRoute} from '@angular/router';
+import {PortfolioStore} from './store/index';
+import {SelectAction} from './store/product.actions';
+
+@Component({
+ templateUrl: './product.index.component.html'
+})
+export class ProductIndexComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ constructor(private route: ActivatedRoute, private store: PortfolioStore) {}
+
+ ngOnInit(): void {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectAction(params['productId']))
+ .subscribe(this.store);
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ }
+}
diff --git a/src/app/loans/products/product.list.component.html b/src/app/loans/products/product.list.component.html
new file mode 100644
index 0000000..496ad8e
--- /dev/null
+++ b/src/app/loans/products/product.list.component.html
@@ -0,0 +1,28 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Manage loan products' | translate}}">
+ <fims-data-table flex
+ (onFetch)="fetchProducts($event)"
+ (onActionCellClick)="rowSelect($event)"
+ [columns]="columns"
+ [sortable]="true"
+ [pageable]="true"
+ [data]="productData | async">
+ </fims-data-table>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Create new product' | translate}}" icon="add" [link]="['create']" [permission]="{ id: 'portfolio_products', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/loans/products/product.list.component.ts b/src/app/loans/products/product.list.component.ts
new file mode 100644
index 0000000..3fa1314
--- /dev/null
+++ b/src/app/loans/products/product.list.component.ts
@@ -0,0 +1,62 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {Component, OnInit} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {TableData} from '../../common/data-table/data-table.component';
+import {FetchRequest} from '../../services/domain/paging/fetch-request.model';
+import {PortfolioStore} from './store/index';
+import * as fromPortfolio from './store';
+import {Observable} from 'rxjs/Observable';
+import {SEARCH} from './store/product.actions';
+import {FimsProduct} from './store/model/fims-product.model';
+
+@Component({
+ templateUrl: './product.list.component.html'
+})
+export class ProductListComponent implements OnInit {
+
+ productData: Observable<TableData>;
+
+ columns: any[] = [
+ { name: 'identifier', label: 'Id' },
+ { name: 'name', label: 'Name' },
+ { name: 'enabled', label: 'Enabled'}
+ ];
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: PortfolioStore) {}
+
+ ngOnInit(): void {
+ this.productData = this.store.select(fromPortfolio.getProductSearchResults)
+ .map(productPage => ({
+ data: productPage.products,
+ totalElements: productPage.totalElements,
+ totalPages: productPage.totalPages
+ }));
+ this.fetchProducts();
+ }
+
+ fetchProducts(fetchRequest?: FetchRequest): void {
+ this.store.dispatch({ type: SEARCH, payload: fetchRequest });
+ }
+
+ rowSelect(product: FimsProduct): void {
+ this.router.navigate(['detail', product.identifier], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/loans/products/product.module.ts b/src/app/loans/products/product.module.ts
new file mode 100644
index 0000000..88a1822
--- /dev/null
+++ b/src/app/loans/products/product.module.ts
@@ -0,0 +1,187 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {NgModule} from '@angular/core';
+import {FimsSharedModule} from '../../common/common.module';
+import {ProductRoutes} from './product.routes';
+import {RouterModule} from '@angular/router';
+import {ProductListComponent} from './product.list.component';
+import {ProductCreateComponent} from './form/create.component';
+import {ProductDetailComponent} from './product.detail.component';
+import {ProductEditComponent} from './form/edit.component';
+import {ProductFormComponent} from './form/form.component';
+import {ProductFeeFormComponent} from './form/fees/fee.component';
+import {ProductInterestFormComponent} from './form/interests/interests.component';
+import {ProductTermFormComponent} from './components/term/term.component';
+import {ProductChargeListComponent} from './charges/charge.list.component';
+import {ProductChargeDetailComponent} from './charges/charge.detail.component';
+import {ProductChargeFormComponent} from './charges/form/form.component';
+import {ProductChargeCreateFormComponent} from './charges/form/create.component';
+import {ProductStatusComponent} from './status/status.component';
+import {ProductStatusCreateFormComponent} from './status/form/create.component';
+import {ProductTaskFormComponent} from './status/form/form.component';
+import {ProductStatusEditFormComponent} from './status/form/edit.component';
+import {ProductStatusDetailComponent} from './status/status.detail.component';
+import {ProductMoratoriumFormComponent} from './form/moratorium/moratorium.component';
+import {ProductChargeEditFormComponent} from './charges/form/edit.component';
+import {Store} from '@ngrx/store';
+import {PortfolioStore, portfolioStoreFactory} from './store/index';
+import {ProductExistsGuard} from './product-exists.guard';
+import {ProductTaskExistsGuard} from './status/task-exists.guard';
+import {ProductChargeExistsGuard} from './charges/charge-exists.guard';
+import {ProductSettingsFormComponent} from './form/settings/settings.component';
+import {ProductChargesNotificationEffects} from './store/charges/effects/notification.effects';
+import {ProductChargesRouteEffects} from './store/charges/effects/route.effects';
+import {EffectsModule} from '@ngrx/effects';
+import {ProductChargesApiEffects} from './store/charges/effects/service.effects';
+import {ProductTasksNotificationEffects} from './store/tasks/effects/notification.effects';
+import {ProductTasksRouteEffects} from './store/tasks/effects/route.effects';
+import {ProductTasksApiEffects} from './store/tasks/effects/service.effects';
+import {ProductNotificationEffects} from './store/effects/notification.effects';
+import {ProductRouteEffects} from './store/effects/route.effects';
+import {ProductApiEffects} from './store/effects/service.effects';
+import {TranslateModule} from '@ngx-translate/core';
+import {CommonModule} from '@angular/common';
+import {ReactiveFormsModule} from '@angular/forms';
+import {
+ MatButtonModule,
+ MatCheckboxModule,
+ MatIconModule,
+ MatInputModule,
+ MatListModule,
+ MatOptionModule,
+ MatRadioModule,
+ MatSelectModule,
+ MatSlideToggleModule,
+ MatToolbarModule
+} from '@angular/material';
+import {CovalentDataTableModule, CovalentMessageModule, CovalentStepsModule} from '@covalent/core';
+import {ProductIndexComponent} from './product.index.component';
+import {ProductDetailFormComponent} from './form/detail/detail.component';
+import {ProductChargeRangeListComponent} from './charges/ranges/range.list.component';
+import {ProductChargeRangeDetailComponent} from './charges/ranges/range.detail.component';
+import {ProductChargeRangeFormComponent} from './charges/ranges/form/form.component';
+import {EditProductChargeRangeFormComponent} from './charges/ranges/form/edit.component';
+import {CreateProductChargeRangeFormComponent} from './charges/ranges/form/create.component';
+import {ProductChargeRangesRouteEffects} from './store/ranges/effects/route.effects';
+import {ProductChargeRangesApiEffects} from './store/ranges/effects/service.effects';
+import {ProductChargeRangeExistsGuard} from './charges/ranges/range-exists.guard';
+import {ProductChargeRangeIndexComponent} from './charges/ranges/range.index.component';
+import {ProductChargeRangesNotificationEffects} from './store/ranges/effects/notification.effects';
+import {ProductLossProvisionApiEffects} from './store/lossProvision/effects/service.effects';
+import {ProductLossProvisionRouteEffects} from './store/lossProvision/effects/route.effects';
+import {ProductLossProvisionNotificationEffects} from './store/lossProvision/effects/notification.effects';
+import {LoanLossProvisionExistsGuard} from './lossProvision/loss-provision-exists.guard';
+import {CreateProductLossProvisionFormComponent} from './lossProvision/form/create.component';
+import {ProductLossProvisionFormComponent} from './lossProvision/form/form.component';
+import {LossProvisionDetailComponent} from './lossProvision/loss-provision.detail.component';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(ProductRoutes),
+ FimsSharedModule,
+ TranslateModule,
+ CommonModule,
+ ReactiveFormsModule,
+ MatIconModule,
+ MatListModule,
+ MatToolbarModule,
+ MatInputModule,
+ MatButtonModule,
+ MatSlideToggleModule,
+ MatRadioModule,
+ MatOptionModule,
+ MatSelectModule,
+ MatCheckboxModule,
+ CovalentDataTableModule,
+ CovalentStepsModule,
+ CovalentMessageModule,
+
+ EffectsModule.run(ProductApiEffects),
+ EffectsModule.run(ProductRouteEffects),
+ EffectsModule.run(ProductNotificationEffects),
+
+ EffectsModule.run(ProductTasksApiEffects),
+ EffectsModule.run(ProductTasksRouteEffects),
+ EffectsModule.run(ProductTasksNotificationEffects),
+
+ EffectsModule.run(ProductChargesApiEffects),
+ EffectsModule.run(ProductChargesRouteEffects),
+ EffectsModule.run(ProductChargesNotificationEffects),
+
+ EffectsModule.run(ProductChargeRangesApiEffects),
+ EffectsModule.run(ProductChargeRangesRouteEffects),
+ EffectsModule.run(ProductChargeRangesNotificationEffects),
+
+ EffectsModule.run(ProductLossProvisionApiEffects),
+ EffectsModule.run(ProductLossProvisionRouteEffects),
+ EffectsModule.run(ProductLossProvisionNotificationEffects),
+ ],
+ declarations: [
+ // product
+ ProductListComponent,
+ ProductIndexComponent,
+ ProductDetailComponent,
+ ProductFormComponent,
+ ProductCreateComponent,
+ ProductEditComponent,
+ ProductDetailFormComponent,
+ ProductFeeFormComponent,
+ ProductInterestFormComponent,
+ ProductTermFormComponent,
+ ProductMoratoriumFormComponent,
+ ProductSettingsFormComponent,
+
+ // charge
+ ProductChargeListComponent,
+ ProductChargeDetailComponent,
+ ProductChargeFormComponent,
+ ProductChargeCreateFormComponent,
+ ProductChargeEditFormComponent,
+
+ // ranges
+ ProductChargeRangeListComponent,
+ ProductChargeRangeIndexComponent,
+ ProductChargeRangeDetailComponent,
+ ProductChargeRangeFormComponent,
+ CreateProductChargeRangeFormComponent,
+ EditProductChargeRangeFormComponent,
+
+ // status
+ ProductStatusComponent,
+ ProductTaskFormComponent,
+ ProductStatusCreateFormComponent,
+ ProductStatusEditFormComponent,
+ ProductStatusDetailComponent,
+
+ // Loss provision
+ LossProvisionDetailComponent,
+ ProductLossProvisionFormComponent,
+ CreateProductLossProvisionFormComponent,
+ ProductChargeDetailComponent
+ ],
+ providers: [
+ ProductExistsGuard,
+ ProductTaskExistsGuard,
+ ProductChargeExistsGuard,
+ ProductChargeRangeExistsGuard,
+ LoanLossProvisionExistsGuard,
+ { provide: PortfolioStore, useFactory: portfolioStoreFactory, deps: [Store]}
+ ]
+})
+export class ProductModule {}
diff --git a/src/app/loans/products/product.routes.ts b/src/app/loans/products/product.routes.ts
new file mode 100644
index 0000000..2db26ba
--- /dev/null
+++ b/src/app/loans/products/product.routes.ts
@@ -0,0 +1,151 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Routes} from '@angular/router';
+import {ProductListComponent} from './product.list.component';
+import {ProductCreateComponent} from './form/create.component';
+import {ProductDetailComponent} from './product.detail.component';
+import {ProductEditComponent} from './form/edit.component';
+import {ProductChargeListComponent} from './charges/charge.list.component';
+import {ProductChargeDetailComponent} from './charges/charge.detail.component';
+import {ProductChargeCreateFormComponent} from './charges/form/create.component';
+import {ProductStatusComponent} from './status/status.component';
+import {ProductStatusCreateFormComponent} from './status/form/create.component';
+import {ProductStatusEditFormComponent} from './status/form/edit.component';
+import {ProductStatusDetailComponent} from './status/status.detail.component';
+import {ProductChargeEditFormComponent} from './charges/form/edit.component';
+import {ProductExistsGuard} from './product-exists.guard';
+import {ProductTaskExistsGuard} from './status/task-exists.guard';
+import {ProductChargeExistsGuard} from './charges/charge-exists.guard';
+import {ProductIndexComponent} from './product.index.component';
+import {ProductChargeRangeListComponent} from './charges/ranges/range.list.component';
+import {CreateProductChargeRangeFormComponent} from './charges/ranges/form/create.component';
+import {EditProductChargeRangeFormComponent} from './charges/ranges/form/edit.component';
+import {ProductChargeRangeExistsGuard} from './charges/ranges/range-exists.guard';
+import {ProductChargeRangeIndexComponent} from './charges/ranges/range.index.component';
+import {ProductChargeRangeDetailComponent} from './charges/ranges/range.detail.component';
+import {LoanLossProvisionExistsGuard} from './lossProvision/loss-provision-exists.guard';
+import {CreateProductLossProvisionFormComponent} from './lossProvision/form/create.component';
+import {LossProvisionDetailComponent} from './lossProvision/loss-provision.detail.component';
+
+export const ProductRoutes: Routes = [
+ {path: '', component: ProductListComponent, data: {hasPermission: {id: 'portfolio_products', accessLevel: 'READ'}} /* List */},
+ {
+ path: 'create',
+ component: ProductCreateComponent,
+ data: {hasPermission: {id: 'portfolio_products', accessLevel: 'CHANGE'}} /* Create */
+ },
+ {
+ path: 'detail/:productId', /* Parent view to resolve product */
+ component: ProductIndexComponent,
+ canActivate: [ProductExistsGuard],
+ children: [
+ {
+ path: '',
+ component: ProductDetailComponent /* Detail */
+ },
+ {
+ path: 'edit',
+ component: ProductEditComponent,
+ data: { hasPermission: { id: 'portfolio_products', accessLevel: 'CHANGE' } }
+ },
+ {
+ path: 'charges',
+ component: ProductChargeListComponent /* Charges list view */
+ },
+ {
+ path: 'charges/ranges',
+ children: [
+ {
+ path: '',
+ component: ProductChargeRangeListComponent
+ },
+ {
+ path: 'create',
+ component: CreateProductChargeRangeFormComponent
+ },
+ {
+ path: 'detail/:rangeId',
+ component: ProductChargeRangeIndexComponent,
+ canActivate: [ProductChargeRangeExistsGuard],
+ children: [
+ {
+ path: '',
+ component: ProductChargeRangeDetailComponent
+ },
+ {
+ path: 'edit',
+ component: EditProductChargeRangeFormComponent
+ }
+ ]
+ }
+ ]
+ },
+ {
+ path: 'charges/create',
+ component: ProductChargeCreateFormComponent,
+ data: { hasPermission: { id: 'portfolio_products', accessLevel: 'CHANGE' } } /* Charges create view */},
+ {
+ path: 'charges/detail/:chargeId',
+ component: ProductChargeDetailComponent,
+ canActivate: [ProductChargeExistsGuard]/* Charges detail view */
+ },
+ {
+ path: 'charges/detail/:chargeId/edit',
+ component: ProductChargeEditFormComponent,
+ canActivate: [ProductChargeExistsGuard],
+ data: { hasPermission: { id: 'portfolio_products', accessLevel: 'CHANGE' } }/* Charges detail view */
+ },
+ {
+ path: 'tasks',
+ component: ProductStatusComponent
+ },
+ {
+ path: 'tasks/create', component: ProductStatusCreateFormComponent,
+ data: { hasPermission: { id: 'portfolio_products', accessLevel: 'CHANGE' } }
+ },
+ {
+ path: 'tasks/detail/:taskId',
+ component: ProductStatusDetailComponent,
+ canActivate: [ProductTaskExistsGuard]
+ },
+ {
+ path: 'tasks/detail/:taskId/edit',
+ component: ProductStatusEditFormComponent,
+ canActivate: [ProductTaskExistsGuard],
+ data: { hasPermission: { id: 'portfolio_products', accessLevel: 'CHANGE' } }
+ },
+ {
+ path: 'lossProvision',
+ canActivate: [LoanLossProvisionExistsGuard],
+ data: { hasPermission: { id: 'portfolio_loss_provision', accessLevel: 'READ' } },
+ children: [
+ {
+ path: '',
+ component: LossProvisionDetailComponent
+ },
+ {
+ path: 'edit',
+ component: CreateProductLossProvisionFormComponent,
+ data: { hasPermission: { id: 'portfolio_loss_provision', accessLevel: 'CHANGE' } },
+ }
+ ]
+ }
+ ]
+ }
+];
diff --git a/src/app/loans/products/status/form/create.component.html b/src/app/loans/products/status/form/create.component.html
new file mode 100644
index 0000000..8764bf7
--- /dev/null
+++ b/src/app/loans/products/status/form/create.component.html
@@ -0,0 +1,20 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Add new task definition' | translate}}">
+ <fims-product-task-form-component #form (onSave)="onSave($event)" (onCancel)="onCancel()" [task]="task"></fims-product-task-form-component>
+</fims-layout-card-over>
diff --git a/src/app/loans/products/status/form/create.component.ts b/src/app/loans/products/status/form/create.component.ts
new file mode 100644
index 0000000..b2a7a5a
--- /dev/null
+++ b/src/app/loans/products/status/form/create.component.ts
@@ -0,0 +1,91 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {TaskDefinition} from '../../../../services/portfolio/domain/task-definition.model';
+import {ProductTaskFormComponent} from './form.component';
+import {Subscription} from 'rxjs/Subscription';
+import {PortfolioStore} from '../../store/index';
+import * as fromPortfolio from '../../store';
+import {CREATE, RESET_FORM} from '../../store/tasks/task.actions';
+import {Error} from '../../../../services/domain/error.model';
+import {FimsProduct} from '../../store/model/fims-product.model';
+
+@Component({
+ templateUrl: './create.component.html'
+})
+export class ProductStatusCreateFormComponent implements OnInit, OnDestroy {
+
+ private productSubscription: Subscription;
+
+ private formStateSubscription: Subscription;
+
+ @ViewChild('form') formComponent: ProductTaskFormComponent;
+
+ private product: FimsProduct;
+
+ task: TaskDefinition = {
+ identifier: '',
+ name: '',
+ description: '',
+ actions: ['OPEN'],
+ fourEyes: false,
+ mandatory: false
+ };
+
+ constructor(private router: Router, private route: ActivatedRoute, private portfolioStore: PortfolioStore) {}
+
+ ngOnInit(): void {
+ this.productSubscription = this.portfolioStore.select(fromPortfolio.getSelectedProduct)
+ .subscribe(product => this.product = product);
+
+ this.formStateSubscription = this.portfolioStore.select(fromPortfolio.getProductTaskFormError)
+ .filter((error: Error) => !!error)
+ .subscribe((error: Error) => {
+ const detailForm = this.formComponent.detailForm;
+ const errors = detailForm.get('identifier').errors || {};
+ errors['unique'] = true;
+ detailForm.get('identifier').setErrors(errors);
+ this.formComponent.openDetailsStep();
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.productSubscription.unsubscribe();
+ this.formStateSubscription.unsubscribe();
+
+ this.portfolioStore.dispatch({ type: RESET_FORM });
+ }
+
+ onSave(task: TaskDefinition): void {
+ this.portfolioStore.dispatch({ type: CREATE, payload: {
+ productId: this.product.identifier,
+ task: task,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel(): void {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/loans/products/status/form/edit.component.html b/src/app/loans/products/status/form/edit.component.html
new file mode 100644
index 0000000..add688f
--- /dev/null
+++ b/src/app/loans/products/status/form/edit.component.html
@@ -0,0 +1,20 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Edit task definition' | translate}}">
+ <fims-product-task-form-component (onSave)="onSave($event)" (onCancel)="onCancel()" [task]="task" [editMode]="true"></fims-product-task-form-component>
+</fims-layout-card-over>
diff --git a/src/app/loans/products/status/form/edit.component.ts b/src/app/loans/products/status/form/edit.component.ts
new file mode 100644
index 0000000..2a81e5d
--- /dev/null
+++ b/src/app/loans/products/status/form/edit.component.ts
@@ -0,0 +1,71 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {TaskDefinition} from '../../../../services/portfolio/domain/task-definition.model';
+import {ActivatedRoute, Router} from '@angular/router';
+import {PortfolioStore} from '../../store/index';
+import {Subscription} from 'rxjs/Subscription';
+import * as fromPortfolio from '../../store';
+import {UPDATE} from '../../store/tasks/task.actions';
+import {FimsProduct} from '../../store/model/fims-product.model';
+
+@Component({
+ templateUrl: './edit.component.html'
+})
+export class ProductStatusEditFormComponent implements OnInit, OnDestroy {
+
+ private taskSubscription: Subscription;
+
+ private productSubscription: Subscription;
+
+ private product: FimsProduct;
+
+ task: TaskDefinition;
+
+ constructor(private router: Router, private route: ActivatedRoute, private portfolioStore: PortfolioStore) {}
+
+ ngOnInit(): void {
+ this.taskSubscription = this.portfolioStore.select(fromPortfolio.getSelectedProductTask)
+ .subscribe(task => this.task = task);
+
+ this.productSubscription = this.portfolioStore.select(fromPortfolio.getSelectedProduct)
+ .subscribe(product => this.product = product);
+ }
+
+ ngOnDestroy(): void {
+ this.taskSubscription.unsubscribe();
+ this.productSubscription.unsubscribe();
+ }
+
+ onSave(task: TaskDefinition): void {
+ this.portfolioStore.dispatch({ type: UPDATE, payload: {
+ productId: this.product.identifier,
+ task: task,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel(): void {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/loans/products/status/form/form.component.html b/src/app/loans/products/status/form/form.component.html
new file mode 100644
index 0000000..5012c50
--- /dev/null
+++ b/src/app/loans/products/status/form/form.component.html
@@ -0,0 +1,57 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Task details' | translate}}" [state]="detailForm.valid ? 'complete' : detailForm.pristine ? 'none' : 'required'">
+ <form [formGroup]="detailForm" layout="column">
+ <fims-id-input flex [form]="detailForm" controlName="identifier" [readonly]="editMode"></fims-id-input>
+ <fims-text-input [form]="detailForm" controlName="name" placeholder="{{'Name' | translate}}"></fims-text-input>
+ <mat-form-field layout-margin flex>
+ <textarea matInput placeholder="{{'Description' | translate}}" formControlName="description"></textarea>
+ </mat-form-field>
+ <mat-checkbox formControlName="mandatory" layout-margin translate>
+ Mandatory
+ </mat-checkbox>
+ <mat-checkbox formControlName="fourEyes" layout-margin translate>
+ Four eyes
+ </mat-checkbox>
+ <div layout-gt-xs="column" layout-margin formArrayName="actions">
+ <h4 translate>Task needs to be executed, before loan</h4>
+ <div *ngFor="let address of actions; let i=index" layout="row" [formGroupName]="i">
+ <mat-form-field>
+ <mat-select formControlName="action">
+ <mat-option *ngFor="let actionOption of actionOptions" [value]="actionOption.type">
+ {{actionOption.label}}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ <button mat-button (click)="removeAction(i)">{{'Remove' | translate}}</button>
+ </div>
+ <button mat-button (click)="addAction()">{{'Add action' | translate}}</button>
+ </div>
+ </form>
+ <ng-template td-step-actions>
+ <fims-form-final-action
+ [resourceName]="'TASK'"
+ [editMode]="editMode"
+ [disabled]="!detailForm.valid"
+ (onCancel)="cancel()"
+ (onSave)="save()">
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/loans/products/status/form/form.component.ts b/src/app/loans/products/status/form/form.component.ts
new file mode 100644
index 0000000..9e9a486
--- /dev/null
+++ b/src/app/loans/products/status/form/form.component.ts
@@ -0,0 +1,126 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
+import {TaskDefinition} from '../../../../services/portfolio/domain/task-definition.model';
+import {TdStepComponent} from '@covalent/core';
+import {AbstractControl, FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {ActionOption} from '../../../../common/domain/action-option.model';
+import {WorkflowAction} from '../../../../services/portfolio/domain/individuallending/workflow-action.model';
+import {FimsValidators} from '../../../../common/validator/validators';
+
+@Component({
+ selector: 'fims-product-task-form-component',
+ templateUrl: './form.component.html'
+})
+export class ProductTaskFormComponent implements OnInit {
+
+ @Input('task') set task(task: TaskDefinition){
+ this.prepareDetailForm(task);
+ };
+
+ @Input('editMode') editMode: boolean;
+
+ @Output('onSave') onSave = new EventEmitter<TaskDefinition>();
+
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ @ViewChild('detailsStep') detailsStep: TdStepComponent;
+
+ detailForm: FormGroup;
+
+ actionOptions: ActionOption[] = [
+ { type: 'OPEN', label: 'can be opened' },
+ { type: 'DENY', label: 'can be denied' },
+ { type: 'APPROVE', label: 'can be approved' },
+ { type: 'DISBURSE', label: 'can be disbursed' },
+ { type: 'WRITE_OFF', label: 'can be written off' },
+ { type: 'CLOSE', label: 'can be closed' },
+ { type: 'RECOVER', label: 'can recovered' },
+ { type: 'ACCEPT_PAYMENT', label: 'can be repayed' },
+ ];
+
+ constructor(private formBuilder: FormBuilder) {}
+
+ ngOnInit(): void {
+ this.openDetailsStep();
+ }
+
+ openDetailsStep(): void {
+ this.detailsStep.open();
+ }
+
+ private prepareDetailForm(task: TaskDefinition) {
+ this.detailForm = this.formBuilder.group({
+ identifier: [task.identifier, [Validators.required, Validators.minLength(3), Validators.maxLength(32), FimsValidators.urlSafe]],
+ name: [task.name, [Validators.required]],
+ description: [task.description, [Validators.required]],
+ actions: this.initActions(task.actions),
+ fourEyes: [task.fourEyes, [Validators.required]],
+ mandatory: [task.mandatory, [Validators.required]],
+ });
+ }
+
+ private initActions(values: string[]): FormArray {
+ const formControls: FormGroup[] = [];
+ values.forEach(value => formControls.push(this.initAction(value)));
+ return this.formBuilder.array(formControls);
+ }
+
+ private initAction(value?: string): FormGroup {
+ return this.formBuilder.group({
+ action: [value ? value : '', Validators.required]
+ });
+ }
+
+ addAction(): void {
+ const actions: FormArray = this.detailForm.get('actions') as FormArray;
+ actions.push(this.initAction());
+ }
+
+ removeAction(index: number): void {
+ const actions: FormArray = this.detailForm.get('actions') as FormArray;
+ actions.removeAt(index);
+ }
+
+ get actions(): AbstractControl[] {
+ const actions: FormArray = this.detailForm.get('actions') as FormArray;
+ return actions.controls;
+ }
+
+ save(): void {
+ const actions: any[] = this.detailForm.get('actions').value;
+ const rawActions: WorkflowAction[] = [];
+ actions.forEach(action => rawActions.push(action.action));
+
+ const task: TaskDefinition = {
+ identifier: this.detailForm.get('identifier').value,
+ name: this.detailForm.get('name').value,
+ description: this.detailForm.get('description').value,
+ actions: rawActions,
+ fourEyes: this.detailForm.get('fourEyes').value,
+ mandatory: this.detailForm.get('mandatory').value
+ };
+ this.onSave.emit(task);
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+}
diff --git a/src/app/loans/products/status/status.component.html b/src/app/loans/products/status/status.component.html
new file mode 100644
index 0000000..165df99
--- /dev/null
+++ b/src/app/loans/products/status/status.component.html
@@ -0,0 +1,21 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Manage task definitions' | translate}}" [navigateBackTo]="['../']">
+ <fims-data-table flex (onFetch)="fetchTasks($event)" (onActionCellClick)="rowSelect($event)" [columns]="columns" [data]="tasksData$ | async" [sortable]="false"></fims-data-table>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Create new task' | translate}}" icon="add" [link]="['create']" [permission]="{ id: 'portfolio_products', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/loans/products/status/status.component.ts b/src/app/loans/products/status/status.component.ts
new file mode 100644
index 0000000..f4acb06
--- /dev/null
+++ b/src/app/loans/products/status/status.component.ts
@@ -0,0 +1,75 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {TaskDefinition} from '../../../services/portfolio/domain/task-definition.model';
+import {ActivatedRoute, Router} from '@angular/router';
+import {TableData, TableFetchRequest} from '../../../common/data-table/data-table.component';
+import {PortfolioStore} from '../store/index';
+import {Observable} from 'rxjs/Observable';
+import {Subscription} from 'rxjs/Subscription';
+import * as fromPortfolio from '../store';
+import {LOAD_ALL} from '../store/tasks/task.actions';
+import {FimsProduct} from '../store/model/fims-product.model';
+
+@Component({
+ templateUrl: './status.component.html'
+})
+export class ProductStatusComponent implements OnInit, OnDestroy {
+
+ private productSubscription: Subscription;
+
+ tasksData$: Observable<TableData>;
+
+ columns: any[] = [
+ { name: 'identifier', label: 'Id' },
+ { name: 'name', label: 'Name' }
+ ];
+
+ private product: FimsProduct;
+
+ constructor(private router: Router, private route: ActivatedRoute, private portfolioStore: PortfolioStore) {}
+
+ ngOnInit(): void {
+ this.productSubscription = this.portfolioStore.select(fromPortfolio.getSelectedProduct)
+ .subscribe(product => {
+ this.product = product;
+ this.fetchTasks();
+ });
+
+ this.tasksData$ = this.portfolioStore.select(fromPortfolio.getAllProductTaskEntities)
+ .map(tasks => ({
+ totalElements: tasks.length,
+ totalPages: 1,
+ data: tasks
+ }));
+
+ }
+
+ ngOnDestroy(): void {
+ this.productSubscription.unsubscribe();
+ }
+
+ fetchTasks(event?: TableFetchRequest): void {
+ this.portfolioStore.dispatch({ type: LOAD_ALL, payload: this.product.identifier });
+ }
+
+ rowSelect(taskDefinition: TaskDefinition): void {
+ this.router.navigate(['detail', taskDefinition.identifier], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/loans/products/status/status.detail.component.html b/src/app/loans/products/status/status.detail.component.html
new file mode 100644
index 0000000..82cc2a2
--- /dev/null
+++ b/src/app/loans/products/status/status.detail.component.html
@@ -0,0 +1,44 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over [title]="task.name" [subTitle]="task.description" *ngIf="task$ | async as task" [navigateBackTo]="['../../']">
+ <fims-layout-card-over-header-menu *ngIf="product$ | async as product">
+ <button mat-icon-button (click)="deleteTask(product, task)" title="{{'Delete this task' | translate}}" *hasPermission="{ id: 'portfolio_products', accessLevel: 'CHANGE' }"><mat-icon>delete</mat-icon></button>
+ </fims-layout-card-over-header-menu>
+ <div class="mat-content inset" flex>
+ <div layout="row">
+ <mat-list>
+ <mat-list-item>
+ <mat-icon matListAvatar>account_balance</mat-icon>
+ <h3 matLine translate>Actions</h3>
+ <p matLine>{{task.actions.join(',')}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <mat-icon matListAvatar>person</mat-icon>
+ <h3 matLine translate>Mandatory</h3>
+ <p matLine>{{task.mandatory}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <mat-icon matListAvatar>person</mat-icon>
+ <h3 matLine translate>Four eyes</h3>
+ <p matLine>{{task.fourEyes}}</p>
+ </mat-list-item>
+ </mat-list>
+ </div>
+ </div>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Edit task' | translate}}" icon="mode_edit" [link]="['edit']" [permission]="{ id: 'portfolio_products', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/loans/products/status/status.detail.component.ts b/src/app/loans/products/status/status.detail.component.ts
new file mode 100644
index 0000000..269e129
--- /dev/null
+++ b/src/app/loans/products/status/status.detail.component.ts
@@ -0,0 +1,77 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {TaskDefinition} from '../../../services/portfolio/domain/task-definition.model';
+import {DELETE, SelectAction} from '../store/tasks/task.actions';
+import {PortfolioStore} from '../store/index';
+import {Subscription} from 'rxjs/Subscription';
+import * as fromPortfolio from '../store';
+import {Observable} from 'rxjs/Observable';
+import {TdDialogService} from '@covalent/core';
+import {Product} from '../../../services/portfolio/domain/product.model';
+import {FimsProduct} from '../store/model/fims-product.model';
+
+@Component({
+ templateUrl: './status.detail.component.html'
+})
+export class ProductStatusDetailComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ task$: Observable<TaskDefinition>;
+
+ product$: Observable<FimsProduct>;
+
+ constructor(private route: ActivatedRoute, private portfolioStore: PortfolioStore, private dialogService: TdDialogService) {}
+
+ ngOnInit(): void {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectAction(params['taskId']))
+ .subscribe(this.portfolioStore);
+
+ this.task$ = this.portfolioStore.select(fromPortfolio.getSelectedProductTask);
+ this.product$ = this.portfolioStore.select(fromPortfolio.getSelectedProduct);
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ }
+
+ confirmDeletion(): Observable<boolean> {
+ return this.dialogService.openConfirm({
+ message: 'Do you want to delete this task?',
+ title: 'Confirm deletion',
+ acceptButton: 'DELETE TASK',
+ }).afterClosed();
+ }
+
+ deleteTask(product: Product, task: TaskDefinition): void {
+ this.confirmDeletion()
+ .filter(accept => accept)
+ .subscribe(() => {
+ this.portfolioStore.dispatch({ type: DELETE, payload: {
+ productId: product.identifier,
+ task,
+ activatedRoute: this.route
+ } });
+ });
+ }
+}
diff --git a/src/app/loans/products/status/task-exists.guard.ts b/src/app/loans/products/status/task-exists.guard.ts
new file mode 100644
index 0000000..adecb20
--- /dev/null
+++ b/src/app/loans/products/status/task-exists.guard.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import {Injectable} from '@angular/core';
+import * as fromProducts from '../store';
+import {Observable} from 'rxjs/Observable';
+import {LoadAction} from '../store/tasks/task.actions';
+import {of} from 'rxjs/observable/of';
+import {PortfolioStore} from '../store/index';
+import {PortfolioService} from '../../../services/portfolio/portfolio.service';
+import {ExistsGuardService} from '../../../common/guards/exists-guard';
+
+@Injectable()
+export class ProductTaskExistsGuard implements CanActivate {
+
+ constructor(private store: PortfolioStore,
+ private portfolioService: PortfolioService,
+ private existsGuardService: ExistsGuardService) {}
+
+ hasTaskInStore(id: string): Observable<boolean> {
+ const timestamp$ = this.store.select(fromProducts.getProductTasksLoadedAt)
+ .map(loadedAt => loadedAt[id]);
+
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ hasTaskInApi(productId: string, taskId: string): Observable<boolean> {
+ const getTaskDefintion$ = this.portfolioService.getTaskDefinition(productId, taskId)
+ .map(taskEntity => new LoadAction({
+ resource: taskEntity
+ }))
+ .do((action: LoadAction) => this.store.dispatch(action))
+ .map(task => !!task);
+
+ return this.existsGuardService.routeTo404OnError(getTaskDefintion$);
+ }
+
+ hasTask(productId: string, taskId: string): Observable<boolean> {
+ return this.hasTaskInStore(taskId)
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+
+ return this.hasTaskInApi(productId, taskId);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasTask(route.parent.params['productId'], route.params['taskId']);
+ }
+}
diff --git a/src/app/loans/products/store/charges/charge.actions.ts b/src/app/loans/products/store/charges/charge.actions.ts
new file mode 100644
index 0000000..4b5d29a
--- /dev/null
+++ b/src/app/loans/products/store/charges/charge.actions.ts
@@ -0,0 +1,155 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Action} from '@ngrx/store';
+import {Error} from '../../../../services/domain/error.model';
+import {type} from '../../../../store/util';
+import {RoutePayload} from '../../../../common/store/route-payload';
+import {ChargeDefinition} from '../../../../services/portfolio/domain/charge-definition.model';
+import {
+ CreateResourceSuccessPayload,
+ DeleteResourceSuccessPayload,
+ LoadResourcePayload,
+ SelectResourcePayload,
+ UpdateResourceSuccessPayload
+} from '../../../../common/store/resource.reducer';
+
+export const LOAD_ALL = type('[Product Charge] Load All');
+export const LOAD_ALL_COMPLETE = type('[Product Charge] Load All Complete');
+
+export const LOAD = type('[Product Charge] Load');
+export const SELECT = type('[Product Charge] Select');
+
+export const CREATE = type('[Product Charge] Create');
+export const CREATE_SUCCESS = type('[Product Charge] Create Success');
+export const CREATE_FAIL = type('[Product Charge] Create Fail');
+
+export const UPDATE = type('[Product Charge] Update');
+export const UPDATE_SUCCESS = type('[Product Charge] Update Success');
+export const UPDATE_FAIL = type('[Product Charge] Update Fail');
+
+export const DELETE = type('[Product Charge] Delete');
+export const DELETE_SUCCESS = type('[Product Charge] Delete Success');
+export const DELETE_FAIL = type('[Product Charge] Delete Fail');
+
+export const RESET_FORM = type('[Product Charge] Reset Form');
+
+export interface ChargeRoutePayload extends RoutePayload {
+ productId: string;
+ charge: ChargeDefinition;
+}
+
+export class LoadAllAction implements Action {
+ readonly type = LOAD_ALL;
+
+ constructor(public payload: string) { }
+}
+
+export class LoadAllCompleteAction implements Action {
+ readonly type = LOAD_ALL_COMPLETE;
+
+ constructor(public payload: ChargeDefinition[]) { }
+}
+
+export class LoadAction implements Action {
+ readonly type = LOAD;
+
+ constructor(public payload: LoadResourcePayload) { }
+}
+
+export class SelectAction implements Action {
+ readonly type = SELECT;
+
+ constructor(public payload: SelectResourcePayload) { }
+}
+
+export class CreateChargeAction implements Action {
+ readonly type = CREATE;
+
+ constructor(public payload: ChargeRoutePayload) { }
+}
+
+export class CreateChargeSuccessAction implements Action {
+ readonly type = CREATE_SUCCESS;
+
+ constructor(public payload: CreateResourceSuccessPayload) { }
+}
+
+export class CreateChargeFailAction implements Action {
+ readonly type = CREATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class UpdateChargeAction implements Action {
+ readonly type = UPDATE;
+
+ constructor(public payload: ChargeRoutePayload) { }
+}
+
+export class UpdateChargeSuccessAction implements Action {
+ readonly type = UPDATE_SUCCESS;
+
+ constructor(public payload: UpdateResourceSuccessPayload) { }
+}
+
+export class UpdateChargeFailAction implements Action {
+ readonly type = UPDATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class DeleteChargeAction implements Action {
+ readonly type = DELETE;
+
+ constructor(public payload: ChargeRoutePayload) { }
+}
+
+export class DeleteChargeSuccessAction implements Action {
+ readonly type = DELETE_SUCCESS;
+
+ constructor(public payload: DeleteResourceSuccessPayload) { }
+}
+
+export class DeleteChargeFailAction implements Action {
+ readonly type = DELETE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class ResetChargeFormAction implements Action {
+ readonly type = RESET_FORM;
+
+ constructor() {}
+}
+
+export type Actions
+ = LoadAllAction
+ | LoadAllCompleteAction
+ | LoadAction
+ | SelectAction
+ | CreateChargeAction
+ | CreateChargeSuccessAction
+ | CreateChargeFailAction
+ | UpdateChargeAction
+ | UpdateChargeSuccessAction
+ | UpdateChargeFailAction
+ | DeleteChargeAction
+ | DeleteChargeSuccessAction
+ | DeleteChargeFailAction
+ | ResetChargeFormAction;
diff --git a/src/app/loans/products/store/charges/charges.reducer.ts b/src/app/loans/products/store/charges/charges.reducer.ts
new file mode 100644
index 0000000..dc3df87
--- /dev/null
+++ b/src/app/loans/products/store/charges/charges.reducer.ts
@@ -0,0 +1,66 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as charge from './charge.actions';
+import {ChargeDefinition} from '../../../../services/portfolio/domain/charge-definition.model';
+import {ResourceState} from '../../../../common/store/resource.reducer';
+import {idsToHashWithCurrentTimestamp, resourcesToHash} from '../../../../common/store/reducer.helper';
+
+export interface State extends ResourceState {
+ ids: string[];
+ entities: { [id: string]: ChargeDefinition };
+ selectedId: string | null;
+}
+
+export const initialState: State = {
+ ids: [],
+ entities: {},
+ loadedAt: {},
+ selectedId: null,
+};
+
+export function reducer(state = initialState, action: charge.Actions): ResourceState {
+
+ switch (action.type) {
+
+ case charge.LOAD_ALL: {
+ return initialState;
+ }
+
+ case charge.LOAD_ALL_COMPLETE: {
+ const chargeDefinitions: ChargeDefinition[] = action.payload;
+
+ const ids = chargeDefinitions.map(chargeDefinition => chargeDefinition.identifier);
+
+ const entities = resourcesToHash(chargeDefinitions);
+
+ const loadedAt = idsToHashWithCurrentTimestamp(ids);
+
+ return {
+ ids: [ ...ids ],
+ entities: entities,
+ loadedAt: loadedAt,
+ selectedId: state.selectedId
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
diff --git a/src/app/loans/products/store/charges/effects/notification.effects.ts b/src/app/loans/products/store/charges/effects/notification.effects.ts
new file mode 100644
index 0000000..01139ba
--- /dev/null
+++ b/src/app/loans/products/store/charges/effects/notification.effects.ts
@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {NotificationService, NotificationType} from '../../../../../services/notification/notification.service';
+import {Action} from '@ngrx/store';
+import {Observable} from 'rxjs/Observable';
+import {Actions, Effect} from '@ngrx/effects';
+import {Injectable} from '@angular/core';
+import * as chargeActions from '../charge.actions';
+
+@Injectable()
+export class ProductChargesNotificationEffects {
+
+ @Effect({dispatch: false})
+ createUpdateCustomerChargeSuccess$: Observable<Action> = this.actions$
+ .ofType(chargeActions.CREATE_SUCCESS, chargeActions.UPDATE)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Fee is going to be saved'
+ }));
+
+ @Effect({dispatch: false})
+ deleteCustomerChargeSuccess$: Observable<Action> = this.actions$
+ .ofType(chargeActions.DELETE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Fee is going to be deleted'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+}
+
diff --git a/src/app/loans/products/store/charges/effects/route.effects.ts b/src/app/loans/products/store/charges/effects/route.effects.ts
new file mode 100644
index 0000000..b5f4b12
--- /dev/null
+++ b/src/app/loans/products/store/charges/effects/route.effects.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as chargeActions from '../charge.actions';
+import {Router} from '@angular/router';
+
+@Injectable()
+export class ProductChargesRouteEffects {
+
+ @Effect({ dispatch: false })
+ createUpdateProductChargeSuccess$: Observable<Action> = this.actions$
+ .ofType(chargeActions.CREATE_SUCCESS, chargeActions.UPDATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute }));
+
+ @Effect({ dispatch: false })
+ deleteChargeSuccess$: Observable<Action> = this.actions$
+ .ofType(chargeActions.DELETE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../../'], { relativeTo: payload.activatedRoute }));
+
+ constructor(private actions$: Actions, private router: Router) { }
+
+}
diff --git a/src/app/loans/products/store/charges/effects/service.effects.ts b/src/app/loans/products/store/charges/effects/service.effects.ts
new file mode 100644
index 0000000..3e88033
--- /dev/null
+++ b/src/app/loans/products/store/charges/effects/service.effects.ts
@@ -0,0 +1,84 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+import * as chargeActions from '../charge.actions';
+import {PortfolioService} from '../../../../../services/portfolio/portfolio.service';
+
+@Injectable()
+export class ProductChargesApiEffects {
+
+ @Effect()
+ loadAll$: Observable<Action> = this.actions$
+ .ofType(chargeActions.LOAD_ALL)
+ .debounceTime(300)
+ .map((action: chargeActions.LoadAllAction) => action.payload)
+ .switchMap(id => {
+ const nextSearch$ = this.actions$.ofType(chargeActions.LOAD_ALL).skip(1);
+
+ return this.portfolioService.findAllChargeDefinitionsForProduct(id)
+ .takeUntil(nextSearch$)
+ .map(chargeDefinitions => new chargeActions.LoadAllCompleteAction(chargeDefinitions))
+ .catch(() => of(new chargeActions.LoadAllCompleteAction([])));
+ });
+
+ @Effect()
+ createCharge$: Observable<Action> = this.actions$
+ .ofType(chargeActions.CREATE)
+ .map((action: chargeActions.CreateChargeAction) => action.payload)
+ .mergeMap(payload =>
+ this.portfolioService.createChargeDefinition(payload.productId, payload.charge)
+ .map(() => new chargeActions.CreateChargeSuccessAction({
+ resource: payload.charge,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new chargeActions.CreateChargeFailAction(error)))
+ );
+
+ @Effect()
+ updateCharge$: Observable<Action> = this.actions$
+ .ofType(chargeActions.UPDATE)
+ .map((action: chargeActions.UpdateChargeAction) => action.payload)
+ .mergeMap(payload =>
+ this.portfolioService.changeChargeDefinition(payload.productId, payload.charge)
+ .map(() => new chargeActions.UpdateChargeSuccessAction({
+ resource: payload.charge,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new chargeActions.UpdateChargeFailAction(error)))
+ );
+
+ @Effect()
+ deleteCharge$: Observable<Action> = this.actions$
+ .ofType(chargeActions.DELETE)
+ .map((action: chargeActions.DeleteChargeAction) => action.payload)
+ .mergeMap(payload =>
+ this.portfolioService.deleteChargeDefinition(payload.productId, payload.charge.identifier)
+ .map(() => new chargeActions.DeleteChargeSuccessAction({
+ resource: payload.charge,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new chargeActions.DeleteChargeFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private portfolioService: PortfolioService) { }
+}
diff --git a/src/app/loans/products/store/effects/notification.effects.ts b/src/app/loans/products/store/effects/notification.effects.ts
new file mode 100644
index 0000000..3361111
--- /dev/null
+++ b/src/app/loans/products/store/effects/notification.effects.ts
@@ -0,0 +1,76 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect, toPayload} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as productActions from '../product.actions';
+import {NotificationService, NotificationType} from '../../../../services/notification/notification.service';
+
+@Injectable()
+export class ProductNotificationEffects {
+
+ @Effect({ dispatch: false })
+ createProductSuccess$: Observable<Action> = this.actions$
+ .ofType(productActions.CREATE_SUCCESS, productActions.UPDATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Product is going to be saved'
+ }));
+
+ @Effect({ dispatch: false })
+ deleteProductSuccess$: Observable<Action> = this.actions$
+ .ofType(productActions.DELETE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Product is going to be deleted'
+ }));
+
+ @Effect({ dispatch: false })
+ deleteProductFail$: Observable<Action> = this.actions$
+ .ofType(productActions.DELETE_FAIL)
+ .do(() => this.notificationService.send({
+ type: NotificationType.ALERT,
+ title: 'Product can\'t be deleted',
+ message: 'Product is already assigned to a member.'
+ }));
+
+ @Effect({ dispatch: false })
+ enableProductSuccess$: Observable<Action> = this.actions$
+ .ofType(productActions.ENABLE_SUCCESS)
+ .map(toPayload)
+ .do(payload => {
+ const action: string = payload.enable ? 'enabled' : 'disabled';
+ this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: `Product is going to be ${action}`
+ });
+ });
+
+ @Effect({ dispatch: false })
+ enableProductFail$: Observable<Action> = this.actions$
+ .ofType(productActions.ENABLE_FAIL)
+ .map(toPayload)
+ .do(payload => this.notificationService.send({
+ type: NotificationType.ALERT,
+ message: 'Product could not be enabled'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+}
diff --git a/src/app/loans/products/store/effects/route.effects.ts b/src/app/loans/products/store/effects/route.effects.ts
new file mode 100644
index 0000000..0b76373
--- /dev/null
+++ b/src/app/loans/products/store/effects/route.effects.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as productActions from '../product.actions';
+import {Router} from '@angular/router';
+
+@Injectable()
+export class ProductRouteEffects {
+
+ @Effect({ dispatch: false })
+ createProductSuccess$: Observable<Action> = this.actions$
+ .ofType(productActions.CREATE_SUCCESS, productActions.UPDATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute} ));
+
+ @Effect({ dispatch: false })
+ deleteProductSuccess$: Observable<Action> = this.actions$
+ .ofType(productActions.DELETE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../../../'], { relativeTo: payload.activatedRoute} ));
+
+ constructor(private actions$: Actions, private router: Router) { }
+
+}
diff --git a/src/app/loans/products/store/effects/service.effects.ts b/src/app/loans/products/store/effects/service.effects.ts
new file mode 100644
index 0000000..c53ce52
--- /dev/null
+++ b/src/app/loans/products/store/effects/service.effects.ts
@@ -0,0 +1,102 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {PortfolioService} from '../../../../services/portfolio/portfolio.service';
+import {Action} from '@ngrx/store';
+import {Observable} from 'rxjs/Observable';
+import {Actions, Effect} from '@ngrx/effects';
+import * as productActions from '../product.actions';
+import {of} from 'rxjs/observable/of';
+import {mapToFimsProducts, mapToProduct} from '../model/fims-product.mapper';
+import {emptySearchResult} from '../../../../common/store/search.reducer';
+
+@Injectable()
+export class ProductApiEffects {
+
+ @Effect()
+ search$: Observable<Action> = this.actions$
+ .ofType(productActions.SEARCH)
+ .map((action: productActions.SelectAction) => action.payload)
+ .debounceTime(300)
+ .switchMap(fetchRequest => {
+ const nextSearch$ = this.actions$.ofType(productActions.SEARCH).skip(1);
+
+ return this.portfolioService.findAllProducts(true, fetchRequest)
+ .takeUntil(nextSearch$)
+ .map(productPage => new productActions.SearchCompleteAction({
+ elements: mapToFimsProducts(productPage.elements),
+ totalElements: productPage.totalElements,
+ totalPages: productPage.totalPages
+ }))
+ .catch(() => of(new productActions.SearchCompleteAction(emptySearchResult())));
+ });
+
+ @Effect()
+ createProduct$: Observable<Action> = this.actions$
+ .ofType(productActions.CREATE)
+ .map((action: productActions.CreateProductAction) => action.payload)
+ .mergeMap(payload =>
+ this.portfolioService.createProduct(mapToProduct(payload.product))
+ .map(() => new productActions.CreateProductSuccessAction({
+ resource: payload.product,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new productActions.CreateProductFailAction(error)))
+ );
+
+ @Effect()
+ updateProduct$: Observable<Action> = this.actions$
+ .ofType(productActions.UPDATE)
+ .map((action: productActions.UpdateProductAction) => action.payload)
+ .mergeMap(payload =>
+ this.portfolioService.changeProduct(mapToProduct(payload.product))
+ .map(() => new productActions.UpdateProductSuccessAction({
+ resource: payload.product,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new productActions.UpdateProductFailAction(error)))
+ );
+
+ @Effect()
+ deleteProduct$: Observable<Action> = this.actions$
+ .ofType(productActions.DELETE)
+ .map((action: productActions.DeleteProductAction) => action.payload)
+ .mergeMap(payload =>
+ this.portfolioService.deleteProduct(payload.product.identifier)
+ .map(() => new productActions.DeleteProductSuccessAction({
+ resource: payload.product,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new productActions.DeleteProductFailAction(error)))
+ );
+
+ @Effect()
+ enableProduct$: Observable<Action> = this.actions$
+ .ofType(productActions.ENABLE)
+ .map((action: productActions.EnableProductAction) => action.payload)
+ .mergeMap(payload =>
+ this.portfolioService.enableProduct(payload.product.identifier, payload.enable)
+ .map(() => new productActions.EnableProductSuccessAction(payload))
+ .catch((error) =>
+ this.portfolioService.incompleteaccountassignments(payload.product.identifier)
+ .map(accountAssignments => new productActions.EnableProductFailAction(accountAssignments)))
+ );
+
+ constructor(private actions$: Actions, private portfolioService: PortfolioService) { }
+}
diff --git a/src/app/loans/products/store/index.ts b/src/app/loans/products/store/index.ts
new file mode 100644
index 0000000..7576882
--- /dev/null
+++ b/src/app/loans/products/store/index.ts
@@ -0,0 +1,151 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as fromRoot from '../../../store';
+import {ActionReducer, Store} from '@ngrx/store';
+import {createSelector} from 'reselect';
+import {createReducer} from '../../../store/index';
+import * as fromProducts from './products.reducer';
+import * as fromProductTasks from './tasks/tasks.reducer';
+import * as fromProductCharges from './charges/charges.reducer';
+import * as fromProductChargeRanges from './ranges/ranges.reducer';
+import * as fromProductLossProvision from './lossProvision/loss-provision.reducer';
+import {getLossProvisionConfiguration, getLossProvisionConfigurationLoadedAt} from './lossProvision/loss-provision.reducer';
+
+import {
+ createResourceReducer,
+ getResourceAll,
+ getResourceLoadedAt,
+ getResourceSelected,
+ ResourceState
+} from '../../../common/store/resource.reducer';
+import {
+ createSearchReducer,
+ getSearchEntities,
+ getSearchTotalElements,
+ getSearchTotalPages,
+ SearchState
+} from '../../../common/store/search.reducer';
+import {createFormReducer, FormState, getFormError} from '../../../common/store/form.reducer';
+
+export interface State extends fromRoot.State {
+ products: ResourceState;
+ productSearch: SearchState;
+ productForm: FormState;
+ productTasks: ResourceState;
+ productTaskForm: FormState;
+ productCharges: ResourceState;
+ productChargeForm: FormState;
+ productChargeRanges: ResourceState;
+ productLossProvision: fromProductLossProvision.State;
+}
+
+const reducers = {
+ products: createResourceReducer('Product', fromProducts.reducer),
+ productSearch: createSearchReducer('Product'),
+ productForm: createFormReducer('Product'),
+ productTasks: createResourceReducer('Product Task', fromProductTasks.reducer),
+ productTaskForm: createFormReducer('Product Task'),
+ productCharges: createResourceReducer('Product Charge', fromProductCharges.reducer),
+ productChargeForm: createFormReducer('Product Charge'),
+ productChargeRanges: createResourceReducer('Product Charge Range', fromProductChargeRanges.reducer),
+ productLossProvision: fromProductLossProvision.reducer
+};
+
+export const portfolioModuleReducer: ActionReducer<State> = createReducer(reducers);
+
+export class PortfolioStore extends Store<State> {}
+
+export function portfolioStoreFactory(appStore: Store<fromRoot.State>) {
+ appStore.replaceReducer(portfolioModuleReducer);
+ return appStore;
+}
+
+/**
+ * Product selectors
+ */
+export const getProductsState = (state: State) => state.products;
+
+export const getProductFormState = (state: State) => state.productForm;
+export const getProductFormError = createSelector(getProductFormState, getFormError);
+
+export const getProductsLoadedAt = createSelector(getProductsState, getResourceLoadedAt);
+export const getSelectedProduct = createSelector(getProductsState, getResourceSelected);
+
+/**
+ * Product search selector
+ */
+export const getProductSearchState = (state: State) => state.productSearch;
+
+export const getSearchProducts = createSelector(getProductSearchState, getSearchEntities);
+export const getProductSearchTotalElements = createSelector(getProductSearchState, getSearchTotalElements);
+export const getProductSearchTotalPages = createSelector(getProductSearchState, getSearchTotalPages);
+
+export const getProductSearchResults = createSelector(getSearchProducts, getProductSearchTotalPages, getProductSearchTotalElements,
+ (products, totalPages, totalElements) => {
+ return {
+ products: products,
+ totalPages: totalPages,
+ totalElements: totalElements
+ };
+});
+
+/**
+ * Product Task Selectors
+ */
+export const getProductTasksState = (state: State) => state.productTasks;
+
+export const getProductTaskFormState = (state: State) => state.productTaskForm;
+export const getProductTaskFormError = createSelector(getProductTaskFormState, getFormError);
+
+export const getProductTasksLoadedAt = createSelector(getProductTasksState, getResourceLoadedAt);
+export const getSelectedProductTask = createSelector(getProductTasksState, getResourceSelected);
+
+export const getAllProductTaskEntities = createSelector(getProductTasksState, getResourceAll);
+
+/**
+ * Product Charge Selectors
+ */
+export const getProductChargesState = (state: State) => state.productCharges;
+
+export const getProductChargesLoadedAt = createSelector(getProductChargesState, getResourceLoadedAt);
+export const getSelectedProductCharge = createSelector(getProductChargesState, getResourceSelected);
+
+export const getAllProductChargeEntities = createSelector(getProductChargesState, getResourceAll);
+
+/**
+ * Product Charge Range Selectors
+ */
+
+export const getProductChargeRangesState = (state: State) => state.productChargeRanges;
+
+export const getProductChargeRangesLoadedAt = createSelector(getProductChargeRangesState, getResourceLoadedAt);
+export const getSelectedProductChargeRange = createSelector(getProductChargeRangesState, getResourceSelected);
+
+export const getAllProductChargeRangeEntities = createSelector(getProductChargeRangesState, getResourceAll);
+
+/**
+ * Product Loss Configuration Selectors
+ */
+
+export const getProductLossProvisionState = (state: State) => state.productLossProvision;
+
+export const getProductLossProvisionConfigurationLoadedAt = createSelector(
+ getProductLossProvisionState, getLossProvisionConfigurationLoadedAt
+);
+export const getProductLossProvisionConfiguration = createSelector(getProductLossProvisionState, getLossProvisionConfiguration);
diff --git a/src/app/loans/products/store/lossProvision/effects/notification.effects.ts b/src/app/loans/products/store/lossProvision/effects/notification.effects.ts
new file mode 100644
index 0000000..d218443
--- /dev/null
+++ b/src/app/loans/products/store/lossProvision/effects/notification.effects.ts
@@ -0,0 +1,38 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {NotificationService, NotificationType} from '../../../../../services/notification/notification.service';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as lossProvisionActions from '../loss-provision.actions';
+
+@Injectable()
+export class ProductLossProvisionNotificationEffects {
+
+ @Effect({ dispatch: false })
+ updateLossProvisionSuccess$: Observable<Action> = this.actions$
+ .ofType(lossProvisionActions.UPDATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Loss provision configuration is going to be saved'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) { }
+}
diff --git a/src/app/loans/products/store/lossProvision/effects/route.effects.ts b/src/app/loans/products/store/lossProvision/effects/route.effects.ts
new file mode 100644
index 0000000..2acae17
--- /dev/null
+++ b/src/app/loans/products/store/lossProvision/effects/route.effects.ts
@@ -0,0 +1,36 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import * as lossProvisionActions from '../loss-provision.actions';
+import {Action} from '@ngrx/store';
+import {Router} from '@angular/router';
+
+@Injectable()
+export class ProductLossProvisionRouteEffects {
+
+ @Effect({ dispatch: false })
+ updateLossProvisionSuccess$: Observable<Action> = this.actions$
+ .ofType(lossProvisionActions.UPDATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute} ));
+
+ constructor(private actions$: Actions, private router: Router) { }
+}
diff --git a/src/app/loans/products/store/lossProvision/effects/service.effects.ts b/src/app/loans/products/store/lossProvision/effects/service.effects.ts
new file mode 100644
index 0000000..9ca4205
--- /dev/null
+++ b/src/app/loans/products/store/lossProvision/effects/service.effects.ts
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+import {PortfolioService} from '../../../../../services/portfolio/portfolio.service';
+import * as lossProvisionActions from '../loss-provision.actions';
+
+@Injectable()
+export class ProductLossProvisionApiEffects {
+
+ @Effect()
+ updateConfiguration$: Observable<Action> = this.actions$
+ .ofType(lossProvisionActions.UPDATE)
+ .map((action: lossProvisionActions.UpdateLossProvisionAction) => action.payload)
+ .mergeMap(payload =>
+ this.portfolioService.changeLossProvisionConfiguration(payload.productIdentifier, payload.configuration)
+ .map(() => new lossProvisionActions.UpdateLossProvisionSuccessAction(payload))
+ .catch(error => of(new lossProvisionActions.UpdateLossProvisionFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private portfolioService: PortfolioService) { }
+
+}
diff --git a/src/app/loans/products/store/lossProvision/loss-provision.actions.ts b/src/app/loans/products/store/lossProvision/loss-provision.actions.ts
new file mode 100644
index 0000000..1b3ea7d
--- /dev/null
+++ b/src/app/loans/products/store/lossProvision/loss-provision.actions.ts
@@ -0,0 +1,63 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {type} from '../../../../store/util';
+import {Action} from '@ngrx/store';
+import {RoutePayload} from '../../../../common/store/route-payload';
+import {LossProvisionConfiguration} from '../../../../services/portfolio/domain/loss-provision-configuration.model';
+
+export const LOAD = type('[Product Loss Provision] Load');
+
+export const UPDATE = type('[Product Loss Provision] Update');
+export const UPDATE_SUCCESS = type('[Product Loss Provision] Update Success');
+export const UPDATE_FAIL = type('[Product Loss Provision] Update Fail');
+
+export interface LossProvisionPayload extends RoutePayload {
+ productIdentifier: string;
+ configuration: LossProvisionConfiguration;
+}
+
+export class LoadAction implements Action {
+ readonly type = LOAD;
+
+ constructor(public payload: LossProvisionConfiguration) { }
+}
+
+export class UpdateLossProvisionAction implements Action {
+ readonly type = UPDATE;
+
+ constructor(public payload: LossProvisionPayload) { }
+}
+
+export class UpdateLossProvisionSuccessAction implements Action {
+ readonly type = UPDATE_SUCCESS;
+
+ constructor(public payload: LossProvisionPayload) { }
+}
+
+export class UpdateLossProvisionFailAction implements Action {
+ readonly type = UPDATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export type Actions
+ = LoadAction
+ | UpdateLossProvisionAction
+ | UpdateLossProvisionSuccessAction
+ | UpdateLossProvisionFailAction;
diff --git a/src/app/loans/products/store/lossProvision/loss-provision.reducer.ts b/src/app/loans/products/store/lossProvision/loss-provision.reducer.ts
new file mode 100644
index 0000000..5f5d2db
--- /dev/null
+++ b/src/app/loans/products/store/lossProvision/loss-provision.reducer.ts
@@ -0,0 +1,61 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as provisionActions from './loss-provision.actions';
+import {LossProvisionConfiguration} from '../../../../services/portfolio/domain/loss-provision-configuration.model';
+
+export interface State {
+ configuration: LossProvisionConfiguration;
+ loadedAt: number;
+}
+
+const initialState: State = {
+ configuration: null,
+ loadedAt: null
+};
+
+export function reducer(state: State = initialState, action: provisionActions.Actions): State {
+
+ switch (action.type) {
+
+ case provisionActions.LOAD: {
+ const configuration: LossProvisionConfiguration = action.payload;
+
+ return Object.assign({}, state, {
+ configuration,
+ loadedAt: Date.now()
+ });
+ }
+
+ case provisionActions.UPDATE_SUCCESS: {
+ const configuration: LossProvisionConfiguration = action.payload.configuration;
+
+ return Object.assign({}, state, {
+ configuration,
+ loadedAt: state.loadedAt
+ });
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+export const getLossProvisionConfiguration = (state: State) => state.configuration;
+export const getLossProvisionConfigurationLoadedAt = (state: State) => state.loadedAt;
diff --git a/src/app/loans/products/store/model/fims-product.mapper.ts b/src/app/loans/products/store/model/fims-product.mapper.ts
new file mode 100644
index 0000000..7b1445c
--- /dev/null
+++ b/src/app/loans/products/store/model/fims-product.mapper.ts
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Product} from '../../../../services/portfolio/domain/product.model';
+import {FimsProduct} from './fims-product.model';
+
+export function mapToProduct(product: FimsProduct): Product {
+ return Object.assign({}, product, {
+ parameters: JSON.stringify(product.parameters)
+ });
+}
+
+export function mapToFimsProduct(product: Product): FimsProduct {
+ return Object.assign({}, product, {
+ parameters: JSON.parse(product.parameters)
+ });
+}
+
+export function mapToFimsProducts(products: Product[]): FimsProduct[] {
+ const fimsProducts: FimsProduct[] = [];
+
+ for (const product of products){
+ fimsProducts.push(mapToFimsProduct(product));
+ }
+
+ return fimsProducts;
+}
diff --git a/src/app/loans/products/store/model/fims-product.model.ts b/src/app/loans/products/store/model/fims-product.model.ts
new file mode 100644
index 0000000..e366338
--- /dev/null
+++ b/src/app/loans/products/store/model/fims-product.model.ts
@@ -0,0 +1,50 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ProductParameters} from '../../../../services/portfolio/domain/individuallending/product-parameters.model';
+import {AccountAssignment} from '../../../../services/portfolio/domain/account-assignment.model';
+import {InterestBasis} from '../../../../services/portfolio/domain/interest-basis.model';
+import {InterestRange} from '../../../../services/portfolio/domain/interest-range.model';
+import {BalanceRange} from '../../../../services/portfolio/domain/balance-range.model';
+import {TermRange} from '../../../../services/portfolio/domain/term-range.model';
+
+/**
+ * Model interface with concrete ProductParameters instead of JSON string.
+ */
+
+export interface FimsProduct {
+ identifier: string;
+ name: string;
+ termRange: TermRange;
+ balanceRange: BalanceRange;
+ interestRange: InterestRange;
+ interestBasis: InterestBasis;
+ patternPackage: string;
+ description: string;
+ accountAssignments: AccountAssignment[];
+ parameters: ProductParameters;
+ currencyCode: string;
+ minorCurrencyUnitDigits: number;
+ enabled?: boolean;
+ createdOn?: string;
+ createdBy?: string;
+ lastModifiedOn?: string;
+ lastModifiedBy?: string;
+}
+
+
diff --git a/src/app/loans/products/store/product.actions.ts b/src/app/loans/products/store/product.actions.ts
new file mode 100644
index 0000000..cad72f3
--- /dev/null
+++ b/src/app/loans/products/store/product.actions.ts
@@ -0,0 +1,186 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Action} from '@ngrx/store';
+import {Error} from '../../../services/domain/error.model';
+import {type} from '../../../store/util';
+import {RoutePayload} from '../../../common/store/route-payload';
+import {FimsProduct} from './model/fims-product.model';
+import {SearchResult} from '../../../common/store/search.reducer';
+import {
+ CreateResourceSuccessPayload,
+ DeleteResourceSuccessPayload,
+ LoadResourcePayload,
+ SelectResourcePayload,
+ UpdateResourceSuccessPayload
+} from '../../../common/store/resource.reducer';
+import {AccountAssignment} from '../../../services/portfolio/domain/account-assignment.model';
+import {FetchRequest} from '../../../services/domain/paging/fetch-request.model';
+
+export const SEARCH = type('[Product] Search');
+export const SEARCH_COMPLETE = type('[Product] Search Complete');
+
+export const LOAD = type('[Product] Load');
+export const SELECT = type('[Product] Select');
+
+export const CREATE = type('[Product] Create');
+export const CREATE_SUCCESS = type('[Product] Create Success');
+export const CREATE_FAIL = type('[Product] Create Fail');
+
+export const UPDATE = type('[Product] Update');
+export const UPDATE_SUCCESS = type('[Product] Update Success');
+export const UPDATE_FAIL = type('[Product] Update Fail');
+
+export const DELETE = type('[Product] Delete');
+export const DELETE_SUCCESS = type('[Product] Delete Success');
+export const DELETE_FAIL = type('[Product] Delete Fail');
+
+export const ENABLE = type('[Product] Enable');
+export const ENABLE_SUCCESS = type('[Product] Enable Success');
+export const ENABLE_FAIL = type('[Product] Enable Fail');
+
+export const RESET_FORM = type('[Product] Reset Form');
+
+export interface ProductRoutePayload extends RoutePayload {
+ product: FimsProduct;
+}
+
+export interface EnableProductPayload {
+ product: FimsProduct;
+ enable: boolean;
+}
+
+export class SearchAction implements Action {
+ readonly type = SEARCH;
+
+ constructor(payload: FetchRequest) { }
+}
+
+export class SearchCompleteAction implements Action {
+ readonly type = SEARCH_COMPLETE;
+
+ constructor(public payload: SearchResult) { }
+}
+
+export class LoadAction implements Action {
+ readonly type = LOAD;
+
+ constructor(public payload: LoadResourcePayload) { }
+}
+
+export class SelectAction implements Action {
+ readonly type = SELECT;
+
+ constructor(public payload: SelectResourcePayload) { }
+}
+
+export class CreateProductAction implements Action {
+ readonly type = CREATE;
+
+ constructor(public payload: ProductRoutePayload) { }
+}
+
+export class CreateProductSuccessAction implements Action {
+ readonly type = CREATE_SUCCESS;
+
+ constructor(public payload: CreateResourceSuccessPayload) { }
+}
+
+export class CreateProductFailAction implements Action {
+ readonly type = CREATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class UpdateProductAction implements Action {
+ readonly type = UPDATE;
+
+ constructor(public payload: ProductRoutePayload) { }
+}
+
+export class UpdateProductSuccessAction implements Action {
+ readonly type = UPDATE_SUCCESS;
+
+ constructor(public payload: UpdateResourceSuccessPayload) { }
+}
+
+export class UpdateProductFailAction implements Action {
+ readonly type = UPDATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class DeleteProductAction implements Action {
+ readonly type = DELETE;
+
+ constructor(public payload: ProductRoutePayload) { }
+}
+
+export class DeleteProductSuccessAction implements Action {
+ readonly type = DELETE_SUCCESS;
+
+ constructor(public payload: DeleteResourceSuccessPayload) { }
+}
+
+export class DeleteProductFailAction implements Action {
+ readonly type = DELETE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class EnableProductAction implements Action {
+ readonly type = ENABLE;
+
+ constructor(public payload: EnableProductPayload) { }
+}
+
+export class EnableProductSuccessAction implements Action {
+ readonly type = ENABLE_SUCCESS;
+
+ constructor(public payload: EnableProductPayload) { }
+}
+
+export class EnableProductFailAction implements Action {
+ readonly type = ENABLE_FAIL;
+
+ constructor(public payload: AccountAssignment[]) { }
+}
+
+export class ResetProductFormAction implements Action {
+ readonly type = RESET_FORM;
+
+ constructor() {}
+}
+
+export type Actions
+ = SearchAction
+ | SearchCompleteAction
+ | LoadAction
+ | SelectAction
+ | CreateProductAction
+ | CreateProductSuccessAction
+ | CreateProductFailAction
+ | UpdateProductAction
+ | UpdateProductSuccessAction
+ | UpdateProductFailAction
+ | DeleteProductAction
+ | DeleteProductSuccessAction
+ | DeleteProductFailAction
+ | EnableProductAction
+ | EnableProductSuccessAction
+ | EnableProductFailAction;
diff --git a/src/app/loans/products/store/products.reducer.ts b/src/app/loans/products/store/products.reducer.ts
new file mode 100644
index 0000000..b14430b
--- /dev/null
+++ b/src/app/loans/products/store/products.reducer.ts
@@ -0,0 +1,51 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as product from './product.actions';
+import {ResourceState} from '../../../common/store/resource.reducer';
+
+export const initialState: ResourceState = {
+ ids: [],
+ entities: {},
+ loadedAt: {},
+ selectedId: null,
+};
+
+export function reducer(state = initialState, action: product.Actions): ResourceState {
+ switch (action.type) {
+
+ case product.ENABLE_SUCCESS: {
+ const product = action.payload.product;
+
+ return {
+ ids: [ ...state.ids ],
+ entities: Object.assign({}, state.entities, {
+ [product.identifier]: Object.assign({}, product, {
+ enabled: action.payload.enable
+ })
+ }),
+ selectedId: state.selectedId,
+ loadedAt: state.loadedAt
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
diff --git a/src/app/loans/products/store/ranges/effects/notification.effects.ts b/src/app/loans/products/store/ranges/effects/notification.effects.ts
new file mode 100644
index 0000000..606d05d
--- /dev/null
+++ b/src/app/loans/products/store/ranges/effects/notification.effects.ts
@@ -0,0 +1,46 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {NotificationService, NotificationType} from '../../../../../services/notification/notification.service';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import {RangeActions} from '../range.actions';
+
+@Injectable()
+export class ProductChargeRangesNotificationEffects {
+
+ @Effect({ dispatch: false })
+ createRangeSuccess$: Observable<Action> = this.actions$
+ .ofType(RangeActions.CREATE_SUCCESS, RangeActions.UPDATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Range is going to be saved'
+ }));
+
+ @Effect({ dispatch: false })
+ deleteProductSuccess$: Observable<Action> = this.actions$
+ .ofType(RangeActions.DELETE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Range is going to be deleted'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) { }
+}
diff --git a/src/app/loans/products/store/ranges/effects/route.effects.ts b/src/app/loans/products/store/ranges/effects/route.effects.ts
new file mode 100644
index 0000000..dc8d53d
--- /dev/null
+++ b/src/app/loans/products/store/ranges/effects/route.effects.ts
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Router} from '@angular/router';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import {RangeActions} from '../range.actions';
+
+@Injectable()
+export class ProductChargeRangesRouteEffects {
+
+ @Effect({ dispatch: false })
+ createRangeSuccess$: Observable<Action> = this.actions$
+ .ofType(RangeActions.CREATE_SUCCESS, RangeActions.UPDATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.data.activatedRoute} ));
+
+ @Effect({ dispatch: false })
+ deleteRangeSuccess$: Observable<Action> = this.actions$
+ .ofType(RangeActions.DELETE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../../../../../../../'], { relativeTo: payload.data.activatedRoute} ));
+
+ constructor(private actions$: Actions, private router: Router) { }
+}
diff --git a/src/app/loans/products/store/ranges/effects/service.effects.ts b/src/app/loans/products/store/ranges/effects/service.effects.ts
new file mode 100644
index 0000000..55c8c7d
--- /dev/null
+++ b/src/app/loans/products/store/ranges/effects/service.effects.ts
@@ -0,0 +1,94 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+import * as resourceActions from '../../../../../common/store/action-creator/actions';
+import {PortfolioService} from '../../../../../services/portfolio/portfolio.service';
+import {RangeActions} from '../range.actions';
+import {FimsRange} from '../../../../../services/portfolio/domain/range-model';
+
+@Injectable()
+export class ProductChargeRangesApiEffects {
+
+ @Effect()
+ loadAll$: Observable<Action> = this.actions$
+ .ofType(RangeActions.LOAD_ALL)
+ .debounceTime(300)
+ .map((action: resourceActions.LoadAllAction) => action.payload)
+ .switchMap(id => {
+ const nextSearch$ = this.actions$.ofType(RangeActions.LOAD_ALL).skip(1);
+
+ return this.portfolioService.findAllRanges(id)
+ .takeUntil(nextSearch$)
+ .map(resources => RangeActions.loadAllCompleteAction({
+ resources
+ }))
+ .catch(() => of(RangeActions.loadAllCompleteAction({
+ resources: []
+ })));
+ });
+
+ @Effect()
+ createRange$: Observable<Action> = this.actions$
+ .ofType(RangeActions.CREATE)
+ .map((action: resourceActions.ResourceAction<FimsRange>) => action.payload)
+ .mergeMap(payload =>
+ this.portfolioService.createRange(payload.data.productIdentifier, payload.resource)
+ .map(() => RangeActions.createSuccessAction(payload))
+ .catch((error) => of(RangeActions.createFailAction({
+ resource: payload.resource,
+ data: payload.data,
+ error
+ })))
+ );
+
+ @Effect()
+ updateRange$: Observable<Action> = this.actions$
+ .ofType(RangeActions.UPDATE)
+ .map((action: resourceActions.ResourceAction<FimsRange>) => action.payload)
+ .mergeMap(payload =>
+ this.portfolioService.changeRange(payload.data.productIdentifier, payload.resource)
+ .map(() => RangeActions.updateSuccessAction(payload))
+ .catch((error) => of(RangeActions.updateFailAction({
+ resource: payload.resource,
+ data: payload.data,
+ error
+ })))
+ );
+
+ @Effect()
+ deleteRange$: Observable<Action> = this.actions$
+ .ofType(RangeActions.DELETE)
+ .map((action: resourceActions.ResourceAction<FimsRange>) => action.payload)
+ .mergeMap(payload =>
+ this.portfolioService.deleteRange(payload.data.productIdentifier, payload.resource.identifier)
+ .map(() => RangeActions.deleteSuccessAction(payload))
+ .catch((error) => of(RangeActions.deleteFailAction({
+ resource: payload.resource,
+ data: payload.data,
+ error
+ })))
+ );
+
+ constructor(private actions$: Actions, private portfolioService: PortfolioService) { }
+
+}
diff --git a/src/app/loans/products/store/ranges/range.actions.ts b/src/app/loans/products/store/ranges/range.actions.ts
new file mode 100644
index 0000000..a39fb69
--- /dev/null
+++ b/src/app/loans/products/store/ranges/range.actions.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {createResourceActions} from '../../../../common/store/action-creator/action-creator';
+import {FimsRange} from '../../../../services/portfolio/domain/range-model';
+
+export const RangeActions = createResourceActions<FimsRange>('Product Charge Range');
diff --git a/src/app/loans/products/store/ranges/ranges.reducer.ts b/src/app/loans/products/store/ranges/ranges.reducer.ts
new file mode 100644
index 0000000..a7a7824
--- /dev/null
+++ b/src/app/loans/products/store/ranges/ranges.reducer.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ResourceState} from '../../../../common/store/resource.reducer';
+import {RangeActions} from './range.actions';
+import {idsToHashWithCurrentTimestamp, resourcesToHash} from '../../../../common/store/reducer.helper';
+import {Actions} from '../../../../common/store/action-creator/action-creator';
+import {FimsRange} from '../../../../services/portfolio/domain/range-model';
+
+
+export interface State extends ResourceState {
+ ids: string[];
+ entities: { [id: string]: FimsRange };
+ selectedId: string | null;
+}
+
+export const initialState: State = {
+ ids: [],
+ entities: {},
+ loadedAt: {},
+ selectedId: null,
+};
+
+export function reducer(state = initialState, action: Actions<FimsRange>): ResourceState {
+
+ switch (action.type) {
+
+ case RangeActions.LOAD_ALL: {
+ return initialState;
+ }
+
+ case RangeActions.LOAD_ALL_COMPLETE: {
+ const ranges: FimsRange[] = action.payload.resources;
+
+ const ids = ranges.map(chargeDefinition => chargeDefinition.identifier);
+
+ const entities = resourcesToHash(ranges);
+
+ const loadedAt = idsToHashWithCurrentTimestamp(ids);
+
+ return {
+ ids: [ ...ids ],
+ entities: entities,
+ loadedAt: loadedAt,
+ selectedId: state.selectedId
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
diff --git a/src/app/loans/products/store/tasks/effects/notification.effects.ts b/src/app/loans/products/store/tasks/effects/notification.effects.ts
new file mode 100644
index 0000000..3b798d5
--- /dev/null
+++ b/src/app/loans/products/store/tasks/effects/notification.effects.ts
@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {NotificationService, NotificationType} from '../../../../../services/notification/notification.service';
+import {Action} from '@ngrx/store';
+import {Observable} from 'rxjs/Observable';
+import {Actions, Effect} from '@ngrx/effects';
+import {Injectable} from '@angular/core';
+import * as taskActions from '../task.actions';
+
+@Injectable()
+export class ProductTasksNotificationEffects {
+
+ @Effect({dispatch: false})
+ createUpdateCustomerTaskSuccess$: Observable<Action> = this.actions$
+ .ofType(taskActions.CREATE_SUCCESS, taskActions.UPDATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Task is going to be created'
+ }));
+
+ @Effect({dispatch: false})
+ deleteCustomerTaskSuccess$: Observable<Action> = this.actions$
+ .ofType(taskActions.DELETE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Task is going to be deleted'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+}
+
diff --git a/src/app/loans/products/store/tasks/effects/route.effects.ts b/src/app/loans/products/store/tasks/effects/route.effects.ts
new file mode 100644
index 0000000..2a64003
--- /dev/null
+++ b/src/app/loans/products/store/tasks/effects/route.effects.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as taskActions from '../task.actions';
+import {Router} from '@angular/router';
+
+@Injectable()
+export class ProductTasksRouteEffects {
+
+ @Effect({ dispatch: false })
+ createUpdateProductTaskSuccess$: Observable<Action> = this.actions$
+ .ofType(taskActions.CREATE_SUCCESS, taskActions.UPDATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute }));
+
+ @Effect({ dispatch: false })
+ deleteProductTaskSuccess$: Observable<Action> = this.actions$
+ .ofType(taskActions.DELETE)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../../'], { relativeTo: payload.activatedRoute }));
+
+ constructor(private actions$: Actions, private router: Router) { }
+
+}
diff --git a/src/app/loans/products/store/tasks/effects/service.effects.ts b/src/app/loans/products/store/tasks/effects/service.effects.ts
new file mode 100644
index 0000000..b368e3d
--- /dev/null
+++ b/src/app/loans/products/store/tasks/effects/service.effects.ts
@@ -0,0 +1,84 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+import * as taskActions from '../task.actions';
+import {PortfolioService} from '../../../../../services/portfolio/portfolio.service';
+
+@Injectable()
+export class ProductTasksApiEffects {
+
+ @Effect()
+ loadAll$: Observable<Action> = this.actions$
+ .ofType(taskActions.LOAD_ALL)
+ .debounceTime(300)
+ .map((action: taskActions.LoadAllAction) => action.payload)
+ .switchMap(id => {
+ const nextSearch$ = this.actions$.ofType(taskActions.LOAD_ALL).skip(1);
+
+ return this.portfolioService.findAllTaskDefinitionsForProduct(id)
+ .takeUntil(nextSearch$)
+ .map(taskDefinitions => new taskActions.LoadAllCompleteAction(taskDefinitions))
+ .catch(() => of(new taskActions.LoadAllCompleteAction([])));
+ });
+
+ @Effect()
+ createTask$: Observable<Action> = this.actions$
+ .ofType(taskActions.CREATE)
+ .map((action: taskActions.CreateTaskAction) => action.payload)
+ .mergeMap(payload =>
+ this.portfolioService.createTaskDefinition(payload.productId, payload.task)
+ .map(() => new taskActions.CreateTaskSuccessAction({
+ resource: payload.task,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new taskActions.CreateTaskFailAction(error)))
+ );
+
+ @Effect()
+ updateTask$: Observable<Action> = this.actions$
+ .ofType(taskActions.UPDATE)
+ .map((action: taskActions.UpdateTaskAction) => action.payload)
+ .mergeMap(payload =>
+ this.portfolioService.changeTaskDefinition(payload.productId, payload.task)
+ .map(() => new taskActions.UpdateTaskSuccessAction({
+ resource: payload.task,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new taskActions.UpdateTaskFailAction(error)))
+ );
+
+ @Effect()
+ deleteTask$: Observable<Action> = this.actions$
+ .ofType(taskActions.DELETE)
+ .map((action: taskActions.DeleteTaskAction) => action.payload)
+ .mergeMap(payload =>
+ this.portfolioService.deleteTaskDefinition(payload.productId, payload.task.identifier)
+ .map(() => new taskActions.DeleteTaskSuccessAction({
+ resource: payload.task,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new taskActions.DeleteTaskFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private portfolioService: PortfolioService) { }
+}
diff --git a/src/app/loans/products/store/tasks/task.actions.ts b/src/app/loans/products/store/tasks/task.actions.ts
new file mode 100644
index 0000000..3a8ae76
--- /dev/null
+++ b/src/app/loans/products/store/tasks/task.actions.ts
@@ -0,0 +1,155 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Action} from '@ngrx/store';
+import {Error} from '../../../../services/domain/error.model';
+import {type} from '../../../../store/util';
+import {TaskDefinition} from '../../../../services/portfolio/domain/task-definition.model';
+import {RoutePayload} from '../../../../common/store/route-payload';
+import {
+ CreateResourceSuccessPayload,
+ DeleteResourceSuccessPayload,
+ LoadResourcePayload,
+ SelectResourcePayload,
+ UpdateResourceSuccessPayload
+} from '../../../../common/store/resource.reducer';
+
+export const LOAD_ALL = type('[Product Task] Load All');
+export const LOAD_ALL_COMPLETE = type('[Product Task] Load All Complete');
+
+export const LOAD = type('[Product Task] Load');
+export const SELECT = type('[Product Task] Select');
+
+export const CREATE = type('[Product Task] Create');
+export const CREATE_SUCCESS = type('[Product Task] Create Success');
+export const CREATE_FAIL = type('[Product Task] Create Fail');
+
+export const UPDATE = type('[Product Task] Update');
+export const UPDATE_SUCCESS = type('[Product Task] Update Success');
+export const UPDATE_FAIL = type('[Product Task] Update Fail');
+
+export const DELETE = type('[Product Task] Delete');
+export const DELETE_SUCCESS = type('[Product Task] Delete Success');
+export const DELETE_FAIL = type('[Product Task] Delete Fail');
+
+export const RESET_FORM = type('[Product Task] Reset Form');
+
+export interface TaskRoutePayload extends RoutePayload {
+ productId: string;
+ task: TaskDefinition;
+}
+
+export class LoadAllAction implements Action {
+ readonly type = LOAD_ALL;
+
+ constructor(public payload: string) { }
+}
+
+export class LoadAllCompleteAction implements Action {
+ readonly type = LOAD_ALL_COMPLETE;
+
+ constructor(public payload: TaskDefinition[]) { }
+}
+
+export class LoadAction implements Action {
+ readonly type = LOAD;
+
+ constructor(public payload: LoadResourcePayload) { }
+}
+
+export class SelectAction implements Action {
+ readonly type = SELECT;
+
+ constructor(public payload: SelectResourcePayload) { }
+}
+
+export class CreateTaskAction implements Action {
+ readonly type = CREATE;
+
+ constructor(public payload: TaskRoutePayload) { }
+}
+
+export class CreateTaskSuccessAction implements Action {
+ readonly type = CREATE_SUCCESS;
+
+ constructor(public payload: CreateResourceSuccessPayload) { }
+}
+
+export class CreateTaskFailAction implements Action {
+ readonly type = CREATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class UpdateTaskAction implements Action {
+ readonly type = UPDATE;
+
+ constructor(public payload: TaskRoutePayload) { }
+}
+
+export class UpdateTaskSuccessAction implements Action {
+ readonly type = UPDATE_SUCCESS;
+
+ constructor(public payload: UpdateResourceSuccessPayload) { }
+}
+
+export class UpdateTaskFailAction implements Action {
+ readonly type = UPDATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class DeleteTaskAction implements Action {
+ readonly type = DELETE;
+
+ constructor(public payload: TaskRoutePayload) { }
+}
+
+export class DeleteTaskSuccessAction implements Action {
+ readonly type = DELETE_SUCCESS;
+
+ constructor(public payload: DeleteResourceSuccessPayload) { }
+}
+
+export class DeleteTaskFailAction implements Action {
+ readonly type = DELETE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class ResetTaskFormAction implements Action {
+ readonly type = RESET_FORM;
+
+ constructor() {}
+}
+
+export type Actions
+ = LoadAllAction
+ | LoadAllCompleteAction
+ | LoadAction
+ | SelectAction
+ | CreateTaskAction
+ | CreateTaskSuccessAction
+ | CreateTaskFailAction
+ | UpdateTaskAction
+ | UpdateTaskSuccessAction
+ | UpdateTaskFailAction
+ | DeleteTaskAction
+ | DeleteTaskSuccessAction
+ | DeleteTaskFailAction
+ | ResetTaskFormAction;
diff --git a/src/app/loans/products/store/tasks/tasks.reducer.ts b/src/app/loans/products/store/tasks/tasks.reducer.ts
new file mode 100644
index 0000000..e9cee51
--- /dev/null
+++ b/src/app/loans/products/store/tasks/tasks.reducer.ts
@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as task from './task.actions';
+import {TaskDefinition} from '../../../../services/portfolio/domain/task-definition.model';
+import {ResourceState} from '../../../../common/store/resource.reducer';
+import {idsToHashWithCurrentTimestamp, resourcesToHash} from '../../../../common/store/reducer.helper';
+
+export const initialState: ResourceState = {
+ ids: [],
+ entities: {},
+ loadedAt: {},
+ selectedId: null
+};
+
+export function reducer(state = initialState, action: task.Actions): ResourceState {
+
+ switch (action.type) {
+
+ case task.LOAD_ALL: {
+ return initialState;
+ }
+
+ case task.LOAD_ALL_COMPLETE: {
+ const taskDefinitions: TaskDefinition[] = action.payload;
+
+ const ids = taskDefinitions.map(task => task.identifier);
+
+ const entities = resourcesToHash(taskDefinitions);
+
+ const loadedAt = idsToHashWithCurrentTimestamp(ids);
+
+ return {
+ ids: [ ...ids ],
+ entities: entities,
+ loadedAt: loadedAt,
+ selectedId: state.selectedId
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
diff --git a/src/app/login/login.component.html b/src/app/login/login.component.html
index 4560c05..8aea8b0 100644
--- a/src/app/login/login.component.html
+++ b/src/app/login/login.component.html
@@ -1,3 +1,20 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
<div class="main-background" style="background-image:url('assets/Background.png');">
<div style="margin-top:5%; margin-left:25%;">
<div class="welcome">
diff --git a/src/app/login/login.component.spec.ts b/src/app/login/login.component.spec.ts
index aa23f4f..e3c8fb5 100644
--- a/src/app/login/login.component.spec.ts
+++ b/src/app/login/login.component.spec.ts
@@ -1,3 +1,21 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing';
import {LoginComponent} from './login.component';
import {Observable} from 'rxjs/Observable';
@@ -146,4 +164,3 @@
})));
});
-
diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts
index 6b92113..69e5038 100644
--- a/src/app/login/login.component.ts
+++ b/src/app/login/login.component.ts
@@ -1,24 +1,40 @@
-import { Component,OnDestroy, OnInit } from '@angular/core';
-import { FormGroup, FormBuilder, Validators } from '@angular/forms';
-import {Router } from '@angular/router'
-import { Subscription, Observable } from 'rxjs';
-import { TdLoadingService, LoadingType } from '@covalent/core';
-import { Store } from '@ngrx/store';
-import { ITdLoadingConfig } from '@covalent/core';
-import { LOGIN } from '../store/security/security.actions';
-import { MatSelectChange } from '@angular/material';
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {ITdLoadingConfig, LoadingType, TdLoadingService} from '@covalent/core';
+import {TranslateService} from '@ngx-translate/core';
+import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import * as fromRoot from '../store';
-import { TranslateService } from '@ngx-translate/core';
-import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {Store} from '@ngrx/store';
+import {LOGIN} from '../store/security/security.actions';
+import {Subscription} from 'rxjs/Subscription';
import {TRANSLATE_STORAGE_KEY} from '../common/i18n/translate';
+import {Observable} from 'rxjs/Observable';
+import {MatSelectChange} from '@angular/material';
@Component({
- selector: 'app-login',
+ selector: 'fims-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'],
-
})
-export class LoginComponent implements OnInit {
+export class LoginComponent implements OnInit, OnDestroy {
+
private loadingSubscription: Subscription;
currentLanguage: string;
@@ -27,9 +43,8 @@
{id: 'en', label: 'Welcome to fims'},
{id: 'es', label: 'Bienvenido a fims'}
];
-
- hide= true;
- form: FormGroup;
+
+ form: FormGroup;
error$: Observable<string>;
diff --git a/src/app/login/login.module.ts b/src/app/login/login.module.ts
new file mode 100644
index 0000000..9d82311
--- /dev/null
+++ b/src/app/login/login.module.ts
@@ -0,0 +1,49 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {NgModule} from '@angular/core';
+import {LoginComponent} from './login.component';
+import {LoginRoutes} from './login.routing';
+import {RouterModule} from '@angular/router';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {MatButtonModule, MatCardModule, MatIconModule, MatInputModule, MatSelectModule, MatTooltipModule, MatFormFieldModule} from '@angular/material';
+import {CommonModule} from '@angular/common';
+import {TranslateModule} from '@ngx-translate/core';
+import {CovalentLoadingModule} from '@covalent/core';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(LoginRoutes),
+ TranslateModule,
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ MatIconModule,
+ MatCardModule,
+ MatInputModule,
+ MatButtonModule,
+ MatSelectModule,
+ MatTooltipModule,
+ MatFormFieldModule,
+ CovalentLoadingModule
+ ],
+ declarations: [
+ LoginComponent
+ ]
+})
+export class LoginModule {}
diff --git a/src/app/login/login.routing.ts b/src/app/login/login.routing.ts
new file mode 100644
index 0000000..2bfd0b1
--- /dev/null
+++ b/src/app/login/login.routing.ts
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Routes} from '@angular/router';
+import {LoginComponent} from './login.component';
+
+export const LoginRoutes: Routes = [
+ {
+ path: '',
+ component: LoginComponent,
+ pathMatch: 'full'
+ }
+];
diff --git a/src/app/main/access.denied.component.html b/src/app/main/access.denied.component.html
new file mode 100644
index 0000000..052566f
--- /dev/null
+++ b/src/app/main/access.denied.component.html
@@ -0,0 +1,20 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<div style="padding: 20%;text-align:center;" translate>
+ Sorry, but you are not allowed to see this page.
+</div>
diff --git a/src/app/main/access.denied.component.ts b/src/app/main/access.denied.component.ts
new file mode 100644
index 0000000..144a4ee
--- /dev/null
+++ b/src/app/main/access.denied.component.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component} from '@angular/core';
+
+@Component({
+ templateUrl: './access.denied.component.html'
+})
+export class AccessDeniedComponent {}
diff --git a/src/app/main/main.component.html b/src/app/main/main.component.html
new file mode 100644
index 0000000..ed0ab27
--- /dev/null
+++ b/src/app/main/main.component.html
@@ -0,0 +1,140 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+ <div class="flex-container" fullscreen>
+ <mat-progress-bar *ngIf="isLoading$ | async" mode="indeterminate"></mat-progress-bar>
+ <mat-toolbar class="my-toolbar">
+ <div>
+ <div class="search2">
+ <ul id="menu" class="menu1">
+ <li>
+ <button mat-icon-button (click)="sidenav.toggle()" class="dropdown-button">
+ <mat-icon class="fineract-toolicon">menu</mat-icon>
+ </button>
+ </li>
+ <li>
+
+ <span style="color:purple">{{tenant$ | async}}</span>
+ </li>
+ <li>
+
+ <button mat-button [matMenuTriggerFor]="client" class="dropdown-button">
+ <mat-icon>group</mat-icon>Clients
+ <mat-icon>arrow_drop_down</mat-icon>
+ </button>
+ <mat-menu #client="matMenu">
+ <a mat-menu-item [routerLink]="['/customers']">Client</a>
+ <a mat-menu-item [routerLink]="['/']">Group</a>
+ <a mat-menu-item [routerLink]="['/']">Center</a>
+ </mat-menu>
+ </li>
+ <li>
+ <button mat-button [routerLink]="['/accounting']" class="dropdown-button">Accounting
+ </button>
+ </li>
+ <li>
+ <button mat-button [matMenuTriggerFor]="report" class="dropdown-button">Reports
+ <mat-icon>arrow_drop_down</mat-icon>
+ </button>
+ <mat-menu #report="matMenu">
+ <a mat-menu-item [routerLink]="['/reporting']">Member</a>
+ <a mat-menu-item [routerLink]="['/reporting/deposits']">Deposit</a>
+
+ </mat-menu>
+ </li>
+ <li>
+
+ <button mat-button [matMenuTriggerFor]="admin" class="dropdown-button">
+ <mat-icon>build</mat-icon>Admin
+ <mat-icon>arrow_drop_down</mat-icon>
+ </button>
+ <mat-menu #admin="matMenu">
+ <button mat-menu-item [routerLink]="['/employees']">Users</button>
+ <button mat-menu-item [routerLink]="['/teller']">Teller</button>
+ <button mat-menu-item [routerLink]="['/roles']">Roles/Permissions</button>
+ <button mat-menu-item [routerLink]="['/navbar/admin/products']">Products</button>
+
+ </mat-menu>
+ </li>
+
+ </ul>
+ </div>
+ <div class="search1">
+ <ul id="menu">
+ <li>
+ <input matInput placeholder="click or press alt + X to Search" type="text" class="fineract-toolsearch">
+ </li>
+
+ <li class="dropdown-button">
+ <button mat-icon-button [routerLink]="['/navbar/notification']">
+ <mat-icon>notifications</mat-icon>
+ </button>
+ </li>
+ <li>
+ <button mat-button [matMenuTriggerFor]="mifos" class="dropdown-button">Fineract CN
+ <mat-icon>arrow_drop_down</mat-icon>
+ </button>
+ <mat-menu #mifos="matMenu">
+ <button mat-menu-item [disabled]="true">
+ <mat-icon>account_circle</mat-icon>
+ <span>{{username$ | async}}</span>
+ </button>
+ <hr>
+ <button mat-menu-item (click)="goToSettings()">
+ <mat-icon>settings</mat-icon>
+ <span>{{'Settings' | translate}}</span>
+ </button>
+ <button mat-menu-item (click)="logout()">
+ <mat-icon>exit_to_app</mat-icon>
+ <span>{{'Sign Out' | translate}}</span>
+ </button>
+ </mat-menu>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </mat-toolbar>
+ </div>
+
+ <mat-sidenav-container class="app-container" style="background-image:url('assets/Background.png');">
+ <mat-sidenav #sidenav class="app-sidenav mat-elevation-z2" mode="side" opened="true">
+
+ <mat-nav-list>
+ <ng-container *ngFor="let menuItem of menuItems; let i = index">
+ <a mat-list-item [routerLink]="[menuItem.routerLink]" routerLinkActive="active" *hasPermission="menuItem.permission">
+ <mat-icon matListAvatar>{{menuItem.icon}}</mat-icon>
+ <h3 matLine>{{menuItem.title | translate}}</h3>
+ <p matLine>{{menuItem.description | translate}}</p>
+ </a>
+ <mat-divider *ngIf="i === 0"></mat-divider>
+ </ng-container>
+ </mat-nav-list>
+
+ </mat-sidenav>
+ <div class="app-sidenav-content">
+ <div class="wrapper" >
+ <router-outlet></router-outlet>
+ </div>
+
+ </div>
+ <fims-notification></fims-notification>
+ </mat-sidenav-container>
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/main/main.component.scss b/src/app/main/main.component.scss
new file mode 100644
index 0000000..94b91a5
--- /dev/null
+++ b/src/app/main/main.component.scss
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+ .dropdown-button{
+
+ color:#3498dB;
+}
+::placeholder{
+ color: #3498dB;
+ font-size: 12px;
+}
+.app-container {
+ flex: 1;
+ width: 100%;
+ height:100%;
+ min-width: 100%;
+ position:fixed;
+}
+.my-toolbar{
+ height:20px;
+ background-color: #363636;
+ opacity:100%;
+}
+
+.app-sidenav-content{
+ display:flex;
+ min-height:100%;
+
+ flex-direction:column;
+ opacity: 40%;
+
+}
+.app-sidenav{
+ display:flex;
+ height:100%;
+ flex-direction:column;
+
+ opacity:40%;
+
+
+}
+mat-list > a{
+
+ color:white;
+}
+.fineract-toolicon{
+ color:white;
+}
+.wrapper{
+ margin:50px;
+
+}
+ul#menu li{
+ display: inline;
+}
+.search1{
+
+ margin-left:8%;
+
+}
+.fineract-toolsearch{
+ width:230px;
+ max-height: 10px;
+}
+
+
+.fineract-logo{
+ width:40px;
+ height:37px;
+ vertical-align: bottom;
+
+}
+.logo-title{
+ vertical-align: middle;
+ font-size:11px;
+ color:white;
+}
+
+.search1,.search2{
+ display: inline-block;
+}
+
+.menu1{
+ padding:0;
+}
+
+.test{
+ padding-left:0;
+ padding-right: 0;
+}
+.flex-container{
+ height: 45px;
+}
diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts
new file mode 100644
index 0000000..cbe9a42
--- /dev/null
+++ b/src/app/main/main.component.ts
@@ -0,0 +1,192 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {ActivatedRoute, NavigationEnd, Router, RouterState} from '@angular/router';
+import {Title} from '@angular/platform-browser';
+import {Action, HttpClient} from '../services/http/http.service';
+import {Store} from '@ngrx/store';
+import * as fromRoot from '../store';
+import {LOGOUT} from '../store/security/security.actions';
+import {Observable} from 'rxjs/Observable';
+import {FimsPermission} from '../services/security/authz/fims-permission.model';
+import {CountryService} from '../services/country/country.service';
+import {TdMediaService} from '@covalent/core';
+import {Subscription} from 'rxjs/Subscription';
+import {MatSidenav} from '@angular/material';
+
+interface MenuItem {
+ permission?: FimsPermission;
+ icon: string;
+ title: string;
+ description?: string;
+ routerLink: string;
+}
+
+@Component({
+ selector: 'fims-main',
+ templateUrl: './main.component.html',
+ styleUrls: ['main.component.scss']
+})
+export class MainComponent implements OnInit, OnDestroy, AfterViewInit {
+
+ private routerEventSubscription: Subscription;
+
+ @ViewChild(MatSidenav) sidenav: MatSidenav;
+
+ menuItems: MenuItem[] = [
+ {title: 'Quick access', icon: 'dashboard', routerLink: '/quickAccess'},
+ {
+ title: 'Offices',
+ description: 'Manage offices',
+ icon: 'store',
+ routerLink: '/offices',
+ permission: {id: 'office_offices', accessLevel: 'READ'}
+ },
+ {
+ title: 'Roles/Permissions',
+ description: 'Manage roles and permissions',
+ icon: 'https',
+ routerLink: '/roles',
+ permission: {id: 'identity_roles', accessLevel: 'READ'}
+ },
+ {
+ title: 'Employees',
+ description: 'Manage employees',
+ icon: 'group',
+ routerLink: '/employees',
+ permission: {id: 'office_employees', accessLevel: 'READ'}
+ },
+ {
+ title: 'Accounting',
+ description: 'Manage ledger accounts',
+ icon: 'receipt',
+ routerLink: '/accounting',
+ permission: {id: 'accounting_ledgers', accessLevel: 'READ'}
+ },
+ {
+ title: 'Member',
+ description: 'Manage members',
+ icon: 'face',
+ routerLink: '/customers',
+ permission: {id: 'customer_customers', accessLevel: 'READ'}
+ },
+ {
+ title: 'Loan products',
+ description: 'Manage loan products',
+ icon: 'credit_card',
+ routerLink: '/loans',
+ permission: {id: 'portfolio_products', accessLevel: 'READ'}
+ },
+ {
+ title: 'Deposit',
+ description: 'Account management',
+ icon: 'attach_money',
+ routerLink: '/deposits',
+ permission: {id: 'deposit_definitions', accessLevel: 'READ'}
+ },
+ {
+ title: 'Teller',
+ description: 'Teller management',
+ icon: 'person',
+ routerLink: '/teller',
+ permission: {id: 'teller_operations', accessLevel: 'READ'}
+ },
+ {
+ title: 'Reports',
+ description: 'View reports',
+ icon: 'show_chart',
+ routerLink: '/reports',
+ permission: {id: 'reporting_management', accessLevel: 'READ'}
+ },
+ {
+ title: 'Collection Sheet',
+ description: 'View collection sheet',
+ icon: 'event_note',
+ routerLink: '/collection',
+ permission: {id: 'accounting_ledgers', accessLevel: 'READ'}
+
+ },
+ ];
+
+ isLoading$: Observable<boolean>;
+
+ isOpened$: Observable<boolean>;
+
+ tenant$: Observable<string>;
+
+ username$: Observable<string>;
+
+ constructor(private router: Router, private titleService: Title, private httpClient: HttpClient, private countryService: CountryService,
+ private store: Store<fromRoot.State>, private media: TdMediaService) {}
+
+ ngOnInit(): void {
+ this.routerEventSubscription = this.router.events
+ .filter(event => event instanceof NavigationEnd)
+ .map(() => this.getTitle(this.router.routerState, this.router.routerState.root).join(' - '))
+ .subscribe(title => this.titleService.setTitle(title));
+
+ this.tenant$ = this.store.select(fromRoot.getTenant);
+ this.username$ = this.store.select(fromRoot.getUsername);
+ this.isOpened$ = this.media.registerQuery('gt-md');
+
+ this.countryService.init();
+ }
+
+ ngOnDestroy(): void {
+ this.routerEventSubscription.unsubscribe();
+ }
+
+ ngAfterViewInit(): void {
+ this.isLoading$ = this.httpClient.process
+ .debounceTime(1000)
+ .map((action: Action) => action === Action.QueryStart);
+
+ this.media.broadcast();
+ }
+
+ getTitle(state: RouterState, parent: ActivatedRoute): string[] {
+ const data = [];
+
+ if (parent && parent.snapshot.data) {
+ const dataProperty: any = parent.snapshot.data;
+
+ if (dataProperty.title) {
+ data.push(dataProperty.title);
+ }
+ }
+
+ if (state && parent) {
+ data.push(... this.getTitle(state, parent.firstChild));
+ }
+
+ return data;
+ }
+
+ toggleSideNav(): void {
+ this.sidenav.toggle(!this.sidenav.opened);
+ }
+
+ logout(): void {
+ this.store.dispatch({ type: LOGOUT });
+ }
+
+ goToSettings(): void {
+ this.router.navigate(['/user']);
+ }
+}
diff --git a/src/app/main/main.module.ts b/src/app/main/main.module.ts
new file mode 100644
index 0000000..dd6b56e
--- /dev/null
+++ b/src/app/main/main.module.ts
@@ -0,0 +1,73 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {NgModule} from '@angular/core';
+import {RouterModule} from '@angular/router';
+import {MainComponent} from './main.component';
+import {MainRoutes, mainRoutingProviders} from './main.routing';
+import {AccessDeniedComponent} from './access.denied.component';
+import {FimsSharedModule} from '../common/common.module';
+import {NotificationComponent} from './notification.component';
+import {QuickAccessComponent} from '../quickAccess/quick-access.component';
+import {
+ MatButtonModule,
+ MatCardModule,
+ MatIconModule,
+ MatListModule,
+ MatMenuModule,
+ MatProgressBarModule,
+ MatSidenavModule,
+ MatToolbarModule,
+ MatTooltipModule,
+ MatFormFieldModule
+} from '@angular/material';
+import {TranslateModule} from '@ngx-translate/core';
+import {CommonModule} from '@angular/common';
+import {CovalentLayoutModule, CovalentMediaModule} from '@covalent/core';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(MainRoutes),
+ FimsSharedModule,
+ TranslateModule,
+ CommonModule,
+ MatButtonModule,
+ MatToolbarModule,
+ MatSidenavModule,
+ MatListModule,
+ MatProgressBarModule,
+ MatIconModule,
+ MatMenuModule,
+ MatTooltipModule,
+ MatCardModule,
+ MatFormFieldModule,
+ CovalentLayoutModule,
+ CovalentMediaModule
+ ],
+ declarations: [
+ MainComponent,
+ QuickAccessComponent,
+ AccessDeniedComponent,
+ NotificationComponent
+ ],
+ providers: [
+ mainRoutingProviders
+ ],
+})
+export class MainModule {}
diff --git a/src/app/main/main.routing.ts b/src/app/main/main.routing.ts
new file mode 100644
index 0000000..1f81b96
--- /dev/null
+++ b/src/app/main/main.routing.ts
@@ -0,0 +1,53 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Routes} from '@angular/router';
+import {MainComponent} from './main.component';
+import {ChangePasswordGuard} from '../services/security/change.password.service';
+import {AccessDeniedComponent} from './access.denied.component';
+import {PermissionGuard} from '../services/security/authz/permission.guard';
+import {QuickAccessComponent} from '../quickAccess/quick-access.component';
+
+export const MainRoutes: Routes = [
+ {
+ path: '', component: MainComponent, canActivateChild: [ChangePasswordGuard, PermissionGuard], children: [
+ { path: '', redirectTo: '/quickAccess', pathMatch: 'full'},
+ { path: 'quickAccess', component: QuickAccessComponent, data: { title: 'Quick access' } },
+ { path: 'offices', loadChildren: './../offices/office.module#OfficeModule' },
+ { path: 'employees', loadChildren: './../employees/employee.module#EmployeeModule' },
+ { path: 'roles', loadChildren: './../roles/role.module#RoleModule' },
+ { path: 'user', loadChildren: './../user/user.module#UserModule' },
+ { path: 'customers', loadChildren: './../customers/customer.module#CustomerModule' },
+ { path: 'accounting', loadChildren: './../accounting/accounting.module#AccountingModule' },
+ { path: 'loans', loadChildren: './../loans/products/product.module#ProductModule' },
+ { path: 'deposits', loadChildren: './../depositAccount/deposit-account.module#DepositAccountModule' },
+ { path: 'teller', loadChildren: './../teller/teller.module#TellerModule' },
+ { path: 'reports', loadChildren: './../reporting/reporting.module#ReportingModule' },
+ { path: 'denied', component: AccessDeniedComponent, data: { title: 'Not allowed' } }
+ ]
+ },
+ {
+ path: 'changePassword', loadChildren: './../user/user.module#UserModule', data: { title: 'Change password' }
+ }
+
+];
+
+export const mainRoutingProviders: any[] = [
+ ChangePasswordGuard,
+ PermissionGuard
+];
diff --git a/src/app/main/notification.component.ts b/src/app/main/notification.component.ts
new file mode 100644
index 0000000..8abd7cf
--- /dev/null
+++ b/src/app/main/notification.component.ts
@@ -0,0 +1,114 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {AfterViewInit, Component, OnDestroy, OnInit, ViewContainerRef} from '@angular/core';
+import {NotificationEvent, NotificationService, NotificationType} from '../services/notification/notification.service';
+import {TranslateService} from '@ngx-translate/core';
+import {HttpClient} from '../services/http/http.service';
+import {TdDialogService} from '@covalent/core';
+import {Subscription} from 'rxjs/Subscription';
+import {MatSnackBar, MatSnackBarConfig, MatSnackBarRef} from '@angular/material';
+
+@Component({
+ selector: 'fims-notification',
+ template: ''
+})
+export class NotificationComponent implements OnInit, OnDestroy, AfterViewInit {
+
+ private notificationSubscription: Subscription;
+ private errorSubscription: Subscription;
+
+ constructor(private notificationService: NotificationService, private translate: TranslateService, private httpClient: HttpClient,
+ private snackBar: MatSnackBar, private viewContainerRef: ViewContainerRef, private dialogService: TdDialogService) {}
+
+ ngOnInit(): void {
+ const config: MatSnackBarConfig = new MatSnackBarConfig();
+ config.viewContainerRef = this.viewContainerRef;
+ this.notificationSubscription = this.notificationService.notifications$
+ .subscribe((notification: NotificationEvent) => this.showNotification(notification, config));
+ }
+
+ ngOnDestroy(): void {
+ this.notificationSubscription.unsubscribe();
+ this.errorSubscription.unsubscribe();
+ }
+
+ ngAfterViewInit(): void {
+ this.errorSubscription = this.httpClient.error.subscribe((error: any) => this.showError(error));
+ }
+
+ private showNotification(notification: NotificationEvent, config: MatSnackBarConfig): void {
+ switch (notification.type) {
+ case NotificationType.MESSAGE:
+ this.showMessage(notification.message, config);
+ break;
+ case NotificationType.ALERT:
+ this.showAlert(notification.title, notification.message);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private showError(error: any): void {
+ switch (error.status) {
+ case 400:
+ this.showAlert('Unexpected error',
+ 'We are very sorry, it seems the request you sent could not be accepted by our servers.'
+ );
+ break;
+ case 404:
+ this.showAlert('Resource not available',
+ 'It seems the resource you requested is either not available or you don\'t have the permission to access it.'
+ );
+ break;
+ case 504:
+ case 500:
+ this.showAlert('Service not available',
+ 'We are very sorry, it seems there is a problem with our servers. Please contact your administrator if the problem occurs.'
+ );
+ break;
+ default:
+ break;
+ }
+ }
+
+ private showMessage(message: string, config: MatSnackBarConfig): void {
+ this.translate.get(message)
+ .take(1)
+ .subscribe((result) => {
+ const snackBarRef: MatSnackBarRef<any> = this.snackBar.open(result, '', config);
+ setTimeout(() => snackBarRef.dismiss(), 3000);
+ });
+ }
+
+ private showAlert(title: string = '', message: string): void {
+ this.translate.get([title, message, 'OK'])
+ .take(1)
+ .subscribe((result) => {
+ this.dialogService.openAlert({
+ message: result[message],
+ viewContainerRef: this.viewContainerRef,
+ title: result[title],
+ closeButton: result['OK']
+ });
+ });
+
+ }
+}
diff --git a/src/app/offices/detail/office.detail.component.html b/src/app/offices/detail/office.detail.component.html
new file mode 100644
index 0000000..bb0e2cf
--- /dev/null
+++ b/src/app/offices/detail/office.detail.component.html
@@ -0,0 +1,73 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ng-container *ngIf="office$ | async as office">
+ <fims-layout-card-over title="{{office.parentIdentifier ? office.name : office.name + ' (Headquarter)'}}" subTitle="{{office.description}}"
+ [navigateBackTo]="office.parentIdentifier ? ['../../', this.office.parentIdentifier] : undefined">
+ <fims-layout-card-over-header-menu layout="row" layout-align="end center">
+ <td-search-box #searchBox placeholder="{{'Search' | translate}}" (search)="searchOffice($event)"
+ [alwaysVisible]="false"></td-search-box>
+ <a mat-icon-button [routerLink]="['edit']" *hasPermission="{ id: 'office_offices', accessLevel: 'CHANGE' }">
+ <mat-icon>mode_edit</mat-icon>
+ </a>
+ <button mat-icon-button (click)="deleteOffice(office)" title="{{'Delete this office' | translate}}"
+ *ngIf="canDelete$ | async">
+ <mat-icon>delete</mat-icon>
+ </button>
+ </fims-layout-card-over-header-menu>
+ <fims-two-column-layout>
+ <mat-nav-list left>
+ <h3 mat-subheader translate>Management</h3>
+ <a mat-list-item [routerLink]="['tellers']" *hasPermission="{ id: 'teller_management', accessLevel: 'READ'}">
+ <mat-icon matListAvatar>person</mat-icon>
+ <h3 matLine translate>Tellers</h3>
+ <p matLine translate>Manage tellers</p>
+ </a>
+ </mat-nav-list>
+ <ng-container right>
+ <mat-list>
+ <h3 mat-subheader translate>Address</h3>
+ <mat-list-item *ngIf="office.address">
+ <mat-icon matListAvatar>location_on</mat-icon>
+ <h3 matLine>{{office.address.street}}, {{office.address.postalCode}}, {{office.address.city}}<span *ngIf="office.address.region">({{office.address.region}})</span>
+ </h3>
+ <p matLine>{{office.address.country}}</p>
+ </mat-list-item>
+ <mat-list-item *ngIf="!office.address">
+ <mat-icon matListAvatar>location_on</mat-icon>
+ <h3 matLine translate>No address available</h3>
+ </mat-list-item>
+ <h3 mat-subheader translate>Branch offices</h3>
+ </mat-list>
+
+ <fims-data-table flex
+ (onFetch)="fetchBranches(office.identifier, $event)"
+ (onActionCellClick)="rowSelect($event)"
+ [columns]="columns"
+ [data]="branchData"
+ [sortable]="true"
+ [pageable]="true">
+ </fims-data-table>
+ </ng-container>
+ </fims-two-column-layout>
+ </fims-layout-card-over>
+ <a mat-fab color="accent" title="{{'Create branch office' | translate}}" class="mat-fab-position-bottom-right"
+ [routerLink]="['../../../create']"
+ [queryParams]="{ parentId: office.identifier }" *hasPermission="{ id: 'office_offices', accessLevel: 'CHANGE' }">
+ <mat-icon>add</mat-icon>
+ </a>
+</ng-container>
diff --git a/src/app/offices/detail/office.detail.component.ts b/src/app/offices/detail/office.detail.component.ts
new file mode 100644
index 0000000..78b568c
--- /dev/null
+++ b/src/app/offices/detail/office.detail.component.ts
@@ -0,0 +1,118 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ActivatedRoute, Router} from '@angular/router';
+import {Component, OnInit} from '@angular/core';
+import {OfficeService} from '../../services/office/office.service';
+import {Office} from '../../services/office/domain/office.model';
+import {FetchRequest} from '../../services/domain/paging/fetch-request.model';
+import {OfficePage} from '../../services/office/domain/office-page.model';
+import {Observable} from 'rxjs/Observable';
+import {TdDialogService} from '@covalent/core';
+import {TableData} from '../../common/data-table/data-table.component';
+import {DELETE} from '../store/office.actions';
+import {getSelectedOffice, OfficesStore} from '../store/index';
+import {FimsPermission} from '../../services/security/authz/fims-permission.model';
+import * as fromRoot from '../../store/index';
+
+@Component({
+ templateUrl: './office.detail.component.html'
+})
+export class OfficeDetailComponent implements OnInit {
+
+ office$: Observable<Office>;
+
+ canDelete$: Observable<boolean>;
+
+ branchData: TableData = {
+ totalElements: 0,
+ totalPages: 0,
+ data: []
+ };
+
+ columns: any[] = [
+ {name: 'identifier', label: 'Id'},
+ {name: 'name', label: 'Name'},
+ {name: 'description', label: 'Description'}
+ ];
+
+ constructor(private store: OfficesStore, private route: ActivatedRoute, private router: Router, private officeService: OfficeService,
+ private dialogService: TdDialogService) {
+ }
+
+ ngOnInit(): void {
+ this.office$ = this.store.select(getSelectedOffice)
+ .filter(office => !!office)
+ .do(office => this.fetchBranches(office.identifier));
+
+ this.canDelete$ = Observable.combineLatest(
+ this.store.select(fromRoot.getPermissions),
+ this.office$,
+ (permissions, office: Office) => ({
+ hasPermission: this.hasDeletePermission(permissions),
+ noExternalReferences: !office.externalReferences
+ }))
+ .map(result => result.hasPermission && result.noExternalReferences);
+ }
+
+ fetchBranches(identifier: string, fetchRequest?: FetchRequest): void {
+ this.officeService.listBranches(identifier, fetchRequest)
+ .subscribe((officePage: OfficePage) => {
+ this.branchData = {
+ data: officePage.offices,
+ totalElements: officePage.totalElements,
+ totalPages: officePage.totalPages
+ };
+ });
+ }
+
+ rowSelect(office: Office): void {
+ this.router.navigate(['../../', office.identifier], {relativeTo: this.route});
+ }
+
+ searchOffice(searchTerm: string): void {
+ this.router.navigate(['../../../'], {queryParams: {term: searchTerm}, relativeTo: this.route});
+ }
+
+ confirmDeletion(): Observable<boolean> {
+ return this.dialogService.openConfirm({
+ message: 'Do you want to delete this office?',
+ title: 'Confirm deletion',
+ acceptButton: 'DELETE OFFICE',
+ }).afterClosed();
+ }
+
+ deleteOffice(office: Office): void {
+ this.confirmDeletion()
+ .filter(accept => accept)
+ .subscribe(() => this.store.dispatch({
+ type: DELETE, payload: {
+ office,
+ activatedRoute: this.route
+ }
+ }));
+ }
+
+ private hasDeletePermission(permissions: FimsPermission[]): boolean {
+ return permissions.filter(permission =>
+ permission.id === 'office_offices' &&
+ permission.accessLevel === 'DELETE'
+ ).length > 0;
+ }
+
+}
diff --git a/src/app/offices/detail/office.index.component.html b/src/app/offices/detail/office.index.component.html
new file mode 100644
index 0000000..ca721b3
--- /dev/null
+++ b/src/app/offices/detail/office.index.component.html
@@ -0,0 +1,18 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<router-outlet></router-outlet>
diff --git a/src/app/offices/detail/office.index.component.ts b/src/app/offices/detail/office.index.component.ts
new file mode 100644
index 0000000..30c50e7
--- /dev/null
+++ b/src/app/offices/detail/office.index.component.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {SelectAction} from '../store/office.actions';
+import {OfficesStore} from '../store/index';
+import {ActivatedRoute} from '@angular/router';
+import {Subscription} from 'rxjs/Subscription';
+
+@Component({
+ templateUrl: './office.index.component.html',
+})
+export class OfficeIndexComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ constructor(private store: OfficesStore, private route: ActivatedRoute) {}
+
+ ngOnInit(): void {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectAction(params['id']))
+ .subscribe(this.store);
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ }
+}
diff --git a/src/app/offices/detail/teller/detail/balance/balance.component.html b/src/app/offices/detail/teller/detail/balance/balance.component.html
new file mode 100644
index 0000000..96d55b4
--- /dev/null
+++ b/src/app/offices/detail/teller/detail/balance/balance.component.html
@@ -0,0 +1,87 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Teller balance' | translate}}" [navigateBackTo]="['../']">
+ <div layout="row">
+ <table td-data-table *ngIf="balance$ | async as balance">
+ <thead>
+ <tr td-data-table-column-row>
+ <th td-data-table-column>
+ <span translate>Transaction date</span>
+ </th>
+ <th td-data-table-column>
+ <span translate>Message</span>
+ </th>
+ <th td-data-table-column>
+ <span translate>Cash Received</span>
+ </th>
+ <th td-data-table-column>
+ <span translate>Cash Disbursed</span>
+ </th>
+ <th td-data-table-column>
+ <span translate>Cheques Received</span>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr td-data-table-row *ngFor="let row of balance.entries">
+ <td td-data-table-cell>
+ {{row['transactionDate'] | date:'short' }}
+ </td>
+ <td td-data-table-cell>
+ {{row['message'] }}
+ </td>
+ <td td-data-table-cell>
+ {{row['type'] == 'DEBIT' ? (row['amount'] | number:numberFormat) : '-'}}
+ </td>
+ <td td-data-table-cell>
+ {{row['type'] == 'CREDIT' ? (row['amount'] | number:numberFormat): '-'}}
+ </td>
+ <td td-data-table-cell>
+ {{row['type'] == 'CHEQUE' ? (row['amount'] | number:numberFormat) : '-'}}
+ </td>
+ </tr>
+ <tr td-data-table-row>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell>
+ <b translate>Subtotal</b>
+ </td>
+ <td td-data-table-cell>
+ <b>{{balance.cashReceivedTotal | number:numberFormat}}</b>
+ </td>
+ <td td-data-table-cell>
+ <b>{{balance.cashDisbursedTotal | number:numberFormat}}</b>
+ </td>
+ <td td-data-table-cell>
+ <b>{{balance.chequesReceivedTotal | number:numberFormat}}</b>
+ </td>
+ </tr>
+ <tr td-data-table-row>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell>
+ <b translate>Cash on hand</b>
+ </td>
+ <td td-data-table-cell>
+ <b>{{balance.cashOnHand | number:numberFormat}}</b>
+ </td>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+</fims-layout-card-over>
diff --git a/src/app/offices/detail/teller/detail/balance/balance.component.ts b/src/app/offices/detail/teller/detail/balance/balance.component.ts
new file mode 100644
index 0000000..3b31642
--- /dev/null
+++ b/src/app/offices/detail/teller/detail/balance/balance.component.ts
@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import * as fromOffices from '../../../../store/index';
+import {OfficesStore} from '../../../../store/index';
+import {TellerBalance} from './services/teller-balance.model';
+import {BalanceSheetService} from './services/balance-sheet.service';
+
+@Component({
+ templateUrl: './balance.component.html'
+})
+export class TellerBalanceComponent implements OnInit {
+
+ numberFormat = '1.2-2';
+
+ balance$: Observable<TellerBalance>;
+
+ constructor(private balanceSheetService: BalanceSheetService, private store: OfficesStore) {}
+
+ ngOnInit(): void {
+ this.balance$ = Observable.combineLatest(
+ this.store.select(fromOffices.getSelectedTeller).filter(teller => !!teller),
+ this.store.select(fromOffices.getSelectedOffice).filter(office => !!office),
+ (teller, office) => ({
+ teller,
+ office
+ })
+ ).switchMap(result => this.balanceSheetService.getBalance(result.office.identifier, result.teller.code));
+ }
+}
diff --git a/src/app/offices/detail/teller/detail/balance/services/balance-sheet.service.ts b/src/app/offices/detail/teller/detail/balance/services/balance-sheet.service.ts
new file mode 100644
index 0000000..a063ae1
--- /dev/null
+++ b/src/app/offices/detail/teller/detail/balance/services/balance-sheet.service.ts
@@ -0,0 +1,54 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {TellerService} from '../../../../../../services/teller/teller-service';
+import {TellerBalance} from './teller-balance.model';
+import {Observable} from 'rxjs/Observable';
+import {TellerEntry} from '../../../../../../services/teller/domain/teller-entry.model';
+
+@Injectable()
+export class BalanceSheetService {
+
+ constructor(private tellerService: TellerService) {
+ }
+
+ private compareEntry(entryA: TellerEntry, entryB: TellerEntry): number {
+ return new Date(entryA.transactionDate).getTime() - new Date(entryB.transactionDate).getTime();
+ }
+
+ getBalance(officeIdentifier: string, tellerCode: string): Observable<TellerBalance> {
+ return this.tellerService.getBalance(officeIdentifier, tellerCode)
+ .map(balance => {
+ const chequeEntries = balance.chequeEntries || [];
+ const cashEntries = balance.cashEntries || [];
+
+ const entries = cashEntries.concat(chequeEntries)
+ .sort((entryA, entryB) => this.compareEntry(entryA, entryB));
+
+ return {
+ day: balance.day,
+ cashOnHand: balance.cashOnHand,
+ cashReceivedTotal: balance.cashReceivedTotal,
+ cashDisbursedTotal: balance.cashDisbursedTotal,
+ chequesReceivedTotal: balance.chequesReceivedTotal,
+ entries
+ };
+ });
+ }
+}
diff --git a/src/app/offices/detail/teller/detail/balance/services/teller-balance.model.ts b/src/app/offices/detail/teller/detail/balance/services/teller-balance.model.ts
new file mode 100644
index 0000000..e9f495f
--- /dev/null
+++ b/src/app/offices/detail/teller/detail/balance/services/teller-balance.model.ts
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {TellerEntry} from '../../../../../../services/teller/domain/teller-entry.model';
+
+export interface TellerBalance {
+ day?: string;
+ cashOnHand: string;
+ cashReceivedTotal: string;
+ cashDisbursedTotal: string;
+ chequesReceivedTotal: string;
+ entries: TellerEntry[];
+}
diff --git a/src/app/offices/detail/teller/detail/command/close.component.html b/src/app/offices/detail/teller/detail/command/close.component.html
new file mode 100644
index 0000000..3fb8d9b
--- /dev/null
+++ b/src/app/offices/detail/teller/detail/command/close.component.html
@@ -0,0 +1,33 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Close teller' | translate}}"
+ [state]="form.valid ? 'complete' : form.pristine ? 'none' : 'required'">
+ <form [formGroup]="form">
+ <fims-teller-adjustment-form
+ [form]="form"
+ [adjustmentOptions]="adjustmentOptions">
+ </fims-teller-adjustment-form>
+ </form>
+ <ng-template td-step-actions>
+ <button mat-raised-button color="primary" (click)="close()" [disabled]="form.invalid">{{'CLOSE TELLER' | translate}}</button>
+ <span flex></span>
+ <button mat-button (click)="cancel()">{{'CANCEL' | translate}}</button>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/offices/detail/teller/detail/command/close.component.ts b/src/app/offices/detail/teller/detail/command/close.component.ts
new file mode 100644
index 0000000..0fb07b0
--- /dev/null
+++ b/src/app/offices/detail/teller/detail/command/close.component.ts
@@ -0,0 +1,76 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, OnInit, Output, ViewChild} from '@angular/core';
+import {FormBuilder, Validators} from '@angular/forms';
+import {TdStepComponent} from '@covalent/core';
+import {TellerManagementCommand} from '../../../../../services/teller/domain/teller-management-command.model';
+import {FormComponent} from '../../../../../common/forms/form.component';
+import {FimsValidators} from '../../../../../common/validator/validators';
+import {AdjustmentOption} from './model/adjustment-option.model';
+
+@Component({
+ selector: 'fims-teller-close-command',
+ templateUrl: './close.component.html'
+})
+export class CloseOfficeTellerFormComponent extends FormComponent<TellerManagementCommand> implements OnInit {
+
+ @ViewChild('detailsStep') step: TdStepComponent;
+
+ @Output() onClose = new EventEmitter<TellerManagementCommand>();
+
+ @Output() onCancel = new EventEmitter<void>();
+
+ adjustmentOptions: AdjustmentOption[] = [
+ { key: 'NONE', label: 'None' },
+ { key: 'CREDIT', label: 'Cash out' }
+ ];
+
+ constructor(private formBuilder: FormBuilder) {
+ super();
+ }
+
+ ngOnInit(): void {
+ this.form = this.formBuilder.group({
+ adjustment: ['NONE'],
+ amount: [0, [Validators.required, FimsValidators.minValue(0)]],
+ });
+
+ this.step.open();
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+ get formData(): TellerManagementCommand {
+ // Not needed
+ return null;
+ }
+
+ close(): void {
+ const command: TellerManagementCommand = {
+ action: 'CLOSE',
+ adjustment: this.form.get('adjustment').value,
+ amount: this.form.get('amount').value,
+ };
+
+ this.onClose.emit(command);
+ }
+
+}
diff --git a/src/app/offices/detail/teller/detail/command/command.component.html b/src/app/offices/detail/teller/detail/command/command.component.html
new file mode 100644
index 0000000..1d1dba3
--- /dev/null
+++ b/src/app/offices/detail/teller/detail/command/command.component.html
@@ -0,0 +1,21 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{title | translate}}">
+ <fims-teller-open-command *ngIf="action === 'OPEN'" (onOpen)="onOpen($event)" (onCancel)="onCancel()"></fims-teller-open-command>
+ <fims-teller-close-command *ngIf="action === 'CLOSE'" (onClose)="onClose($event)" (onCancel)="onCancel()"></fims-teller-close-command>
+</fims-layout-card-over>
diff --git a/src/app/offices/detail/teller/detail/command/command.component.ts b/src/app/offices/detail/teller/detail/command/command.component.ts
new file mode 100644
index 0000000..d9cec26
--- /dev/null
+++ b/src/app/offices/detail/teller/detail/command/command.component.ts
@@ -0,0 +1,91 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import * as fromOffices from '../../../../store/index';
+import {OfficesStore} from '../../../../store/index';
+import {Action, TellerManagementCommand} from '../../../../../services/teller/domain/teller-management-command.model';
+import {Teller} from '../../../../../services/teller/domain/teller.model';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Office} from '../../../../../services/office/domain/office.model';
+import {Subscription} from 'rxjs/Subscription';
+import {EXECUTE_COMMAND} from '../../../../store/teller/teller.actions';
+
+@Component({
+ templateUrl: './command.component.html'
+})
+export class OfficeTellerCommandComponent implements OnInit, OnDestroy {
+
+ private officeSubscription: Subscription;
+
+ private tellerSubscription: Subscription;
+
+ office: Office;
+
+ teller: Teller;
+
+ action: Action = 'OPEN';
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: OfficesStore) {}
+
+ ngOnInit(): void {
+ this.route.queryParams.subscribe(params => this.action = params['action']);
+
+ this.officeSubscription = this.store.select(fromOffices.getSelectedOffice)
+ .filter(office => !!office)
+ .subscribe(office => this.office = office);
+
+ this.tellerSubscription = this.store.select(fromOffices.getSelectedTeller)
+ .filter(teller => !!teller)
+ .subscribe(teller => this.teller = teller);
+ }
+
+ ngOnDestroy(): void {
+ this.officeSubscription.unsubscribe();
+ this.tellerSubscription.unsubscribe();
+ }
+
+ onOpen(command: TellerManagementCommand): void {
+ this.executeCommand(command);
+ }
+
+ onClose(command: TellerManagementCommand): void {
+ this.executeCommand(command);
+ }
+
+ private executeCommand(command: TellerManagementCommand): void {
+ this.store.dispatch({
+ type: EXECUTE_COMMAND,
+ payload: {
+ officeId: this.office.identifier,
+ tellerCode: this.teller.code,
+ activatedRoute: this.route,
+ command
+ }
+ });
+ }
+
+ onCancel(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+
+ get title(): string {
+ return this.action === 'OPEN' ? 'Open teller' : 'Close teller';
+ }
+
+}
diff --git a/src/app/offices/detail/teller/detail/command/components/adjustment.component.html b/src/app/offices/detail/teller/detail/command/components/adjustment.component.html
new file mode 100644
index 0000000..aa58e39
--- /dev/null
+++ b/src/app/offices/detail/teller/detail/command/components/adjustment.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ng-container [formGroup]="form" layout="column">
+ <mat-radio-group formControlName="adjustment">
+ <mat-radio-button *ngFor="let adjustment of adjustmentOptions" [value]="adjustment.key" layout-margin>
+ {{adjustment.label}}
+ </mat-radio-button>
+ </mat-radio-group>
+ <fims-text-input type="number" [form]="form" controlName="amount" placeholder="{{'Amount' | translate}}"></fims-text-input>
+</ng-container>
diff --git a/src/app/offices/detail/teller/detail/command/components/adjustment.component.ts b/src/app/offices/detail/teller/detail/command/components/adjustment.component.ts
new file mode 100644
index 0000000..a7c4bc6
--- /dev/null
+++ b/src/app/offices/detail/teller/detail/command/components/adjustment.component.ts
@@ -0,0 +1,45 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input, OnInit} from '@angular/core';
+import {FormGroup} from '@angular/forms';
+import {AdjustmentOption} from '../model/adjustment-option.model';
+
+@Component({
+ selector: 'fims-teller-adjustment-form',
+ templateUrl: './adjustment.component.html'
+})
+export class AdjustmentComponent implements OnInit {
+
+ @Input() form: FormGroup;
+
+ @Input() adjustmentOptions: AdjustmentOption[];
+
+ ngOnInit(): void {
+ this.form.get('adjustment').valueChanges
+ .startWith(this.form.get('adjustment').value)
+ .subscribe(adjustment => {
+ const amountControl = this.form.get('amount');
+ if (adjustment === 'NONE') {
+ amountControl.disable();
+ } else {
+ amountControl.enable();
+ }
+ });
+ }
+}
diff --git a/src/app/offices/detail/teller/detail/command/model/adjustment-option.model.ts b/src/app/offices/detail/teller/detail/command/model/adjustment-option.model.ts
new file mode 100644
index 0000000..b16c40c
--- /dev/null
+++ b/src/app/offices/detail/teller/detail/command/model/adjustment-option.model.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Adjustment} from '../../../../../../services/teller/domain/teller-management-command.model';
+
+export interface AdjustmentOption {
+ key: string | Adjustment;
+ label: string;
+}
diff --git a/src/app/offices/detail/teller/detail/command/open.component.html b/src/app/offices/detail/teller/detail/command/open.component.html
new file mode 100644
index 0000000..541b328
--- /dev/null
+++ b/src/app/offices/detail/teller/detail/command/open.component.html
@@ -0,0 +1,41 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Open teller' | translate}}"
+ [state]="form.valid ? 'complete' : form.pristine ? 'none' : 'required'">
+ <form [formGroup]="form">
+ <fims-teller-adjustment-form
+ [form]="form"
+ [adjustmentOptions]="adjustmentOptions">
+ </fims-teller-adjustment-form>
+ <fims-employee-auto-complete title="{{'Assigned employee' | translate}}" formControlName="assignedEmployeeIdentifier">
+ <ng-container *ngIf="!form.get('assignedEmployeeIdentifier').pristine && form.get('assignedEmployeeIdentifier').hasError('required')" translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="form.get('assignedEmployeeIdentifier').hasError('invalidEmployee')" translate>
+ Invalid employee
+ </ng-container>
+ </fims-employee-auto-complete>
+ </form>
+ <ng-template td-step-actions>
+ <button mat-raised-button color="primary" (click)="open()" [disabled]="form.invalid">{{'OPEN TELLER' | translate}}</button>
+ <span flex></span>
+ <button mat-button (click)="cancel()">{{'CANCEL' | translate}}</button>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/offices/detail/teller/detail/command/open.component.ts b/src/app/offices/detail/teller/detail/command/open.component.ts
new file mode 100644
index 0000000..f884664
--- /dev/null
+++ b/src/app/offices/detail/teller/detail/command/open.component.ts
@@ -0,0 +1,80 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, OnInit, Output, ViewChild} from '@angular/core';
+import {FormBuilder, Validators} from '@angular/forms';
+import {TdStepComponent} from '@covalent/core';
+import {TellerManagementCommand} from '../../../../../services/teller/domain/teller-management-command.model';
+import {FormComponent} from '../../../../../common/forms/form.component';
+import {OfficeService} from '../../../../../services/office/office.service';
+import {employeeExists} from '../../../../../common/validator/employee-exists.validator';
+import {AdjustmentOption} from './model/adjustment-option.model';
+import {FimsValidators} from '../../../../../common/validator/validators';
+
+@Component({
+ selector: 'fims-teller-open-command',
+ templateUrl: './open.component.html'
+})
+export class OpenOfficeTellerFormComponent extends FormComponent<TellerManagementCommand> implements OnInit {
+
+ @ViewChild('detailsStep') step: TdStepComponent;
+
+ @Output() onOpen = new EventEmitter<TellerManagementCommand>();
+
+ @Output() onCancel = new EventEmitter<void>();
+
+ adjustmentOptions: AdjustmentOption[] = [
+ { key: 'NONE', label: 'None' },
+ { key: 'DEBIT', label: 'Cash in' },
+ ];
+
+ constructor(private formBuilder: FormBuilder, private officeService: OfficeService) {
+ super();
+ }
+
+ ngOnInit(): void {
+ this.form = this.formBuilder.group({
+ adjustment: ['NONE'],
+ amount: [0, [Validators.required, FimsValidators.minValue(0)]],
+ assignedEmployeeIdentifier: ['', [Validators.required], employeeExists(this.officeService)]
+ });
+
+ this.step.open();
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+ get formData(): TellerManagementCommand {
+ // Not needed
+ return null;
+ }
+
+ open(): void {
+ const command: TellerManagementCommand = {
+ action: 'OPEN',
+ assignedEmployeeIdentifier: this.form.get('assignedEmployeeIdentifier').value,
+ adjustment: this.form.get('adjustment').value,
+ amount: this.form.get('amount').value,
+ };
+
+ this.onOpen.emit(command);
+ }
+
+}
diff --git a/src/app/offices/detail/teller/detail/denomination/denomination.list.component.html b/src/app/offices/detail/teller/detail/denomination/denomination.list.component.html
new file mode 100644
index 0000000..bc6475e
--- /dev/null
+++ b/src/app/offices/detail/teller/detail/denomination/denomination.list.component.html
@@ -0,0 +1,39 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{ 'Manage denominations' | translate}}" [navigateBackTo]="['../../../../../../../']">
+ <td-message
+ *ngIf="isTellerNotPaused$ | async"
+ label="{{'Teller is not paused' | translate }}"
+ sublabel="{{'Teller must be paused to create denominations' | translate }}"
+ color="warn"
+ icon="error">
+ </td-message>
+ <fims-data-table flex
+ [columns]="columns"
+ [data]="denominationData$ | async"
+ [sortable]="false"
+ [pageable]="false"
+ [actionColumn]="false">
+ </fims-data-table>
+</fims-layout-card-over>
+<fims-fab-button
+ title="{{'Create denomination' | translate}}"
+ icon="attach_money" [link]="['create']"
+ [permission]="{ id: 'teller_management', accessLevel: 'CHANGE'}"
+ [disabled]="isTellerNotPaused$ | async">
+</fims-fab-button>
diff --git a/src/app/offices/detail/teller/detail/denomination/denomination.list.component.ts b/src/app/offices/detail/teller/detail/denomination/denomination.list.component.ts
new file mode 100644
index 0000000..41b7a1b
--- /dev/null
+++ b/src/app/offices/detail/teller/detail/denomination/denomination.list.component.ts
@@ -0,0 +1,83 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {TableData} from '../../../../../common/data-table/data-table.component';
+import {Subscription} from 'rxjs/Subscription';
+import {Observable} from 'rxjs/Observable';
+import * as fromOffices from '../../../../store/index';
+import {OfficesStore} from '../../../../store/index';
+import {LoadDenominationAction} from '../../../../store/teller/denomination/denomination.actions';
+import {DatePipe} from '@angular/common';
+
+@Component({
+ templateUrl: './denomination.list.component.html',
+ providers: [DatePipe]
+})
+export class TellerDenominationListComponent implements OnInit, OnDestroy {
+
+ private loadDenominationSubscription: Subscription;
+
+ denominationData$: Observable<TableData>;
+
+ isTellerNotPaused$: Observable<boolean>;
+
+ columns: any[] = [
+ {name: 'countedTotal', label: 'Counted total'},
+ {name: 'note', label: 'Note'},
+ {name: 'adjustingJournalEntry', label: 'Adjusting journal entry'},
+ {
+ name: 'createdOn', label: 'Created on', format: (v: any) => {
+ return this.datePipe.transform(v, 'short');
+ }
+ },
+ {name: 'createdBy', label: 'Created by'}
+ ];
+
+ constructor(private store: OfficesStore, private datePipe: DatePipe) {}
+
+ ngOnInit(): void {
+ const selectedTeller$ = this.store.select(fromOffices.getSelectedTeller).filter(teller => !!teller);
+
+ this.isTellerNotPaused$ = selectedTeller$.map(teller => teller.state !== 'PAUSED');
+
+ this.loadDenominationSubscription = Observable.combineLatest(
+ this.store.select(fromOffices.getSelectedOffice).filter(office => !!office),
+ selectedTeller$,
+ (office, teller) => ({
+ office,
+ teller
+ })
+ ).map(result => new LoadDenominationAction({
+ officeId: result.office.identifier,
+ tellerCode: result.teller.code
+ })).subscribe(this.store);
+
+ this.denominationData$ = this.store.select(fromOffices.getDenominationsEntities)
+ .map(tellers => ({
+ data: tellers,
+ totalElements: tellers.length,
+ totalPages: 1
+ }));
+ }
+
+ ngOnDestroy(): void {
+ this.loadDenominationSubscription.unsubscribe();
+ }
+
+}
diff --git a/src/app/offices/detail/teller/detail/denomination/form/create.form.component.html b/src/app/offices/detail/teller/detail/denomination/form/create.form.component.html
new file mode 100644
index 0000000..0414331
--- /dev/null
+++ b/src/app/offices/detail/teller/detail/denomination/form/create.form.component.html
@@ -0,0 +1,24 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Create new denomination' | translate}}" *ngIf="currentSelection$ | async as selection">
+ <fims-denomination-form-component #form
+ [balanceSheet]="balanceSheet$ | async"
+ (onSave)="onSave(selection.officeId, selection.tellerId, $event)"
+ (onCancel)="onCancel()">
+ </fims-denomination-form-component>
+</fims-layout-card-over>
diff --git a/src/app/offices/detail/teller/detail/denomination/form/create.form.component.ts b/src/app/offices/detail/teller/detail/denomination/form/create.form.component.ts
new file mode 100644
index 0000000..2dff441
--- /dev/null
+++ b/src/app/offices/detail/teller/detail/denomination/form/create.form.component.ts
@@ -0,0 +1,76 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import * as fromOffices from '../../../../../store/index';
+import {OfficesStore} from '../../../../../store/index';
+import {TellerDenomination} from '../../../../../../services/teller/domain/teller-denomination.model';
+import {CREATE_DENOMINATION} from '../../../../../store/teller/denomination/denomination.actions';
+import {Observable} from 'rxjs/Observable';
+import {TellerService} from '../../../../../../services/teller/teller-service';
+import {TellerBalanceSheet} from '../../../../../../services/teller/domain/teller-balance-sheet.model';
+
+interface CurrentSelection {
+ officeId: string;
+ tellerId: string;
+}
+
+@Component({
+ templateUrl: './create.form.component.html'
+})
+export class CreateDenominationFormComponent {
+
+ currentSelection$: Observable<CurrentSelection>;
+
+ balanceSheet$: Observable<TellerBalanceSheet>;
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: OfficesStore, private tellerService: TellerService) {
+ this.currentSelection$ = Observable.combineLatest(
+ this.store.select(fromOffices.getSelectedOffice).filter(office => !!office),
+ this.store.select(fromOffices.getSelectedTeller).filter(teller => !!teller),
+ (office, teller) => ({
+ office,
+ teller
+ })).map(result => ({
+ officeId: result.office.identifier,
+ tellerId: result.teller.code
+ }));
+
+ this.balanceSheet$ = this.currentSelection$
+ .switchMap(selection => this.tellerService.getBalance(selection.officeId, selection.tellerId));
+ }
+
+ onSave(officeId: string, tellerCode: string, denomination: TellerDenomination): void {
+ this.store.dispatch({ type: CREATE_DENOMINATION, payload: {
+ officeId,
+ tellerCode,
+ denomination,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel(): void {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+
+}
diff --git a/src/app/offices/detail/teller/detail/denomination/form/form.component.html b/src/app/offices/detail/teller/detail/denomination/form/form.component.html
new file mode 100644
index 0000000..c3afb8c
--- /dev/null
+++ b/src/app/offices/detail/teller/detail/denomination/form/form.component.html
@@ -0,0 +1,79 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Denomination details' | translate}}"
+ [state]="form.valid ? 'complete' : form.pristine ? 'none' : 'required'" [active]="true">
+ <form [formGroup]="form" layout="column">
+ <mat-form-field layout-margin flex>
+ <textarea matInput placeholder="{{'Note(Optional)' | translate}}" formControlName="note"></textarea>
+ <mat-error *ngIf="form.get('note').hasError('required')" translate>
+ Required
+ </mat-error>
+ </mat-form-field>
+ <div layout-gt-xs="column" layout-margin formArrayName="details">
+ <h4 translate>Details</h4>
+ <div *ngFor="let detail of details; let i=index" [formGroupName]="i" layout="row">
+ <fims-text-input type="number" [form]="detail" controlName="value" placeholder="{{'Note value' | translate}}"></fims-text-input>
+ <fims-text-input type="number" [form]="detail" controlName="count" placeholder="{{'Count' | translate}}"></fims-text-input>
+ <mat-form-field layout-margin>
+ <input matInput
+ placeholder="{{'Subtotal' | translate}}"
+ [disabled]="true"
+ [value]="getTotalValue(detail) | displayFimsNumber"
+ flex/>
+ </mat-form-field>
+ <button mat-button (click)="removeDetail(i)" *ngIf="i > 0">{{'Remove detail' | translate}}</button>
+ </div>
+ <button mat-button (click)="addDetail()">{{'Add detail' | translate}}</button>
+ <table td-data-table>
+ <tbody>
+ <tr td-data-table-row>
+ <td td-data-table-cell>
+ <b translate>Total</b>
+ </td>
+ <td td-data-table-cell>
+ <b>{{overallTotal | displayFimsNumber}}</b>
+ </td>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell></td>
+ </tr>
+ <tr td-data-table-row>
+ <td td-data-table-cell>
+ <b translate>Cash on hand</b>
+ </td>
+ <td td-data-table-cell>
+ <b>{{balanceSheet?.cashOnHand | displayFimsNumber}}</b>
+ </td>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </form>
+ <ng-template td-step-actions>
+ <fims-form-final-action
+ [resourceName]="'DENOMINATION'"
+ [editMode]="false"
+ [disabled]="form.invalid"
+ (onCancel)="cancel()"
+ (onSave)="save()" flex>
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/offices/detail/teller/detail/denomination/form/form.component.ts b/src/app/offices/detail/teller/detail/denomination/form/form.component.ts
new file mode 100644
index 0000000..c89cd1f
--- /dev/null
+++ b/src/app/offices/detail/teller/detail/denomination/form/form.component.ts
@@ -0,0 +1,104 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {AbstractControl, FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {FimsValidators} from '../../../../../../common/validator/validators';
+import {Component, EventEmitter, Input, Output} from '@angular/core';
+import {TellerDenomination} from '../../../../../../services/teller/domain/teller-denomination.model';
+import {TellerBalanceSheet} from '../../../../../../services/teller/domain/teller-balance-sheet.model';
+
+interface Detail {
+ label: string;
+ value: string;
+ units: string;
+ totalValue: string;
+}
+
+@Component({
+ selector: 'fims-denomination-form-component',
+ templateUrl: './form.component.html'
+})
+export class DenominationFormComponent {
+
+ form: FormGroup;
+
+ @Input() balanceSheet: TellerBalanceSheet;
+
+ @Output() onSave = new EventEmitter<TellerDenomination>();
+
+ @Output() onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder) {
+ this.form = this.formBuilder.group({
+ note: [''],
+ details: formBuilder.array([])
+ });
+
+ this.addDetail();
+ }
+
+ initDetail(): FormGroup {
+ return this.formBuilder.group({
+ value: ['', [Validators.required, FimsValidators.minValue(0)]],
+ count: ['', [Validators.required, FimsValidators.minValue(0)]]
+ });
+ }
+
+ addDetail(): void {
+ const options: FormArray = this.form.get('details') as FormArray;
+ options.push(this.initDetail());
+ }
+
+ removeDetail(index: number): void {
+ const options: FormArray = this.form.get('details') as FormArray;
+ options.removeAt(index);
+ }
+
+ get details(): FormGroup[] {
+ const charges: FormArray = this.form.get('details') as FormArray;
+ return charges.controls as FormGroup[];
+ }
+
+ save(): void {
+ const denomination: TellerDenomination = {
+ note: this.form.get('note').value,
+ countedTotal: this.overallTotal.toString(10)
+ };
+
+ this.onSave.emit(denomination);
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+ getTotalValue(formGroup: FormGroup): number {
+ const value = formGroup.get('value').value;
+ const units = formGroup.get('count').value;
+ if (value && units) {
+ return parseFloat(value) * parseFloat(units);
+ }
+ return 0;
+ }
+
+ get overallTotal(): number {
+ return this.details.reduce((previous, current) => {
+ return previous + this.getTotalValue(current)
+ }, 0)
+ }
+}
diff --git a/src/app/offices/detail/teller/detail/teller.detail.component.html b/src/app/offices/detail/teller/detail/teller.detail.component.html
new file mode 100644
index 0000000..5d2098f
--- /dev/null
+++ b/src/app/offices/detail/teller/detail/teller.detail.component.html
@@ -0,0 +1,93 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{ 'Teller' | translate }}" [navigateBackTo]="['../../../../../']">
+ <td-message *ngIf="isClosed$ | async" label="{{'Teller can be opened' | translate }}" color="warn" icon="error">
+ <a td-message-actions mat-button [routerLink]="['command']" [queryParams]="{ action: 'OPEN'}"
+ *hasPermission="{ id: 'teller_management', accessLevel: 'CHANGE'}" translate>OPEN TELLER
+ </a>
+ </td-message>
+ <td-message *ngIf="!(isClosed$ | async)" label="{{'Teller can be closed' | translate }}" color="accent" icon="check">
+ <a td-message-actions mat-button [routerLink]="['command']" [queryParams]="{ action: 'CLOSE'}"
+ *hasPermission="{ id: 'teller_management', accessLevel: 'CHANGE'}" translate>CLOSE TELLER
+ </a>
+ </td-message>
+ <fims-two-column-layout>
+ <mat-nav-list left>
+ <h3 mat-subheader translate>Management</h3>
+ <a mat-list-item [routerLink]="['balance']">
+ <mat-icon matListAvatar>account_balance</mat-icon>
+ <h3 matLine translate>Teller balance</h3>
+ <p matLine translate>View current teller balance</p>
+ </a>
+ <a mat-list-item [routerLink]="['denominations']">
+ <mat-icon matListAvatar>attach_money</mat-icon>
+ <h3 matLine translate>Denominations</h3>
+ <p matLine translate>Manage denominations</p>
+ </a>
+ </mat-nav-list>
+ <mat-list *ngIf="teller$ | async as teller" right>
+ <h3 mat-subheader translate>Current status</h3>
+ <fims-state-display [state]="teller.state"></fims-state-display>
+ <mat-list-item>
+ <h3 matLine translate>Number</h3>
+ <p matLine>{{teller.code}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Cash withdrawal limit</h3>
+ <p matLine>{{teller.cashdrawLimit}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Teller account</h3>
+ <p matLine>{{teller.tellerAccountIdentifier}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Vault account</h3>
+ <p matLine>{{teller.vaultAccountIdentifier}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Cheques receivable account</h3>
+ <p matLine>{{teller.chequesReceivableAccount}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Cash over short account</h3>
+ <p matLine>{{teller.cashOverShortAccount}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Denomination required?</h3>
+ <p matLine>{{teller.denominationRequired}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Assigned employee</h3>
+ <p matLine>{{teller.assignedEmployee}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Created by</h3>
+ <p matLine>{{teller.createdBy}} - {{teller.createdOn | date:'medium'}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Last modified by</h3>
+ <p matLine>{{teller.lastModifiedBy}} - {{teller.lastModifiedOn | date:'medium'}}</p>
+ </mat-list-item>
+ <mat-list-item>
+ <h3 matLine translate>Last opened by</h3>
+ <p matLine>{{teller.lastOpenedBy}} - {{teller.lastOpenedOn | date:'medium'}}</p>
+ </mat-list-item>
+ </mat-list>
+ </fims-two-column-layout>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Edit teller' | translate}}" icon="mode_edit" [link]="['edit']" [permission]="{ id: 'teller_management', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/offices/detail/teller/detail/teller.detail.component.ts b/src/app/offices/detail/teller/detail/teller.detail.component.ts
new file mode 100644
index 0000000..9a31b90
--- /dev/null
+++ b/src/app/offices/detail/teller/detail/teller.detail.component.ts
@@ -0,0 +1,45 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import * as fromOffices from '../../../store/index';
+import {OfficesStore} from '../../../store/index';
+import {Teller} from '../../../../services/teller/domain/teller.model';
+import {Observable} from 'rxjs/Observable';
+
+@Component({
+ templateUrl: './teller.detail.component.html'
+})
+export class OfficeTellerDetailComponent implements OnInit {
+
+ teller$: Observable<Teller>;
+
+ isClosed$: Observable<boolean>;
+
+ constructor(private store: OfficesStore) {}
+
+ ngOnInit(): void {
+ this.teller$ = this.store.select(fromOffices.getSelectedTeller)
+ .filter(teller => !!teller);
+
+ this.isClosed$ = this.teller$
+ .map(teller => teller.state === 'CLOSED');
+
+ }
+
+}
diff --git a/src/app/offices/detail/teller/form/create.form.component.html b/src/app/offices/detail/teller/form/create.form.component.html
new file mode 100644
index 0000000..9d60987
--- /dev/null
+++ b/src/app/offices/detail/teller/form/create.form.component.html
@@ -0,0 +1,24 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Create new teller' | translate}}">
+ <fims-teller-form-component #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [teller]="teller">
+ </fims-teller-form-component>
+</fims-layout-card-over>
diff --git a/src/app/offices/detail/teller/form/create.form.component.ts b/src/app/offices/detail/teller/form/create.form.component.ts
new file mode 100644
index 0000000..2cec565
--- /dev/null
+++ b/src/app/offices/detail/teller/form/create.form.component.ts
@@ -0,0 +1,86 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, ViewChild} from '@angular/core';
+import {Subscription} from 'rxjs/Subscription';
+import {Teller} from '../../../../services/teller/domain/teller.model';
+import {OfficeTellerFormComponent} from './form.component';
+import {ActivatedRoute, Router} from '@angular/router';
+import * as fromTeller from '../../../store/index';
+import {OfficesStore} from '../../../store/index';
+import {CREATE_TELLER, RESET_FORM} from '../../../store/teller/teller.actions';
+import {Error} from '../../../../services/domain/error.model';
+import {Office} from '../../../../services/office/domain/office.model';
+
+@Component({
+ templateUrl: './create.form.component.html'
+})
+export class CreateOfficeTellerFormComponent implements OnDestroy {
+
+ private officeSubscription: Subscription;
+
+ private formStateSubscription: Subscription;
+
+ private office: Office;
+
+ teller: Teller = {
+ code: '',
+ password: '',
+ cashdrawLimit: undefined,
+ tellerAccountIdentifier: '',
+ vaultAccountIdentifier: '',
+ chequesReceivableAccount: '',
+ cashOverShortAccount: '',
+ denominationRequired: false
+ };
+
+ @ViewChild('form') formComponent: OfficeTellerFormComponent;
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: OfficesStore) {
+ this.officeSubscription = this.store.select(fromTeller.getSelectedOffice)
+ .filter(office => !!office)
+ .subscribe(office => this.office = office);
+
+ this.formStateSubscription = store.select(fromTeller.getTellerFormError)
+ .filter((error: Error) => !!error)
+ .subscribe((error: Error) => this.formComponent.showCodeValidationError());
+ }
+
+ ngOnDestroy(): void {
+ this.formStateSubscription.unsubscribe();
+ this.officeSubscription.unsubscribe();
+ this.store.dispatch({ type: RESET_FORM });
+ }
+
+ onSave(teller: Teller): void {
+ this.store.dispatch({ type: CREATE_TELLER, payload: {
+ officeId: this.office.identifier,
+ teller,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel(): void {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+
+}
diff --git a/src/app/offices/detail/teller/form/edit.form.component.html b/src/app/offices/detail/teller/form/edit.form.component.html
new file mode 100644
index 0000000..3e176e6
--- /dev/null
+++ b/src/app/offices/detail/teller/form/edit.form.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Edit teller' | translate}}">
+ <fims-teller-form-component #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [teller]="teller$ | async"
+ [editMode]="true">
+ </fims-teller-form-component>
+</fims-layout-card-over>
diff --git a/src/app/offices/detail/teller/form/edit.form.component.ts b/src/app/offices/detail/teller/form/edit.form.component.ts
new file mode 100644
index 0000000..0d0d4e5
--- /dev/null
+++ b/src/app/offices/detail/teller/form/edit.form.component.ts
@@ -0,0 +1,72 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {Teller} from '../../../../services/teller/domain/teller.model';
+import {ActivatedRoute, Router} from '@angular/router';
+import * as fromOffices from '../../../store/index';
+import {OfficesStore} from '../../../store/index';
+import {RESET_FORM, UPDATE_TELLER} from '../../../store/teller/teller.actions';
+import {Observable} from 'rxjs/Observable';
+import {Subscription} from 'rxjs/Subscription';
+import {Office} from '../../../../services/office/domain/office.model';
+
+@Component({
+ templateUrl: './edit.form.component.html'
+})
+export class EditOfficeTellerFormComponent implements OnInit, OnDestroy {
+
+ private officeSubscription: Subscription;
+
+ private office: Office;
+
+ teller$: Observable<Teller>;
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: OfficesStore) {}
+
+ ngOnInit(): void {
+ this.teller$ = this.store.select(fromOffices.getSelectedTeller)
+ .filter(teller => !!teller);
+
+ this.officeSubscription = this.store.select(fromOffices.getSelectedOffice)
+ .filter(office => !!office)
+ .subscribe(office => this.office = office);
+ }
+
+ ngOnDestroy(): void {
+ this.store.dispatch({ type: RESET_FORM });
+ this.officeSubscription.unsubscribe();
+ }
+
+ onSave(teller: Teller): void {
+ this.store.dispatch({ type: UPDATE_TELLER, payload: {
+ officeId: this.office.identifier,
+ teller,
+ activatedRoute: this.route
+ }});
+ }
+
+ onCancel(): void {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+
+}
diff --git a/src/app/offices/detail/teller/form/form.component.html b/src/app/offices/detail/teller/form/form.component.html
new file mode 100644
index 0000000..a82d0a0
--- /dev/null
+++ b/src/app/offices/detail/teller/form/form.component.html
@@ -0,0 +1,78 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Teller details' | translate}}"
+ [state]="form.valid ? 'complete' : form.pristine ? 'none' : 'required'">
+ <form [formGroup]="form" layout="column">
+ <fims-id-input [form]="form" [placeholder]="'Number'" controlName="code" [readonly]="editMode"></fims-id-input>
+ <mat-form-field layout-margin flex>
+ <input matInput type="password" placeholder="{{'Password' | translate}}" formControlName="password" tdAutoTrim autocomplete="new-password"/>
+ <mat-error *ngIf="form.get('password').hasError('required')" translate>Required</mat-error>
+ <mat-error *ngIf="form.get('password').hasError('minlength')">
+ {{ 'Must have at least characters.' | translate:{ value: form.get('password').getError('minlength')['requiredLength']} }}
+ </mat-error>
+ <mat-error *ngIf="form.get('password').hasError('maxlength')">
+ {{ 'Only characters allowed.' | translate:{ value: form.get('password').getError('maxlength')['requiredLength']} }}
+ </mat-error>
+ </mat-form-field>
+ <fims-text-input type="number" [form]="form" controlName="cashdrawLimit" placeholder="{{'Cash withdrawal limit' | translate}}"></fims-text-input>
+ <fims-account-select title="{{'Teller account(Asset accounts only)' | translate}}" formControlName="tellerAccountIdentifier" [type]="'ASSET'">
+ <ng-container *ngIf="!form.get('tellerAccountIdentifier').pristine && form.get('tellerAccountIdentifier').hasError('required')" translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="form.get('tellerAccountIdentifier').hasError('invalidAccount')" translate>
+ Invalid account
+ </ng-container>
+ </fims-account-select>
+ <fims-account-select title="{{'Vault account(Asset accounts only)' | translate}}" formControlName="vaultAccountIdentifier" [type]="'ASSET'">
+ <ng-container *ngIf="!form.get('vaultAccountIdentifier').pristine && form.get('vaultAccountIdentifier').hasError('required')" translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="form.get('vaultAccountIdentifier').hasError('invalidAccount')" translate>
+ Invalid account
+ </ng-container>
+ </fims-account-select>
+ <fims-account-select title="{{'Cheques receivable account(Asset accounts only)' | translate}}" formControlName="chequesReceivableAccount" [type]="'ASSET'">
+ <ng-container *ngIf="!form.get('chequesReceivableAccount').pristine && form.get('chequesReceivableAccount').hasError('required')" translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="form.get('chequesReceivableAccount').hasError('invalidAccount')" translate>
+ Invalid account
+ </ng-container>
+ </fims-account-select>
+ <fims-account-select title="{{'Cash over short account(Expense accounts only)' | translate}}" formControlName="cashOverShortAccount" [type]="'EXPENSE'">
+ <ng-container *ngIf="!form.get('cashOverShortAccount').pristine && form.get('cashOverShortAccount').hasError('required')" translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="form.get('cashOverShortAccount').hasError('invalidAccount')" translate>
+ Invalid account
+ </ng-container>
+ </fims-account-select>
+ <mat-checkbox formControlName="denominationRequired" layout-margin translate>Denomination required?</mat-checkbox>
+ </form>
+ <ng-template td-step-actions>
+ <fims-form-final-action
+ [resourceName]="'TELLER'"
+ [editMode]="editMode"
+ [disabled]="form.invalid"
+ (onCancel)="cancel()"
+ (onSave)="save()" flex>
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/offices/detail/teller/form/form.component.spec.ts b/src/app/offices/detail/teller/form/form.component.spec.ts
new file mode 100644
index 0000000..d5d8231
--- /dev/null
+++ b/src/app/offices/detail/teller/form/form.component.spec.ts
@@ -0,0 +1,125 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, DebugElement} from '@angular/core';
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {By} from '@angular/platform-browser';
+import {TranslateModule} from '@ngx-translate/core';
+import {ReactiveFormsModule} from '@angular/forms';
+import {CovalentStepsModule} from '@covalent/core';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {FimsSharedModule} from '../../../../common/common.module';
+import {OfficeTellerFormComponent} from './form.component';
+import {Teller} from '../../../../services/teller/domain/teller.model';
+import {AccountingService} from '../../../../services/accounting/accounting.service';
+import {MatCheckboxModule, MatInputModule, MatRadioModule} from '@angular/material';
+
+describe('Test teller form', () => {
+
+ let fixture: ComponentFixture<TestComponent>;
+
+ let testComponent: TestComponent;
+
+ beforeEach( async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot(),
+ MatCheckboxModule,
+ MatRadioModule,
+ MatInputModule,
+ CovalentStepsModule,
+ FimsSharedModule,
+ ReactiveFormsModule,
+ NoopAnimationsModule
+ ],
+ declarations: [
+ OfficeTellerFormComponent,
+ TestComponent
+ ],
+ providers: [
+ {
+ provide: AccountingService, useClass: class {
+ findAccount = jasmine.createSpy('findAccount').and.returnValue({});
+ fetchAccounts = jasmine.createSpy('fetchAccounts').and.returnValue([]);
+ }
+ }
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TestComponent);
+ testComponent = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should trigger save event', () => {
+ const button: DebugElement = fixture.debugElement.query(By.css('button[mat-raised-button]'));
+
+ button.nativeElement.click();
+
+ expect(testComponent.savedTeller).toEqual(testComponent.teller);
+ });
+
+ it('should trigger cancel event', () => {
+ const button: DebugElement = fixture.debugElement.query(By.css('button[mat-button]'));
+
+ button.nativeElement.click();
+
+ expect(testComponent.canceled).toBeTruthy();
+ });
+
+});
+
+@Component({
+ template: `
+ <fims-teller-form-component (onSave)="onSave($event)" (onCancel)="onCancel($event)" [teller]="teller" [editMode]="true">
+ </fims-teller-form-component>`
+})
+class TestComponent {
+
+ teller: Teller = {
+ code: 'code',
+ password: 'password',
+ cashdrawLimit: 10,
+ tellerAccountIdentifier: 'tellerAccountIdentifier',
+ vaultAccountIdentifier: 'vaultAccountIdentifier',
+ chequesReceivableAccount: 'chequesReceivableAccount',
+ cashOverShortAccount: 'cashOverShortAccount',
+ denominationRequired: false,
+ assignedEmployee: 'assignedEmployee',
+ createdBy: 'createdBy',
+ createdOn: 'createdOn',
+ lastModifiedBy: 'lastModifiedBy',
+ lastModifiedOn: 'lastModifiedOn',
+ state: 'ACTIVE'
+ };
+
+ savedTeller: Teller;
+
+ canceled: boolean;
+
+ onSave(teller: Teller): void {
+ this.savedTeller = teller;
+ }
+
+ onCancel(): void {
+ this.canceled = true;
+ }
+
+}
diff --git a/src/app/offices/detail/teller/form/form.component.ts b/src/app/offices/detail/teller/form/form.component.ts
new file mode 100644
index 0000000..ccfcce7
--- /dev/null
+++ b/src/app/offices/detail/teller/form/form.component.ts
@@ -0,0 +1,100 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core';
+import {Teller} from '../../../../services/teller/domain/teller.model';
+import {FormBuilder, Validators} from '@angular/forms';
+import {FimsValidators} from '../../../../common/validator/validators';
+import {AccountingService} from '../../../../services/accounting/accounting.service';
+import {accountExists} from '../../../../common/validator/account-exists.validator';
+import {FormComponent} from '../../../../common/forms/form.component';
+import {TdStepComponent} from '@covalent/core';
+
+@Component({
+ selector: 'fims-teller-form-component',
+ templateUrl: './form.component.html'
+})
+export class OfficeTellerFormComponent extends FormComponent<Teller> {
+
+ private _teller: Teller;
+
+ @ViewChild('detailsStep') step: TdStepComponent;
+
+ @Input() set teller(teller: Teller) {
+ this._teller = teller;
+ this.prepareForm(teller);
+ }
+
+ @Input('editMode') editMode: boolean;
+
+ @Output() onSave = new EventEmitter<Teller>();
+
+ @Output() onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder, private accountService: AccountingService) {
+ super();
+ }
+
+ prepareForm(teller: Teller): void {
+ this.form = this.formBuilder.group({
+ code: [teller.code, [Validators.required, Validators.minLength(3), Validators.maxLength(32), FimsValidators.urlSafe]],
+ password: [teller.password, [Validators.required, Validators.minLength(8), Validators.maxLength(4096)]],
+ cashdrawLimit: [teller.cashdrawLimit, [Validators.required, FimsValidators.greaterThanValue(0)]],
+ tellerAccountIdentifier: [teller.tellerAccountIdentifier, [Validators.required], accountExists(this.accountService)],
+ vaultAccountIdentifier: [teller.vaultAccountIdentifier, [Validators.required], accountExists(this.accountService)],
+ chequesReceivableAccount: [teller.chequesReceivableAccount, [Validators.required], accountExists(this.accountService)],
+ cashOverShortAccount: [teller.cashOverShortAccount, [Validators.required], accountExists(this.accountService)],
+ denominationRequired: [teller.denominationRequired]
+ });
+
+ this.step.open();
+ }
+
+ save(): void {
+ const teller: Teller = Object.assign({}, this.teller, {
+ code: this.form.get('code').value,
+ password: this.form.get('password').value,
+ cashdrawLimit: this.form.get('cashdrawLimit').value,
+ tellerAccountIdentifier: this.form.get('tellerAccountIdentifier').value,
+ vaultAccountIdentifier: this.form.get('vaultAccountIdentifier').value,
+ chequesReceivableAccount: this.form.get('chequesReceivableAccount').value,
+ cashOverShortAccount: this.form.get('cashOverShortAccount').value,
+ denominationRequired: this.form.get('denominationRequired').value
+ });
+
+ this.onSave.emit(teller);
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+ get formData(): Teller {
+ // Not needed
+ return null;
+ }
+
+ showCodeValidationError(): void {
+ this.setError('code', 'unique', true);
+ }
+
+ get teller(): Teller {
+ return this._teller;
+ }
+}
diff --git a/src/app/offices/detail/teller/teller-exists.guard.ts b/src/app/offices/detail/teller/teller-exists.guard.ts
new file mode 100644
index 0000000..5211c6a
--- /dev/null
+++ b/src/app/offices/detail/teller/teller-exists.guard.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import {Injectable} from '@angular/core';
+import * as fromOffices from '../../store';
+import {Observable} from 'rxjs/Observable';
+import {of} from 'rxjs/observable/of';
+import {OfficesStore} from '../../store/index';
+import {TellerService} from '../../../services/teller/teller-service';
+import {LoadAction} from '../../store/teller/teller.actions';
+import {ExistsGuardService} from '../../../common/guards/exists-guard';
+
+@Injectable()
+export class TellerExistsGuard implements CanActivate {
+
+ constructor(private store: OfficesStore,
+ private tellerService: TellerService,
+ private existsGuardService: ExistsGuardService) {
+ }
+
+ hasTellerInStore(id: string): Observable<boolean> {
+ const timestamp$: Observable<number> = this.store.select(fromOffices.getTellersLoadedAt)
+ .map(loadedAt => loadedAt[id]);
+
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ hasTellerInApi(officeId: string, tellerCode: string): Observable<boolean> {
+ const getTeller$: Observable<any> = this.tellerService.find(officeId, tellerCode)
+ .map(tellerEntity => new LoadAction({
+ resource: tellerEntity
+ }))
+ .do((action: LoadAction) => this.store.dispatch(action))
+ .map(customer => !!customer);
+
+ return this.existsGuardService.routeTo404OnError(getTeller$);
+ }
+
+ hasTeller(officeId: string, tellerCode: string): Observable<boolean> {
+ return this.hasTellerInStore(tellerCode)
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+ return this.hasTellerInApi(officeId, tellerCode);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasTeller(route.parent.params['id'], route.params['code']);
+ }
+}
diff --git a/src/app/offices/detail/teller/teller.index.component.html b/src/app/offices/detail/teller/teller.index.component.html
new file mode 100644
index 0000000..ca721b3
--- /dev/null
+++ b/src/app/offices/detail/teller/teller.index.component.html
@@ -0,0 +1,18 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<router-outlet></router-outlet>
diff --git a/src/app/offices/detail/teller/teller.index.component.ts b/src/app/offices/detail/teller/teller.index.component.ts
new file mode 100644
index 0000000..1feabc6
--- /dev/null
+++ b/src/app/offices/detail/teller/teller.index.component.ts
@@ -0,0 +1,44 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {Subscription} from 'rxjs/Subscription';
+import {OfficesStore} from '../../store/index';
+import {SelectAction} from '../../store/teller/teller.actions';
+
+@Component({
+ templateUrl: './teller.index.component.html',
+})
+export class OfficeTellerIndexComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ constructor(private store: OfficesStore, private route: ActivatedRoute) {}
+
+ ngOnInit(): void {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectAction(params['code']))
+ .subscribe(this.store);
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ }
+}
diff --git a/src/app/offices/detail/teller/teller.list.component.html b/src/app/offices/detail/teller/teller.list.component.html
new file mode 100644
index 0000000..519ad04
--- /dev/null
+++ b/src/app/offices/detail/teller/teller.list.component.html
@@ -0,0 +1,27 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{ 'Manage teller' | translate}}" [navigateBackTo]="['../']">
+ <fims-data-table flex
+ (onActionCellClick)="rowSelect($event)"
+ [columns]="columns"
+ [data]="tellerData$ | async"
+ [sortable]="false"
+ [pageable]="false">
+ </fims-data-table>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Create teller' | translate}}" icon="add" [link]="['create']" [permission]="{ id: 'teller_management', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/offices/detail/teller/teller.list.component.ts b/src/app/offices/detail/teller/teller.list.component.ts
new file mode 100644
index 0000000..1d6bb46
--- /dev/null
+++ b/src/app/offices/detail/teller/teller.list.component.ts
@@ -0,0 +1,74 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {TableData} from '../../../common/data-table/data-table.component';
+import {Observable} from 'rxjs/Observable';
+import {getAllTellerEntities, getSelectedOffice, OfficesStore} from '../../store/index';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Subscription} from 'rxjs/Subscription';
+import {Teller} from '../../../services/teller/domain/teller.model';
+import {LoadTellerAction} from '../../store/teller/teller.actions';
+import {DatePipe} from '@angular/common';
+
+@Component({
+ templateUrl: './teller.list.component.html',
+ providers: [DatePipe]
+})
+export class OfficeTellerListComponent implements OnInit, OnDestroy {
+
+ private loadTellerSubscription: Subscription;
+
+ tellerData$: Observable<TableData>;
+
+ columns: any[] = [
+ {name: 'code', label: 'Number'},
+ {name: 'cashdrawLimit', label: 'Cash withdrawal limit'},
+ {name: 'assignedEmployee', label: 'Assigned employee'},
+ {name: 'state', label: 'Status'},
+ {
+ name: 'lastOpenedOn', label: 'Last opened on', format: (v: any) => {
+ return this.datePipe.transform(v, 'short');
+ }}
+ ];
+
+ constructor(private store: OfficesStore, private route: ActivatedRoute, private router: Router, private datePipe: DatePipe) {}
+
+ ngOnInit(): void {
+ this.loadTellerSubscription = this.store.select(getSelectedOffice)
+ .filter(office => !!office)
+ .map(office => new LoadTellerAction(office.identifier))
+ .subscribe(this.store);
+
+ this.tellerData$ = this.store.select(getAllTellerEntities)
+ .map(tellers => ({
+ data: tellers,
+ totalElements: tellers.length,
+ totalPages: 1
+ }));
+ }
+
+ ngOnDestroy(): void {
+ this.loadTellerSubscription.unsubscribe();
+ }
+
+ rowSelect(teller: Teller): void {
+ this.router.navigate(['detail', teller.code], {relativeTo: this.route});
+ }
+
+}
diff --git a/src/app/offices/form/create/create.form.component.html b/src/app/offices/form/create/create.form.component.html
new file mode 100644
index 0000000..ef6d293
--- /dev/null
+++ b/src/app/offices/form/create/create.form.component.html
@@ -0,0 +1,20 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Create new office' | translate}}">
+ <fims-office-form-component #form (onSave)="onSave($event)" (onCancel)="onCancel()" [office]="office"></fims-office-form-component>
+</fims-layout-card-over>
diff --git a/src/app/offices/form/create/create.form.component.ts b/src/app/offices/form/create/create.form.component.ts
new file mode 100644
index 0000000..cb791c4
--- /dev/null
+++ b/src/app/offices/form/create/create.form.component.ts
@@ -0,0 +1,94 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {OfficeFormComponent} from '../form.component';
+import {Office} from '../../../services/office/domain/office.model';
+import * as fromOffice from '../../store';
+import {CREATE, CREATE_BRANCH, RESET_FORM} from '../../store/office.actions';
+import {Error} from '../../../services/domain/error.model';
+import {Subscription} from 'rxjs/Subscription';
+import {OfficesStore} from '../../store/index';
+
+@Component({
+ templateUrl: './create.form.component.html'
+})
+export class CreateOfficeFormComponent implements OnInit, OnDestroy {
+
+ private formStateSubscription: Subscription;
+
+ private parentIdentifier: string;
+
+ office: Office = { identifier: '', parentIdentifier: '', name: '' };
+
+ @ViewChild('form') formComponent: OfficeFormComponent;
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: OfficesStore) {
+
+ this.formStateSubscription = store.select(fromOffice.getOfficeFormError)
+ .filter((error: Error) => !!error)
+ .subscribe((error: Error) => {
+ const officeDetailForm = this.formComponent.detailForm;
+ const errors = officeDetailForm.get('identifier').errors || {};
+ errors['unique'] = true;
+ officeDetailForm.get('identifier').setErrors(errors);
+ this.formComponent.step.open();
+ });
+ }
+
+ ngOnInit(): void {
+ this.route.queryParams.subscribe((queryParams) => {
+ this.parentIdentifier = queryParams['parentId'];
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.formStateSubscription.unsubscribe();
+
+ this.store.dispatch({ type: RESET_FORM });
+ }
+
+ onSave(office: Office): void {
+ if (this.parentIdentifier) {
+ office.parentIdentifier = this.parentIdentifier;
+ this.store.dispatch({ type: CREATE_BRANCH, payload: {
+ office,
+ activatedRoute: this.route
+ }});
+ } else {
+ this.store.dispatch({ type: CREATE, payload: {
+ office,
+ activatedRoute: this.route
+ }});
+ }
+ }
+
+ onCancel(): void {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ if (this.parentIdentifier) {
+ this.router.navigate(['../detail', this.parentIdentifier ], { relativeTo: this.route });
+ } else {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+ }
+
+}
diff --git a/src/app/offices/form/edit/edit.form.component.html b/src/app/offices/form/edit/edit.form.component.html
new file mode 100644
index 0000000..f13d5da
--- /dev/null
+++ b/src/app/offices/form/edit/edit.form.component.html
@@ -0,0 +1,20 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Edit office' | translate}}">
+ <fims-office-form-component #form [editMode]="true" (onSave)="onSave($event)" (onCancel)="onCancel()" [office]="office"></fims-office-form-component>
+</fims-layout-card-over>
diff --git a/src/app/offices/form/edit/edit.form.component.ts b/src/app/offices/form/edit/edit.form.component.ts
new file mode 100644
index 0000000..42e6064
--- /dev/null
+++ b/src/app/offices/form/edit/edit.form.component.ts
@@ -0,0 +1,56 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Office} from '../../../services/office/domain/office.model';
+import {getSelectedOffice, OfficesStore} from '../../store';
+import {UPDATE} from '../../store/office.actions';
+import {Subscription} from 'rxjs/Subscription';
+
+@Component({
+ templateUrl: './edit.form.component.html'
+})
+export class EditOfficeFormComponent implements OnInit, OnDestroy {
+
+ private officeSubscription: Subscription;
+
+ office: Office;
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: OfficesStore) {}
+
+ ngOnInit() {
+ this.officeSubscription = this.store.select(getSelectedOffice).subscribe((office: Office) => this.office = office);
+ }
+
+ ngOnDestroy(): void {
+ this.officeSubscription.unsubscribe();
+ }
+
+ onSave(office: Office) {
+ office.parentIdentifier = this.office.parentIdentifier;
+ this.store.dispatch({ type: UPDATE, payload: {
+ office,
+ activatedRoute: this.route
+ }});
+ };
+
+ onCancel() {
+ this.router.navigate(['../'], { relativeTo: this.route } );
+ }
+}
diff --git a/src/app/offices/form/form.component.html b/src/app/offices/form/form.component.html
new file mode 100644
index 0000000..852bf03
--- /dev/null
+++ b/src/app/offices/form/form.component.html
@@ -0,0 +1,45 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #detailsStep label="{{'Office details' | translate}}"
+ [state]="detailForm.valid ? 'complete' : detailForm.pristine ? 'none' : 'required'">
+ <form [formGroup]="detailForm" layout="column">
+ <fims-id-input [form]="detailForm" controlName="identifier" [readonly]="editMode"></fims-id-input>
+ <fims-text-input [form]="detailForm" controlName="name" placeholder="{{'Name' | translate}}"></fims-text-input>
+ <fims-text-input [form]="detailForm" controlName="description" placeholder="{{'Description(optional)' | translate}}"></fims-text-input>
+ </form>
+ <ng-template td-step-actions>
+ <fims-form-continue-action (onContinue)="addressStep.open()"></fims-form-continue-action>
+ </ng-template>
+ </td-step>
+ <td-step #addressStep label="{{'Office Address(optional)' | translate}}"
+ [state]="addressForm.valid ? 'complete' : addressForm.pristine ? 'none' : 'required'">
+ <fims-address-form #addressForm [formData]="addressFormData"></fims-address-form>
+ </td-step>
+ <td-step label="{{'Final step' | translate}}" [state]="'complete'" class="disable-pointer">
+ <ng-template td-step-summary>
+ <fims-form-final-action
+ [resourceName]="'OFFICE'"
+ [editMode]="editMode"
+ [disabled]="formsInvalid()"
+ (onCancel)="cancel()"
+ (onSave)="save()" flex>
+ </fims-form-final-action>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/offices/form/form.component.spec.ts b/src/app/offices/form/form.component.spec.ts
new file mode 100644
index 0000000..0a1c024
--- /dev/null
+++ b/src/app/offices/form/form.component.spec.ts
@@ -0,0 +1,135 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, ViewChild} from '@angular/core';
+import {Office} from '../../services/office/domain/office.model';
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {OfficeFormComponent} from './form.component';
+import {TranslateModule} from '@ngx-translate/core';
+import {CovalentStepsModule} from '@covalent/core';
+import {ReactiveFormsModule} from '@angular/forms';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {MatAutocompleteModule, MatInputModule} from '@angular/material';
+import {CountryService} from '../../services/country/country.service';
+import {Country} from '../../services/country/model/country.model';
+import {FimsSharedModule} from '../../common/common.module';
+
+const officeTemplate: Office = {
+ identifier: 'test',
+ name: 'test',
+ description: 'test',
+ address: {
+ street: 'street',
+ city: 'city',
+ region: 'region',
+ postalCode: '12345',
+ countryCode: 'CC',
+ country: 'country'
+ }
+};
+
+const country: Country = {
+ displayName: '',
+ name: officeTemplate.address.country,
+ alpha2Code: officeTemplate.address.countryCode,
+ translations: {}
+};
+
+describe('Test office form', () => {
+
+ let fixture: ComponentFixture<TestComponent>;
+
+ let testComponent: TestComponent;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ ReactiveFormsModule,
+ FimsSharedModule,
+ MatInputModule,
+ MatAutocompleteModule,
+ CovalentStepsModule,
+ TranslateModule.forRoot(),
+ NoopAnimationsModule
+ ],
+ providers: [
+ {
+ // Used by address component
+ provide: CountryService, useClass: class {
+ fetchByCountryCode = jasmine.createSpy('fetchByCountryCode').and.returnValue(country);
+ fetchCountries = jasmine.createSpy('fetchCountries').and.returnValue([country]);
+ }
+ }
+ ],
+ declarations: [
+ OfficeFormComponent,
+ TestComponent
+ ]
+ });
+
+ fixture = TestBed.createComponent(TestComponent);
+ testComponent = fixture.componentInstance;
+ });
+
+ it('should save address when pristine', (done: DoneFn) => {
+ fixture.detectChanges();
+ testComponent.saveEmitter.subscribe((office) => {
+ expect(office.identifier).toBe(officeTemplate.identifier);
+ expect(office.name).toBe(officeTemplate.name);
+ expect(office.description).toBe(officeTemplate.description);
+
+ expect(office.address).toBeDefined();
+
+ expect(office.address.street).toBe(officeTemplate.address.street);
+ expect(office.address.city).toBe(officeTemplate.address.city);
+ expect(office.address.region).toBe(officeTemplate.address.region);
+ expect(office.address.postalCode).toBe(officeTemplate.address.postalCode);
+ expect(office.address.countryCode).toBe(officeTemplate.address.countryCode);
+ expect(office.address.country).toBe(officeTemplate.address.country);
+
+ done();
+ });
+
+ testComponent.triggerSave();
+ });
+
+});
+
+@Component({
+ template: `
+ <fims-office-form-component #form (onSave)="onSave($event)" (onCancel)="onCancel($event)" [office]="office" [editMode]="false">
+ </fims-office-form-component>`
+})
+class TestComponent {
+
+ saveEmitter = new EventEmitter<Office>();
+
+ @ViewChild('form') formComponent: OfficeFormComponent;
+
+ office: Office = officeTemplate;
+
+ triggerSave(): void {
+ this.formComponent.save();
+ }
+
+ onSave(office: Office): void {
+ this.saveEmitter.emit(office);
+ }
+
+ onCancel(): void {}
+}
diff --git a/src/app/offices/form/form.component.ts b/src/app/offices/form/form.component.ts
new file mode 100644
index 0000000..b3ccc07
--- /dev/null
+++ b/src/app/offices/form/form.component.ts
@@ -0,0 +1,98 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
+import {FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {TdStepComponent} from '@covalent/core';
+import {Office} from '../../services/office/domain/office.model';
+import {FimsValidators} from '../../common/validator/validators';
+import {Address} from '../../services/domain/address/address.model';
+import {AddressFormComponent} from '../../common/address/address.component';
+
+
+@Component({
+ selector: 'fims-office-form-component',
+ templateUrl: './form.component.html'
+})
+export class OfficeFormComponent implements OnInit {
+
+ private _office: Office;
+
+ detailForm: FormGroup;
+
+ @ViewChild('detailsStep') step: TdStepComponent;
+
+ @ViewChild('addressForm') addressForm: AddressFormComponent;
+ addressFormData: Address;
+
+ @Input('editMode') editMode: boolean;
+
+ @Input('office') set office(office: Office) {
+ this._office = office;
+ this.prepareForm(office);
+ }
+
+ @Output('onSave') onSave = new EventEmitter<Office>();
+
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder) {}
+
+ prepareForm(office: Office): void {
+ this.detailForm = this.formBuilder.group({
+ identifier: [office.identifier, [Validators.required, Validators.minLength(3), Validators.maxLength(32), FimsValidators.urlSafe]],
+ name: [office.name, [Validators.required, Validators.maxLength(256)]],
+ description: [office.description, Validators.maxLength(2048)]
+ });
+
+ this.addressFormData = office.address;
+ }
+
+ ngOnInit(): void {
+ this.step.open();
+ }
+
+ formsInvalid(): boolean {
+ return ((this.editMode || !this.addressForm.pristine) && !this.addressForm.valid) || this.detailForm.invalid;
+ }
+
+ save(): void {
+ const office: Office = {
+ identifier: this.detailForm.get('identifier').value,
+ name: this.detailForm.get('name').value,
+ description: this.detailForm.get('description').value
+ };
+
+ if (this.addressForm.pristine) {
+ office.address = this.office.address;
+ } else {
+ office.address = this.addressForm.formData;
+ }
+
+ this.onSave.emit(office);
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+ get office(): Office {
+ return this._office;
+ }
+
+}
diff --git a/src/app/offices/headquarter/headquarter-not-found.component.html b/src/app/offices/headquarter/headquarter-not-found.component.html
new file mode 100644
index 0000000..abba2c9
--- /dev/null
+++ b/src/app/offices/headquarter/headquarter-not-found.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<mat-card>
+ <mat-card-title><i class="material-icons">warning</i><span translate>No Headquarter found</span></mat-card-title>
+ <mat-card-content>
+ <p translate>Oh no, it looks like you don't have a headquarter created yet. No worries you can do it now!</p>
+ <button mat-raised-button color="primary" [routerLink]="['../create']">{{'Create headquarter' | translate}}</button>
+ </mat-card-content>
+</mat-card>
+
diff --git a/src/app/offices/headquarter/headquarter-not-found.component.ts b/src/app/offices/headquarter/headquarter-not-found.component.ts
new file mode 100644
index 0000000..1b9e6b0
--- /dev/null
+++ b/src/app/offices/headquarter/headquarter-not-found.component.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component} from '@angular/core';
+
+@Component({
+ templateUrl: './headquarter-not-found.component.html'
+})
+export class HeadquarterNotFoundComponent {}
diff --git a/src/app/offices/headquarter/headquarter.guard.ts b/src/app/offices/headquarter/headquarter.guard.ts
new file mode 100644
index 0000000..acaab86
--- /dev/null
+++ b/src/app/offices/headquarter/headquarter.guard.ts
@@ -0,0 +1,49 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router';
+import {Observable} from 'rxjs/Observable';
+import {OfficeService} from '../../services/office/office.service';
+import {OfficePage} from '../../services/office/domain/office-page.model';
+import {Office} from '../../services/office/domain/office.model';
+
+@Injectable()
+export class HeadquarterGuard implements CanActivate {
+
+ constructor(private officeService: OfficeService, private router: Router) {}
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ const searchTerm = route.queryParams['term'];
+
+ if (searchTerm) {
+ return Observable.of(true);
+ }
+
+ return this.officeService.listOffices().map((officePage: OfficePage) => {
+ if (officePage.totalElements) {
+ const firstOffice: Office = officePage.offices[0];
+ this.router.navigate(['offices/detail', firstOffice.identifier]);
+ } else {
+ this.router.navigate(['offices/hqNotFound']);
+ }
+
+ return false;
+ });
+ }
+}
diff --git a/src/app/offices/office-exists.guard.ts b/src/app/offices/office-exists.guard.ts
new file mode 100644
index 0000000..eab0c5f
--- /dev/null
+++ b/src/app/offices/office-exists.guard.ts
@@ -0,0 +1,67 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import {Injectable} from '@angular/core';
+import {OfficeService} from '../services/office/office.service';
+import {getOfficesLoadedAt, OfficesStore} from './store';
+import {Observable} from 'rxjs/Observable';
+import {LoadAction} from './store/office.actions';
+import {of} from 'rxjs/observable/of';
+import {ExistsGuardService} from '../common/guards/exists-guard';
+
+@Injectable()
+export class OfficeExistsGuard implements CanActivate {
+
+ constructor(private store: OfficesStore,
+ private officeService: OfficeService,
+ private existsGuardService: ExistsGuardService) {}
+
+ hasOfficeInStore(id: string): Observable<boolean> {
+ const timestamp$ = this.store.select(getOfficesLoadedAt)
+ .map(loadedAt => loadedAt[id]);
+
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ hasOfficeInApi(id: string): Observable<boolean> {
+ const getOffice$ = this.officeService.getOffice(id)
+ .map(officeEntity => new LoadAction({
+ resource: officeEntity
+ }))
+ .do((action: LoadAction) => this.store.dispatch(action))
+ .map(office => !!office);
+
+ return this.existsGuardService.routeTo404OnError(getOffice$);
+ }
+
+ hasOffice(id: string): Observable<boolean> {
+ return this.hasOfficeInStore(id)
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+
+ return this.hasOfficeInApi(id);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasOffice(route.params['id']);
+ }
+}
diff --git a/src/app/offices/office.component.html b/src/app/offices/office.component.html
new file mode 100644
index 0000000..32beda8
--- /dev/null
+++ b/src/app/offices/office.component.html
@@ -0,0 +1,31 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{ 'Search results' | translate}}">
+ <fims-layout-card-over-header-menu>
+ <td-search-box #searchBox placeholder="{{ 'Search' | translate }}" (search)="search($event)" [alwaysVisible]="true"></td-search-box>
+ </fims-layout-card-over-header-menu>
+ <fims-data-table flex
+ (onFetch)="fetchOffices($event)"
+ (onActionCellClick)="rowSelect($event)"
+ [columns]="columns"
+ [data]="officeData$ | async"
+ [loading]="loading$ | async"
+ [sortable]="true"
+ [pageable]="true">
+ </fims-data-table>
+</fims-layout-card-over>
diff --git a/src/app/offices/office.component.ts b/src/app/offices/office.component.ts
new file mode 100644
index 0000000..34c76b8
--- /dev/null
+++ b/src/app/offices/office.component.ts
@@ -0,0 +1,84 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import {ActivatedRoute, Params, Router} from '@angular/router';
+import {FetchRequest} from '../services/domain/paging/fetch-request.model';
+import {TableData} from '../common/data-table/data-table.component';
+import {Office} from '../services/office/domain/office.model';
+import * as fromRoot from '../store';
+
+import {Observable} from 'rxjs/Observable';
+import {SEARCH} from '../store/office/office.actions';
+import {OfficesStore} from './store/index';
+
+@Component({
+ selector: 'fims-office',
+ templateUrl: './office.component.html'
+})
+export class OfficeComponent implements OnInit {
+
+ officeData$: Observable<TableData>;
+
+ loading$: Observable<boolean>;
+
+ columns: any[] = [
+ { name: 'identifier', label: 'Id' },
+ { name: 'name', label: 'Name' },
+ { name: 'description', label: 'Description' }
+ ];
+
+ searchTerm: string;
+
+ private lastFetchRequest: FetchRequest = {};
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: OfficesStore) {}
+
+ ngOnInit(): void {
+ this.route.queryParams.subscribe((params: Params) => {
+ this.search(params['term']);
+ });
+ this.officeData$ = this.store.select(fromRoot.getOfficeSearchResults)
+ .map(officePage => ({
+ data: officePage.offices,
+ totalElements: officePage.totalElements,
+ totalPages: officePage.totalPages
+ }));
+
+ this.loading$ = this.store.select(fromRoot.getOfficeSearchLoading);
+ }
+
+ rowSelect(office: Office): void {
+ this.router.navigate(['detail', office.identifier], { relativeTo: this.route });
+ }
+
+ search(searchTerm: string): void {
+ this.searchTerm = searchTerm;
+ this.fetchOffices();
+ }
+
+ fetchOffices(fetchRequest?: FetchRequest): void {
+ if (fetchRequest) {
+ this.lastFetchRequest = fetchRequest;
+ }
+
+ this.lastFetchRequest.searchTerm = this.searchTerm;
+
+ this.store.dispatch({ type: SEARCH, payload: this.lastFetchRequest});
+ }
+}
diff --git a/src/app/offices/office.module.ts b/src/app/offices/office.module.ts
new file mode 100644
index 0000000..21aadd4
--- /dev/null
+++ b/src/app/offices/office.module.ts
@@ -0,0 +1,137 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {NgModule} from '@angular/core';
+import {RouterModule} from '@angular/router';
+import {ReactiveFormsModule} from '@angular/forms';
+import {OfficeRoutes} from './office.routing';
+import {OfficeComponent} from './office.component';
+import {OfficeFormComponent} from './form/form.component';
+import {OfficeDetailComponent} from './detail/office.detail.component';
+import {CreateOfficeFormComponent} from './form/create/create.form.component';
+import {EditOfficeFormComponent} from './form/edit/edit.form.component';
+import {FimsSharedModule} from '../common/common.module';
+import {HeadquarterGuard} from './headquarter/headquarter.guard';
+import {HeadquarterNotFoundComponent} from './headquarter/headquarter-not-found.component';
+import {OfficeExistsGuard} from './office-exists.guard';
+import {Store} from '@ngrx/store';
+import {OfficesStore, officeStoreFactory} from './store/index';
+import {OfficeNotificationEffects} from './store/effects/notification.effects';
+import {EffectsModule} from '@ngrx/effects';
+import {OfficeRouteEffects} from './store/effects/route.effects';
+import {OfficeApiEffects} from './store/effects/service.effects';
+import {TranslateModule} from '@ngx-translate/core';
+import {
+ MatButtonModule,
+ MatCardModule, MatCheckboxModule,
+ MatIconModule,
+ MatInputModule,
+ MatListModule,
+ MatRadioModule,
+ MatToolbarModule
+} from '@angular/material';
+import {CovalentDataTableModule, CovalentMessageModule, CovalentSearchModule, CovalentStepsModule} from '@covalent/core';
+import {CommonModule} from '@angular/common';
+import {TellerApiEffects} from './store/teller/effects/service.effects';
+import {OfficeTellerListComponent} from './detail/teller/teller.list.component';
+import {OfficeIndexComponent} from './detail/office.index.component';
+import {OfficeTellerFormComponent} from './detail/teller/form/form.component';
+import {CreateOfficeTellerFormComponent} from './detail/teller/form/create.form.component';
+import {EditOfficeTellerFormComponent} from './detail/teller/form/edit.form.component';
+import {TellerExistsGuard} from './detail/teller/teller-exists.guard';
+import {TellerRouteEffects} from './store/teller/effects/route.effects';
+import {TellerNotificationEffects} from './store/teller/effects/notification.effects';
+import {OfficeTellerIndexComponent} from './detail/teller/teller.index.component';
+import {TellerBalanceComponent} from './detail/teller/detail/balance/balance.component';
+import {OfficeTellerDetailComponent} from './detail/teller/detail/teller.detail.component';
+import {OpenOfficeTellerFormComponent} from './detail/teller/detail/command/open.component';
+import {CloseOfficeTellerFormComponent} from './detail/teller/detail/command/close.component';
+import {OfficeTellerCommandComponent} from './detail/teller/detail/command/command.component';
+import {AdjustmentComponent} from './detail/teller/detail/command/components/adjustment.component';
+import {BalanceSheetService} from './detail/teller/detail/balance/services/balance-sheet.service';
+import {TellerDenominationApiEffects} from './store/teller/denomination/effects/service.effects';
+import {TellerDenominationRouteEffects} from './store/teller/denomination/effects/route.effects';
+import {TellerDenominationNotificationEffects} from './store/teller/denomination/effects/notification.effects';
+import {TellerDenominationListComponent} from './detail/teller/detail/denomination/denomination.list.component';
+import {CreateDenominationFormComponent} from './detail/teller/detail/denomination/form/create.form.component';
+import {DenominationFormComponent} from './detail/teller/detail/denomination/form/form.component';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(OfficeRoutes),
+ FimsSharedModule,
+ ReactiveFormsModule,
+ TranslateModule,
+ CommonModule,
+ MatCardModule,
+ MatIconModule,
+ MatListModule,
+ MatToolbarModule,
+ MatInputModule,
+ MatButtonModule,
+ MatRadioModule,
+ MatCheckboxModule,
+ CovalentSearchModule,
+ CovalentStepsModule,
+ CovalentDataTableModule,
+ CovalentMessageModule,
+ EffectsModule.run(OfficeApiEffects),
+ EffectsModule.run(OfficeRouteEffects),
+ EffectsModule.run(OfficeNotificationEffects),
+
+ EffectsModule.run(TellerApiEffects),
+ EffectsModule.run(TellerRouteEffects),
+ EffectsModule.run(TellerNotificationEffects),
+
+ EffectsModule.run(TellerDenominationApiEffects),
+ EffectsModule.run(TellerDenominationRouteEffects),
+ EffectsModule.run(TellerDenominationNotificationEffects),
+ ],
+ declarations: [
+ OfficeComponent,
+ OfficeIndexComponent,
+ OfficeFormComponent,
+ CreateOfficeFormComponent,
+ EditOfficeFormComponent,
+ OfficeDetailComponent,
+ HeadquarterNotFoundComponent,
+ OfficeTellerListComponent,
+ OfficeTellerFormComponent,
+ OfficeTellerIndexComponent,
+ OfficeTellerDetailComponent,
+ CreateOfficeTellerFormComponent,
+ EditOfficeTellerFormComponent,
+ OfficeTellerCommandComponent,
+ OpenOfficeTellerFormComponent,
+ CloseOfficeTellerFormComponent,
+ TellerBalanceComponent,
+ AdjustmentComponent,
+ TellerDenominationListComponent,
+ CreateDenominationFormComponent,
+ DenominationFormComponent
+ ],
+ providers: [
+ HeadquarterGuard,
+ OfficeExistsGuard,
+ TellerExistsGuard,
+ BalanceSheetService,
+ { provide: OfficesStore, useFactory: officeStoreFactory, deps: [Store]}
+ ],
+ entryComponents: []
+})
+export class OfficeModule {}
diff --git a/src/app/offices/office.routing.ts b/src/app/offices/office.routing.ts
new file mode 100644
index 0000000..a376cd7
--- /dev/null
+++ b/src/app/offices/office.routing.ts
@@ -0,0 +1,128 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Routes} from '@angular/router';
+import {OfficeComponent} from './office.component';
+import {OfficeDetailComponent} from './detail/office.detail.component';
+import {EditOfficeFormComponent} from './form/edit/edit.form.component';
+import {CreateOfficeFormComponent} from './form/create/create.form.component';
+import {HeadquarterNotFoundComponent} from './headquarter/headquarter-not-found.component';
+import {HeadquarterGuard} from './headquarter/headquarter.guard';
+import {OfficeExistsGuard} from './office-exists.guard';
+import {OfficeIndexComponent} from './detail/office.index.component';
+import {OfficeTellerListComponent} from './detail/teller/teller.list.component';
+import {OfficeTellerIndexComponent} from './detail/teller/teller.index.component';
+import {TellerExistsGuard} from './detail/teller/teller-exists.guard';
+import {EditOfficeTellerFormComponent} from './detail/teller/form/edit.form.component';
+import {CreateOfficeTellerFormComponent} from './detail/teller/form/create.form.component';
+import {OfficeTellerDetailComponent} from './detail/teller/detail/teller.detail.component';
+import {TellerBalanceComponent} from './detail/teller/detail/balance/balance.component';
+import {OfficeTellerCommandComponent} from './detail/teller/detail/command/command.component';
+import {TellerDenominationListComponent} from './detail/teller/detail/denomination/denomination.list.component';
+import {CreateDenominationFormComponent} from './detail/teller/detail/denomination/form/create.form.component';
+
+export const OfficeRoutes: Routes = [
+ {
+ path: '',
+ component: OfficeComponent,
+ canActivate: [HeadquarterGuard],
+ data: {
+ title: 'Manage offices',
+ hasPermission: {id: 'office_offices', accessLevel: 'READ'}
+ }
+ },
+ {
+ path: 'hqNotFound', component: HeadquarterNotFoundComponent, data: {title: 'Headquarter not found'}
+ },
+ {
+ path: 'create',
+ component: CreateOfficeFormComponent,
+ data: {title: 'Create office', hasPermission: {id: 'office_offices', accessLevel: 'CHANGE'}}
+ },
+ {
+ path: 'detail/:id',
+ component: OfficeIndexComponent,
+ canActivate: [OfficeExistsGuard],
+ children: [
+ {
+ path: '',
+ component: OfficeDetailComponent,
+ data: {title: 'View office', hasPermission: {id: 'office_offices', accessLevel: 'READ'}}
+ },
+ {
+ path: 'edit',
+ component: EditOfficeFormComponent,
+ data: {title: 'Edit office', hasPermission: {id: 'office_offices', accessLevel: 'CHANGE'}}
+ },
+ {
+ path: 'tellers',
+ component: OfficeTellerListComponent,
+ data: {title: 'Manage teller', hasPermission: {id: 'office_offices', accessLevel: 'READ'}}
+ },
+ {
+ path: 'tellers/detail/:code',
+ component: OfficeTellerIndexComponent,
+ canActivate: [TellerExistsGuard],
+ data: {hasPermission: {id: 'teller_management', accessLevel: 'READ'}},
+ children: [
+ {
+ path: '',
+ component: OfficeTellerDetailComponent,
+ data: { title: 'View teller' }
+ },
+ {
+ path: 'edit',
+ component: EditOfficeTellerFormComponent,
+ data: {title: 'Edit teller', hasPermission: {id: 'teller_management', accessLevel: 'CHANGE'}}
+ },
+ {
+ path: 'command',
+ component: OfficeTellerCommandComponent,
+ data: { title: 'Manage teller', hasPermission: {id: 'teller_management', accessLevel: 'CHANGE'}}
+ },
+ {
+ path: 'balance',
+ component: TellerBalanceComponent,
+ data: { title: 'View balance'}
+ },
+ {
+ path: 'denominations',
+ children: [
+ {
+ path: '',
+ component: TellerDenominationListComponent,
+ data: { title: 'View denominations'}
+ },
+ {
+ path: 'create',
+ component: CreateDenominationFormComponent,
+ data: { title: 'Create denomination'}
+ }
+ ]
+ },
+ ]
+ },
+ {
+ path: 'tellers/create',
+ component: CreateOfficeTellerFormComponent,
+ data: {title: 'Create teller', hasPermission: {id: 'teller_management', accessLevel: 'CHANGE'}}
+ }
+ ]
+ }
+
+];
diff --git a/src/app/offices/store/effects/notification.effects.ts b/src/app/offices/store/effects/notification.effects.ts
new file mode 100644
index 0000000..f78ad4e
--- /dev/null
+++ b/src/app/offices/store/effects/notification.effects.ts
@@ -0,0 +1,57 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as officeActions from '../office.actions';
+import {NotificationService, NotificationType} from '../../../services/notification/notification.service';
+
+@Injectable()
+export class OfficeNotificationEffects {
+
+ @Effect({ dispatch: false })
+ createOfficeSuccess$: Observable<Action> = this.actions$
+ .ofType(officeActions.CREATE_SUCCESS, officeActions.UPDATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Office is going to be saved'
+ }));
+
+ @Effect({ dispatch: false })
+ deleteOfficeSuccess$: Observable<Action> = this.actions$
+ .ofType(officeActions.DELETE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Office is going to be deleted'
+ }));
+
+ @Effect({ dispatch: false })
+ deleteOfficeFail$: Observable<Action> = this.actions$
+ .ofType(officeActions.DELETE_FAIL)
+ .do(() => this.notificationService.send({
+ type: NotificationType.ALERT,
+ title: 'Office can\'t be deleted',
+ message: 'Office has either branch offices, employees or teller assigned to it.'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {
+ }
+}
+
diff --git a/src/app/offices/store/effects/route.effects.ts b/src/app/offices/store/effects/route.effects.ts
new file mode 100644
index 0000000..8064460
--- /dev/null
+++ b/src/app/offices/store/effects/route.effects.ts
@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as officeActions from '../office.actions';
+import {Router} from '@angular/router';
+
+@Injectable()
+export class OfficeRouteEffects {
+
+ @Effect({ dispatch: false })
+ createOfficeSuccess$: Observable<Action> = this.actions$
+ .ofType(officeActions.CREATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => {
+ if (payload.resource.parentIdentifier) {
+ this.router.navigate(['../detail', payload.resource.parentIdentifier], { relativeTo: payload.activatedRoute });
+ } else {
+ this.router.navigate(['../detail', payload.resource.identifier], { relativeTo: payload.activatedRoute });
+ }
+ });
+
+ @Effect({ dispatch: false })
+ updateOfficeSuccess$: Observable<Action> = this.actions$
+ .ofType(officeActions.UPDATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../../', payload.resource.identifier], { relativeTo: payload.activatedRoute }));
+
+ @Effect({ dispatch: false })
+ deleteOfficeSuccess$: Observable<Action> = this.actions$
+ .ofType(officeActions.DELETE_SUCCESS)
+ .map((action) => action.payload)
+ .do(payload => {
+ if (payload.resource.parentIdentifier) {
+ this.router.navigate(['../../', payload.resource.parentIdentifier], { relativeTo: payload.activatedRoute});
+ } else {
+ this.router.navigate(['../../'], { relativeTo: payload.activatedRoute});
+ }
+ });
+
+ constructor(private actions$: Actions, private router: Router) { }
+}
diff --git a/src/app/offices/store/effects/service.effects.ts b/src/app/offices/store/effects/service.effects.ts
new file mode 100644
index 0000000..7783b23
--- /dev/null
+++ b/src/app/offices/store/effects/service.effects.ts
@@ -0,0 +1,84 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {OfficeService} from '../../../services/office/office.service';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+import * as officeActions from '../office.actions';
+
+@Injectable()
+export class OfficeApiEffects {
+
+ @Effect()
+ createOffice$: Observable<Action> = this.actions$
+ .ofType(officeActions.CREATE)
+ .map((action: officeActions.CreateOfficeAction) => action.payload)
+ .mergeMap(payload =>
+ this.officeService.createOffice(payload.office)
+ .map(() => new officeActions.CreateOfficeSuccessAction({
+ resource: payload.office,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new officeActions.CreateOfficeFailAction(error)))
+ );
+
+ @Effect()
+ createBranchOffice$: Observable<Action> = this.actions$
+ .ofType(officeActions.CREATE_BRANCH)
+ .map((action: officeActions.CreateBranchOfficeAction) => action.payload)
+ .mergeMap(payload =>
+ this.officeService.addBranch(payload.office.parentIdentifier, payload.office)
+ .map(() => new officeActions.CreateOfficeSuccessAction({
+ resource: payload.office,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new officeActions.CreateOfficeFailAction(error)))
+ );
+
+ @Effect()
+ updateOffice$: Observable<Action> = this.actions$
+ .ofType(officeActions.UPDATE)
+ .map((action: officeActions.UpdateOfficeAction) => action.payload)
+ .mergeMap(payload =>
+ this.officeService.updateOffice(payload.office)
+ .map(() => new officeActions.UpdateOfficeSuccessAction({
+ resource: payload.office,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new officeActions.UpdateOfficeFailAction(error)))
+ );
+
+ @Effect()
+ deleteOffice$: Observable<Action> = this.actions$
+ .ofType(officeActions.DELETE)
+ .map((action: officeActions.DeleteOfficeAction) => action.payload)
+ .mergeMap(payload =>
+ this.officeService.deleteOffice(payload.office.identifier)
+ .map(() => new officeActions.DeleteOfficeSuccessAction({
+ resource: payload.office,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new officeActions.DeleteOfficeFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private officeService: OfficeService) {}
+
+}
diff --git a/src/app/offices/store/index.ts b/src/app/offices/store/index.ts
new file mode 100644
index 0000000..9ae603b
--- /dev/null
+++ b/src/app/offices/store/index.ts
@@ -0,0 +1,81 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import * as fromRoot from '../../store';
+import * as fromTellers from '../store/teller/tellers.reducer';
+import * as fromDenominations from '../store/teller/denomination/denominations.reducer';
+import {ActionReducer, Store} from '@ngrx/store';
+import {createReducer} from '../../store/index';
+import {createSelector} from 'reselect';
+import {
+ createResourceReducer,
+ getResourceAll,
+ getResourceEntities,
+ getResourceLoadedAt,
+ getResourceSelected,
+ ResourceState
+} from '../../common/store/resource.reducer';
+import {createFormReducer, FormState, getFormError} from '../../common/store/form.reducer';
+
+export interface State extends fromRoot.State {
+ offices: ResourceState;
+ officeForm: FormState;
+ tellers: ResourceState;
+ tellerForm: FormState;
+ denominations: fromDenominations.State;
+}
+
+const reducers = {
+ offices: createResourceReducer('Office'),
+ officeForm: createFormReducer('Office'),
+ tellers: createResourceReducer('Office Teller', fromTellers.reducer, 'code'),
+ tellerForm: createFormReducer('Office Teller'),
+ denominations: fromDenominations.reducer
+};
+
+export const officeModuleReducer: ActionReducer<State> = createReducer(reducers);
+
+export class OfficesStore extends Store<State> {}
+
+export function officeStoreFactory(appStore: Store<fromRoot.State>) {
+ appStore.replaceReducer(officeModuleReducer);
+ return appStore;
+}
+
+export const getOfficesState = (state: State) => state.offices;
+
+export const getOfficeFormState = (state: State) => state.officeForm;
+export const getOfficeFormError = createSelector(getOfficeFormState, getFormError);
+
+export const getOfficeEntities = createSelector(getOfficesState, getResourceEntities);
+export const getOfficesLoadedAt = createSelector(getOfficesState, getResourceLoadedAt);
+export const getSelectedOffice = createSelector(getOfficesState, getResourceSelected);
+
+export const getTellerState = (state: State) => state.tellers;
+
+export const getTellerFormState = (state: State) => state.tellerForm;
+export const getTellerFormError = createSelector(getTellerFormState, getFormError);
+
+export const getAllTellerEntities = createSelector(getTellerState, getResourceAll);
+
+export const getTellersLoadedAt = createSelector(getTellerState, getResourceLoadedAt);
+export const getSelectedTeller = createSelector(getTellerState, getResourceSelected);
+
+export const getDenominationState = (state: State) => state.denominations;
+export const getDenominationsEntities = createSelector(getDenominationState, fromDenominations.getDenominationEntities);
diff --git a/src/app/offices/store/office.actions.ts b/src/app/offices/store/office.actions.ts
new file mode 100644
index 0000000..dfad003
--- /dev/null
+++ b/src/app/offices/store/office.actions.ts
@@ -0,0 +1,145 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Action} from '@ngrx/store';
+import {type} from '../../store/util';
+import {Office} from '../../services/office/domain/office.model';
+import {Error} from '../../services/domain/error.model';
+import {RoutePayload} from '../../common/store/route-payload';
+import {
+ CreateResourceSuccessPayload,
+ DeleteResourceSuccessPayload,
+ LoadResourcePayload,
+ SelectResourcePayload,
+ UpdateResourceSuccessPayload
+} from '../../common/store/resource.reducer';
+
+export const LOAD = type('[Office] Load');
+export const SELECT = type('[Office] Select');
+
+export const CREATE = type('[Office] Create');
+export const CREATE_BRANCH = type('[Office] Create Branch');
+export const CREATE_SUCCESS = type('[Office] Create Success');
+export const CREATE_FAIL = type('[Office] Create Fail');
+
+export const UPDATE = type('[Office] Update');
+export const UPDATE_SUCCESS = type('[Office] Update Success');
+export const UPDATE_FAIL = type('[Office] Update Fail');
+
+export const DELETE = type('[Office] Delete');
+export const DELETE_SUCCESS = type('[Office] Delete Success');
+export const DELETE_FAIL = type('[Office] Delete Fail');
+
+export const RESET_FORM = type('[Office] Reset Form');
+
+export interface OfficeRoutePayload extends RoutePayload {
+ office: Office;
+}
+
+export class LoadAction implements Action {
+ readonly type = LOAD;
+
+ constructor(public payload: LoadResourcePayload) { }
+}
+
+export class SelectAction implements Action {
+ readonly type = SELECT;
+
+ constructor(public payload: SelectResourcePayload) { }
+}
+
+export class CreateOfficeAction implements Action {
+ readonly type = CREATE;
+
+ constructor(public payload: OfficeRoutePayload) { }
+}
+
+export class CreateBranchOfficeAction implements Action {
+ readonly type = CREATE_BRANCH;
+
+ constructor(public payload: OfficeRoutePayload) { }
+}
+
+export class CreateOfficeSuccessAction implements Action {
+ readonly type = CREATE_SUCCESS;
+
+ constructor(public payload: CreateResourceSuccessPayload) { }
+}
+
+export class CreateOfficeFailAction implements Action {
+ readonly type = CREATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class UpdateOfficeAction implements Action {
+ readonly type = UPDATE;
+
+ constructor(public payload: OfficeRoutePayload) { }
+}
+
+export class UpdateOfficeSuccessAction implements Action {
+ readonly type = UPDATE_SUCCESS;
+
+ constructor(public payload: UpdateResourceSuccessPayload) { }
+}
+
+export class UpdateOfficeFailAction implements Action {
+ readonly type = UPDATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class DeleteOfficeAction implements Action {
+ readonly type = DELETE;
+
+ constructor(public payload: OfficeRoutePayload) { }
+}
+
+export class DeleteOfficeSuccessAction implements Action {
+ readonly type = DELETE_SUCCESS;
+
+ constructor(public payload: DeleteResourceSuccessPayload) { }
+}
+
+export class DeleteOfficeFailAction implements Action {
+ readonly type = DELETE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class ResetOfficeFormAction implements Action {
+ readonly type = RESET_FORM;
+
+ constructor() {}
+}
+
+export type Actions
+ = LoadAction
+ | SelectAction
+ | CreateOfficeAction
+ | CreateBranchOfficeAction
+ | CreateOfficeSuccessAction
+ | CreateOfficeFailAction
+ | UpdateOfficeAction
+ | UpdateOfficeSuccessAction
+ | UpdateOfficeFailAction
+ | DeleteOfficeAction
+ | DeleteOfficeSuccessAction
+ | DeleteOfficeFailAction
+ | ResetOfficeFormAction;
diff --git a/src/app/offices/store/teller/denomination/denomination.actions.ts b/src/app/offices/store/teller/denomination/denomination.actions.ts
new file mode 100644
index 0000000..f310735
--- /dev/null
+++ b/src/app/offices/store/teller/denomination/denomination.actions.ts
@@ -0,0 +1,77 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {type} from '../../../../store/util';
+import {TellerDenomination} from '../../../../services/teller/domain/teller-denomination.model';
+import {RoutePayload} from '../../../../common/store/route-payload';
+import {Action} from '@ngrx/store';
+
+export const LOAD_DENOMINATION = type('[Teller Denomination] Load All ');
+export const LOAD_DENOMINATION_SUCCESS = type('[Teller Denomination] Load All Success');
+
+export const CREATE_DENOMINATION = type('[Teller Denomination] Create');
+export const CREATE_DENOMINATION_SUCCESS = type('[Teller Denomination] Create Success');
+export const CREATE_DENOMINATION_FAIL = type('[Teller Denomination] Create Fail');
+
+export interface LoadDenominationPayload {
+ officeId: string;
+ tellerCode: string;
+}
+
+export interface DenominationPayload extends RoutePayload {
+ officeId: string;
+ tellerCode: string;
+ denomination: TellerDenomination;
+}
+
+export class LoadDenominationAction implements Action {
+ readonly type = LOAD_DENOMINATION;
+
+ constructor(public payload: LoadDenominationPayload) {}
+}
+
+export class LoadDenominationSuccessAction implements Action {
+ readonly type = LOAD_DENOMINATION_SUCCESS;
+
+ constructor(public payload: TellerDenomination[]) {}
+}
+
+export class CreateDenominationAction implements Action {
+ readonly type = CREATE_DENOMINATION;
+
+ constructor(public payload: DenominationPayload) {}
+}
+
+export class CreateDenominationSuccessAction implements Action {
+ readonly type = CREATE_DENOMINATION_SUCCESS;
+
+ constructor(public payload: DenominationPayload) {}
+}
+
+export class CreateDenominationFailAction implements Action {
+ readonly type = CREATE_DENOMINATION_FAIL;
+
+ constructor(public payload: Error) {}
+}
+
+export type Actions
+ = LoadDenominationAction
+ | LoadDenominationSuccessAction
+ | CreateDenominationAction
+ | CreateDenominationSuccessAction
+ | CreateDenominationFailAction;
diff --git a/src/app/offices/store/teller/denomination/denominations.reducer.ts b/src/app/offices/store/teller/denomination/denominations.reducer.ts
new file mode 100644
index 0000000..5e980f4
--- /dev/null
+++ b/src/app/offices/store/teller/denomination/denominations.reducer.ts
@@ -0,0 +1,61 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as denominations from './denomination.actions';
+import {DenominationPayload} from './denomination.actions';
+import {TellerDenomination} from '../../../../services/teller/domain/teller-denomination.model';
+
+export interface State {
+ entities: TellerDenomination[];
+}
+
+export const initialState: State = {
+ entities: [],
+};
+
+export function reducer(state = initialState, action: denominations.Actions): State {
+
+ switch (action.type) {
+
+ case denominations.LOAD_DENOMINATION: {
+ return initialState;
+ }
+
+ case denominations.LOAD_DENOMINATION_SUCCESS: {
+ const denominations: TellerDenomination[] = action.payload;
+
+ return {
+ entities: denominations,
+ };
+ }
+
+ case denominations.CREATE_DENOMINATION_SUCCESS: {
+ const payload: DenominationPayload = action.payload;
+
+ return {
+ entities: state.entities.concat(payload.denomination),
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+export const getDenominationEntities = (state: State) => state.entities;
diff --git a/src/app/offices/store/teller/denomination/effects/notification.effects.ts b/src/app/offices/store/teller/denomination/effects/notification.effects.ts
new file mode 100644
index 0000000..032f1d8
--- /dev/null
+++ b/src/app/offices/store/teller/denomination/effects/notification.effects.ts
@@ -0,0 +1,39 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as denominationActions from '../denomination.actions';
+import {NotificationService, NotificationType} from '../../../../../services/notification/notification.service';
+
+@Injectable()
+export class TellerDenominationNotificationEffects {
+
+ @Effect({ dispatch: false })
+ createDenominationSuccess$: Observable<Action> = this.actions$
+ .ofType(denominationActions.CREATE_DENOMINATION_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Denomination is going to be saved'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {
+ }
+}
diff --git a/src/app/offices/store/teller/denomination/effects/route.effects.ts b/src/app/offices/store/teller/denomination/effects/route.effects.ts
new file mode 100644
index 0000000..4e09777
--- /dev/null
+++ b/src/app/offices/store/teller/denomination/effects/route.effects.ts
@@ -0,0 +1,38 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Router} from '@angular/router';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as denominationActions from '../denomination.actions';
+
+@Injectable()
+export class TellerDenominationRouteEffects {
+
+ @Effect({ dispatch: false })
+ createDenominationSuccess$: Observable<Action> = this.actions$
+ .ofType(denominationActions.CREATE_DENOMINATION_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => {
+ this.router.navigate(['../'], { relativeTo: payload.activatedRoute });
+ });
+
+ constructor(private actions$: Actions, private router: Router) { }
+}
diff --git a/src/app/offices/store/teller/denomination/effects/service.effects.ts b/src/app/offices/store/teller/denomination/effects/service.effects.ts
new file mode 100644
index 0000000..6789b40
--- /dev/null
+++ b/src/app/offices/store/teller/denomination/effects/service.effects.ts
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as denominationActions from '../denomination.actions';
+import {of} from 'rxjs/observable/of';
+import {TellerService} from '../../../../../services/teller/teller-service';
+
+@Injectable()
+export class TellerDenominationApiEffects {
+
+ @Effect()
+ loadDenomination$: Observable<Action> = this.actions$
+ .ofType(denominationActions.LOAD_DENOMINATION)
+ .map((action: denominationActions.LoadDenominationAction) => action.payload)
+ .mergeMap(payload =>
+ this.tellerService.fetchTellerDenominations(payload.officeId, payload.tellerCode)
+ .map(teller => new denominationActions.LoadDenominationSuccessAction(teller))
+ .catch(error => of(new denominationActions.LoadDenominationSuccessAction([])))
+ );
+
+ @Effect()
+ createDenomination$: Observable<Action> = this.actions$
+ .ofType(denominationActions.CREATE_DENOMINATION)
+ .map((action: denominationActions.CreateDenominationAction) => action.payload)
+ .mergeMap(payload =>
+ this.tellerService.saveTellerDenomination(payload.officeId, payload.tellerCode, payload.denomination)
+ .map(() => new denominationActions.CreateDenominationSuccessAction(payload))
+ .catch((error) => of(new denominationActions.CreateDenominationFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private tellerService: TellerService) {}
+
+}
diff --git a/src/app/offices/store/teller/effects/notification.effects.ts b/src/app/offices/store/teller/effects/notification.effects.ts
new file mode 100644
index 0000000..e5eed57
--- /dev/null
+++ b/src/app/offices/store/teller/effects/notification.effects.ts
@@ -0,0 +1,71 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {NotificationService, NotificationType} from '../../../../services/notification/notification.service';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as tellerActions from '../teller.actions';
+
+@Injectable()
+export class TellerNotificationEffects {
+
+ @Effect({ dispatch: false })
+ createTellerSuccess$: Observable<Action> = this.actions$
+ .ofType(tellerActions.CREATE_TELLER_SUCCESS, tellerActions.UPDATE_TELLER_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Teller is going to be saved'
+ }));
+
+ @Effect({ dispatch: false })
+ executeCommandSuccess$: Observable<Action> = this.actions$
+ .ofType(tellerActions.EXECUTE_COMMAND_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Teller is going to be updated'
+ }));
+
+ @Effect({ dispatch: false })
+ openCommandFail$: Observable<Action> = this.actions$
+ .ofType(tellerActions.EXECUTE_COMMAND_FAIL)
+ .map(action => action.payload.command)
+ .filter(command => command.action === 'OPEN')
+ .do(action => this.notificationService.send({
+ type: NotificationType.ALERT,
+ title: 'Employee already assigned',
+ message: 'Employees can only be assigned to one teller. Please choose a different employee or unassign the employee first.'
+ })
+ );
+
+ @Effect({ dispatch: false })
+ closeCommandFail$: Observable<Action> = this.actions$
+ .ofType(tellerActions.EXECUTE_COMMAND_FAIL)
+ .map(action => action.payload.command)
+ .filter(command => command.action === 'CLOSE')
+ .do(action => this.notificationService.send({
+ type: NotificationType.ALERT,
+ title: 'Denomination required',
+ message: 'This teller requires a denomination before it can be closed.'
+ })
+ );
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {
+ }
+}
diff --git a/src/app/offices/store/teller/effects/route.effects.ts b/src/app/offices/store/teller/effects/route.effects.ts
new file mode 100644
index 0000000..daff2f4
--- /dev/null
+++ b/src/app/offices/store/teller/effects/route.effects.ts
@@ -0,0 +1,46 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Router} from '@angular/router';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as tellerActions from '../teller.actions';
+
+@Injectable()
+export class TellerRouteEffects {
+
+ @Effect({ dispatch: false })
+ createTellerSuccess$: Observable<Action> = this.actions$
+ .ofType(tellerActions.CREATE_TELLER_SUCCESS, tellerActions.UPDATE_TELLER_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => {
+ this.router.navigate(['../'], { relativeTo: payload.activatedRoute });
+ });
+
+ @Effect({ dispatch: false })
+ executeCommandSuccess$: Observable<Action> = this.actions$
+ .ofType(tellerActions.EXECUTE_COMMAND_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => {
+ this.router.navigate(['../'], { relativeTo: payload.activatedRoute });
+ });
+
+ constructor(private actions$: Actions, private router: Router) { }
+}
diff --git a/src/app/offices/store/teller/effects/service.effects.ts b/src/app/offices/store/teller/effects/service.effects.ts
new file mode 100644
index 0000000..8ce8d50
--- /dev/null
+++ b/src/app/offices/store/teller/effects/service.effects.ts
@@ -0,0 +1,82 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {TellerService} from '../../../../services/teller/teller-service';
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as tellerActions from '../../teller/teller.actions';
+import {of} from 'rxjs/observable/of';
+
+@Injectable()
+export class TellerApiEffects {
+
+ @Effect()
+ loadTeller$: Observable<Action> = this.actions$
+ .ofType(tellerActions.LOAD_TELLER)
+ .map((action: tellerActions.LoadTellerAction) => action.payload)
+ .mergeMap(officeId =>
+ this.tellerService.fetch(officeId)
+ .map((teller) => new tellerActions.LoadTellerSuccessAction(teller))
+ .catch((error) => of(new tellerActions.LoadTellerSuccessAction([])))
+ );
+
+ @Effect()
+ createTeller$: Observable<Action> = this.actions$
+ .ofType(tellerActions.CREATE_TELLER)
+ .map((action: tellerActions.CreateTellerAction) => action.payload)
+ .mergeMap(payload =>
+ this.tellerService.create(payload.officeId, payload.teller)
+ .map(() => new tellerActions.CreateTellerSuccessAction({
+ activatedRoute: payload.activatedRoute,
+ resource: payload.teller
+ }))
+ .catch((error) => of(new tellerActions.CreateTellerFailAction(error)))
+ );
+
+ @Effect()
+ updateTeller$: Observable<Action> = this.actions$
+ .ofType(tellerActions.UPDATE_TELLER)
+ .map((action: tellerActions.UpdateTellerAction) => action.payload)
+ .mergeMap(payload =>
+ this.tellerService.change(payload.officeId, payload.teller)
+ .map(() => new tellerActions.UpdateTellerSuccessAction({
+ activatedRoute: payload.activatedRoute,
+ resource: payload.teller
+ }))
+ .catch((error) => of(new tellerActions.UpdateTellerFailAction(error)))
+ );
+
+ @Effect()
+ executeCommand$: Observable<Action> = this.actions$
+ .ofType(tellerActions.EXECUTE_COMMAND)
+ .map((action: tellerActions.ExecuteCommandAction) => action.payload)
+ .mergeMap(payload =>
+ this.tellerService.createCommand(payload.officeId, payload.tellerCode, payload.command)
+ .map(() => new tellerActions.ExecuteCommandSuccessAction(payload))
+ .catch((error) => of(new tellerActions.ExecuteCommandFailAction({
+ command: payload.command,
+ error
+ })))
+ );
+
+ constructor(private actions$: Actions, private tellerService: TellerService) {}
+
+}
diff --git a/src/app/offices/store/teller/teller.actions.ts b/src/app/offices/store/teller/teller.actions.ts
new file mode 100644
index 0000000..51df037
--- /dev/null
+++ b/src/app/offices/store/teller/teller.actions.ts
@@ -0,0 +1,161 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {type} from '../../../store/util';
+import {Teller} from '../../../services/teller/domain/teller.model';
+import {Action} from '@ngrx/store';
+import {
+ CreateResourceSuccessPayload,
+ LoadResourcePayload,
+ SelectResourcePayload,
+ UpdateResourceSuccessPayload
+} from '../../../common/store/resource.reducer';
+import {RoutePayload} from '../../../common/store/route-payload';
+import {TellerManagementCommand} from '../../../services/teller/domain/teller-management-command.model';
+
+export const LOAD_TELLER = type('[Office Teller] Load All ');
+export const LOAD_TELLER_SUCCESS = type('[Office Teller] Load All Success');
+
+export const LOAD = type('[Office Teller] Load');
+export const SELECT = type('[Office Teller] Select');
+export const CREATE_TELLER = type('[Office Teller] Create');
+export const CREATE_TELLER_SUCCESS = type('[Office Teller] Create Success');
+export const CREATE_TELLER_FAIL = type('[Office Teller] Create Fail');
+
+export const UPDATE_TELLER = type('[Office Teller] Update');
+export const UPDATE_TELLER_SUCCESS = type('[Office Teller] Update Success');
+export const UPDATE_TELLER_FAIL = type('[Office Teller] Update Fail');
+
+export const RESET_FORM = type('[Office Teller] Reset Form');
+
+export const EXECUTE_COMMAND = type('[Office Teller] Execute Command');
+export const EXECUTE_COMMAND_SUCCESS = type('[Office Teller] Execute Command Success');
+export const EXECUTE_COMMAND_FAIL = type('[Office Teller] Execute Command Fail');
+
+export interface LoadTellerSuccessPayload {
+ officeId: string;
+ teller: Teller[];
+}
+
+export interface ExecuteCommandPayload extends RoutePayload {
+ officeId: string;
+ tellerCode: string;
+ command: TellerManagementCommand;
+}
+
+export interface ExecuteCommandFailPayload {
+ command: TellerManagementCommand;
+ error: Error;
+}
+
+export interface TellerPayload extends RoutePayload {
+ officeId: string;
+ teller: Teller;
+}
+
+export class LoadTellerAction implements Action {
+ readonly type = LOAD_TELLER;
+
+ constructor(public payload: string) {}
+}
+
+export class LoadTellerSuccessAction implements Action {
+ readonly type = LOAD_TELLER_SUCCESS;
+
+ constructor(public payload: Teller[]) {}
+}
+
+export class LoadAction implements Action {
+ readonly type = LOAD;
+
+ constructor(public payload: LoadResourcePayload) { }
+}
+
+export class SelectAction implements Action {
+ readonly type = SELECT;
+
+ constructor(public payload: SelectResourcePayload) { }
+}
+
+export class CreateTellerAction implements Action {
+ readonly type = CREATE_TELLER;
+
+ constructor(public payload: TellerPayload) {}
+}
+
+export class CreateTellerSuccessAction implements Action {
+ readonly type = CREATE_TELLER_SUCCESS;
+
+ constructor(public payload: CreateResourceSuccessPayload) {}
+}
+
+export class CreateTellerFailAction implements Action {
+ readonly type = CREATE_TELLER_FAIL;
+
+ constructor(public payload: Error) {}
+}
+
+export class UpdateTellerAction implements Action {
+ readonly type = UPDATE_TELLER;
+
+ constructor(public payload: TellerPayload) {}
+}
+
+export class UpdateTellerSuccessAction implements Action {
+ readonly type = UPDATE_TELLER_SUCCESS;
+
+ constructor(public payload: UpdateResourceSuccessPayload) {}
+}
+
+export class UpdateTellerFailAction implements Action {
+ readonly type = UPDATE_TELLER_FAIL;
+
+ constructor(public payload: Error) {}
+}
+
+export class ExecuteCommandAction implements Action {
+ readonly type = EXECUTE_COMMAND;
+
+ constructor(public payload: ExecuteCommandPayload) {}
+}
+
+export class ExecuteCommandSuccessAction implements Action {
+ readonly type = EXECUTE_COMMAND_SUCCESS;
+
+ constructor(public payload: ExecuteCommandPayload) {}
+}
+
+export class ExecuteCommandFailAction implements Action {
+ readonly type = EXECUTE_COMMAND_FAIL;
+
+ constructor(public payload: ExecuteCommandFailPayload) {}
+}
+
+export type Actions
+ = LoadTellerAction
+ | LoadTellerSuccessAction
+ | CreateTellerAction
+ | CreateTellerSuccessAction
+ | CreateTellerFailAction
+ | UpdateTellerAction
+ | UpdateTellerSuccessAction
+ | UpdateTellerFailAction
+ | ExecuteCommandAction
+ | ExecuteCommandSuccessAction
+ | ExecuteCommandFailAction;
diff --git a/src/app/offices/store/teller/tellers.reducer.spec.ts b/src/app/offices/store/teller/tellers.reducer.spec.ts
new file mode 100644
index 0000000..a10b17e
--- /dev/null
+++ b/src/app/offices/store/teller/tellers.reducer.spec.ts
@@ -0,0 +1,83 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {reducer} from './tellers.reducer';
+import {ResourceState} from '../../../common/store/resource.reducer';
+import {ExecuteCommandPayload, ExecuteCommandSuccessAction} from './teller.actions';
+import {Status} from '../../../services/teller/domain/teller.model';
+
+describe('Tellers Reducer', () => {
+
+ describe('EXECUTE_COMMAND_SUCCESS', () => {
+
+ function createState(state?: Status, assignedEmployee?: string): ResourceState {
+ return {
+ ids: ['testTeller'],
+ entities: {
+ 'testTeller': {
+ code: 'testTeller',
+ state,
+ assignedEmployee
+ }
+ },
+ selectedId: null,
+ loadedAt: {}
+ };
+ }
+
+ it('should add assigned employee on open', () => {
+ const payload: ExecuteCommandPayload = {
+ officeId: 'officeId',
+ tellerCode: 'testTeller',
+ command: {
+ action: 'OPEN',
+ assignedEmployeeIdentifier: 'test'
+ },
+ activatedRoute: null
+ };
+
+ const initialState: ResourceState = createState();
+
+ const expectedResult: ResourceState = createState('OPEN', payload.command.assignedEmployeeIdentifier);
+
+ const result = reducer(initialState, new ExecuteCommandSuccessAction(payload));
+
+ expect(result).toEqual(expectedResult);
+ });
+
+ it('should remove assigned employee on close', () => {
+ const payload: ExecuteCommandPayload = {
+ officeId: 'officeId',
+ tellerCode: 'testTeller',
+ command: {
+ action: 'CLOSE'
+ },
+ activatedRoute: null
+ };
+
+ const initialState: ResourceState = createState();
+
+ const expectedResult: ResourceState = createState('CLOSED', null);
+
+ const result = reducer(initialState, new ExecuteCommandSuccessAction(payload));
+
+ expect(result).toEqual(expectedResult);
+ });
+ });
+
+});
diff --git a/src/app/offices/store/teller/tellers.reducer.ts b/src/app/offices/store/teller/tellers.reducer.ts
new file mode 100644
index 0000000..df92f4d
--- /dev/null
+++ b/src/app/offices/store/teller/tellers.reducer.ts
@@ -0,0 +1,92 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ResourceState} from '../../../common/store/resource.reducer';
+import * as tellers from '../teller/teller.actions';
+import {Status, Teller} from '../../../services/teller/domain/teller.model';
+import {idsToHashWithCurrentTimestamp, resourcesToHash} from '../../../common/store/reducer.helper';
+import {TellerManagementCommand} from '../../../services/teller/domain/teller-management-command.model';
+
+export const initialState: ResourceState = {
+ ids: [],
+ entities: {},
+ loadedAt: {},
+ selectedId: null,
+};
+
+export function reducer(state = initialState, action: tellers.Actions): ResourceState {
+
+ switch (action.type) {
+
+ case tellers.LOAD_TELLER: {
+ return initialState;
+ }
+
+ case tellers.LOAD_TELLER_SUCCESS: {
+ const tellers: Teller[] = action.payload;
+
+ const ids = tellers.map(teller => teller.code);
+
+ const entities = resourcesToHash(tellers, 'code');
+
+ const loadedAt = idsToHashWithCurrentTimestamp(ids);
+
+ return {
+ ids: [ ...ids ],
+ entities: entities,
+ loadedAt: loadedAt,
+ selectedId: state.selectedId
+ };
+ }
+
+ case tellers.EXECUTE_COMMAND_SUCCESS: {
+ const payload = action.payload;
+ const tellerCode = payload.tellerCode;
+ const command: TellerManagementCommand = payload.command;
+ const teller: Teller = state.entities[tellerCode];
+
+ let tellerState: Status = null;
+ let assignedEmployee = null;
+
+ if (command.action === 'OPEN') {
+ tellerState = 'OPEN';
+ assignedEmployee = command.assignedEmployeeIdentifier;
+ } else if (command.action === 'CLOSE') {
+ tellerState = 'CLOSED';
+ }
+
+ const newTeller = Object.assign({}, teller, {
+ state: tellerState,
+ assignedEmployee
+ });
+
+ return {
+ ids: [ ...state.ids ],
+ entities: Object.assign({}, state.entities, {
+ [teller.code]: newTeller
+ }),
+ loadedAt: state.loadedAt,
+ selectedId: state.selectedId
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
diff --git a/src/app/quickAccess/quick-access.component.html b/src/app/quickAccess/quick-access.component.html
new file mode 100644
index 0000000..87a1e7f
--- /dev/null
+++ b/src/app/quickAccess/quick-access.component.html
@@ -0,0 +1,98 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<mat-toolbar>
+ <span translate>Quick access</span>
+</mat-toolbar>
+
+<div class="mat-content" flex mat-scroll-y>
+ <div layout="column" layout-gt-sm="row" class="inset">
+ <div flex-gt-sm="50" *hasPermission="{ id: 'office_offices', accessLevel: 'READ'}">
+ <mat-card>
+ <mat-card-title translate>Offices</mat-card-title>
+ <mat-card-subtitle translate>Create and edit your offices here.</mat-card-subtitle>
+ <mat-card-actions>
+ <a mat-button color="accent" class="text-upper" [routerLink]="['/offices']">
+ <span translate>View headquarter office</span>
+ </a>
+ </mat-card-actions>
+ </mat-card>
+ </div>
+ <div flex-gt-sm="50" *hasPermission="{ id: 'office_employees', accessLevel: 'READ'}">
+ <mat-card>
+ <mat-card-title translate>Employees</mat-card-title>
+ <mat-card-subtitle translate>Create and edit your employees and assign them to offices.</mat-card-subtitle>
+ <mat-card-actions>
+ <a mat-button color="accent" class="text-upper" [routerLink]="['/employees']">
+ <span translate>View employees</span>
+ </a>
+ <a mat-button color="accent" class="text-upper" [routerLink]="['/employees/create']" *hasPermission="{ id: 'office_employees', accessLevel: 'CHANGE'}">
+ <span translate>Create new employee</span>
+ </a>
+ </mat-card-actions>
+ </mat-card>
+ </div>
+ </div>
+ <div layout="column" layout-gt-sm="row" class="inset">
+ <div flex-gt-sm="40" *hasPermission="{ id: 'customer_customers', accessLevel: 'READ'}">
+ <mat-card>
+ <mat-card-title translate>Member</mat-card-title>
+ <mat-card-subtitle translate>Create/edit members for your offices.</mat-card-subtitle>
+ <mat-card-actions>
+ <a mat-button color="accent" class="text-upper" [routerLink]="['/customers']">
+ <span translate>View member </span>
+ </a>
+ <a mat-button color="accent" class="text-upper" [routerLink]="['/customers/create']" *hasPermission="{ id: 'customer_customers', accessLevel: 'CHANGE'}">
+ <span translate>Create new member </span>
+ </a>
+ </mat-card-actions>
+ </mat-card>
+ </div>
+ </div>
+ <div layout="column" layout-gt-sm="row" class="inset">
+ <div flex-gt-sm="40" *hasPermission="{ id: 'accounting_ledgers', accessLevel: 'READ'}">
+ <mat-card>
+ <mat-card-title translate>Accounting</mat-card-title>
+ <mat-card-subtitle translate>Create/edit ledgers and accounts for your General Ledger.</mat-card-subtitle>
+ <mat-card-actions>
+ <a mat-button color="accent" class="text-upper" [routerLink]="['/accounting']">
+ <span translate>View General Ledger</span>
+ </a>
+ <a mat-button color="accent" class="text-upper" [routerLink]="['/accounting/create']" *hasPermission="{ id: 'accounting_ledgers', accessLevel: 'CHANGE'}">
+ <span translate>Create new ledger</span>
+ </a>
+ </mat-card-actions>
+ </mat-card>
+ </div>
+ </div>
+ <div layout="column" layout-gt-sm="row" class="inset">
+ <div flex-gt-sm="40" *hasPermission="{ id: 'identity_roles', accessLevel: 'READ'}">
+ <mat-card>
+ <mat-card-title translate>Roles</mat-card-title>
+ <mat-card-subtitle translate>Create and edit roles to manage access levels within fims.</mat-card-subtitle>
+ <mat-card-actions>
+ <a mat-button color="accent" class="text-upper" [routerLink]="['/roles']">
+ <span translate>View roles</span>
+ </a>
+ <a mat-button color="accent" class="text-upper" [routerLink]="['/roles/create']" *hasPermission="{ id: 'identity_roles', accessLevel: 'CHANGE'}">
+ <span translate>Create new role</span>
+ </a>
+ </mat-card-actions>
+ </mat-card>
+ </div>
+ </div>
+</div>
diff --git a/src/app/quickAccess/quick-access.component.ts b/src/app/quickAccess/quick-access.component.ts
new file mode 100644
index 0000000..af148b4
--- /dev/null
+++ b/src/app/quickAccess/quick-access.component.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component} from '@angular/core';
+
+@Component({
+ templateUrl: './quick-access.component.html'
+})
+export class QuickAccessComponent {}
diff --git a/src/app/reporting/detail/criteria/criteria.component.html b/src/app/reporting/detail/criteria/criteria.component.html
new file mode 100644
index 0000000..ff4ff4e
--- /dev/null
+++ b/src/app/reporting/detail/criteria/criteria.component.html
@@ -0,0 +1,40 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps>
+ <td-step #step1 label="Criteria" sublabel="Filter your report based on criteria." [active]="true" [disabled]="false">
+ <fims-reporting-query-params #mandatoryComponent [queryParams]="mandatoryParams"></fims-reporting-query-params>
+ <ng-template td-step-actions>
+ <button mat-raised-button color="primary" (click)="step2.open()" class="text-upper">Next</button>
+ </ng-template>
+ </td-step>
+ <td-step #step2 label="Optional criteria" sublabel="Extend your criteria." [disabled]="false">
+ <fims-reporting-query-params #optionalComponent [queryParams]="optionalParams"></fims-reporting-query-params>
+ <ng-template td-step-actions>
+ <button mat-raised-button color="primary" (click)="step3.open()" class="text-upper">Next</button>
+ </ng-template>
+ </td-step>
+ <td-step #step3 label="Field selection" sublabel="Add or remove fields for your report" [disabled]="false">
+ <fims-reporting-displayable-fields #displayableFieldComponent [displayableFields]="displayableFields">
+ </fims-reporting-displayable-fields>
+ </td-step>
+ <td-step label="{{'Final step' | translate}}" [state]="'complete'">
+ <ng-template td-step-summary>
+ <button mat-raised-button color="primary" class="text-upper" [disabled]="!valid" (click)="generateReport()">Generate report</button>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/reporting/detail/criteria/criteria.component.ts b/src/app/reporting/detail/criteria/criteria.component.ts
new file mode 100644
index 0000000..ca40f60
--- /dev/null
+++ b/src/app/reporting/detail/criteria/criteria.component.ts
@@ -0,0 +1,77 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core';
+import {QueryParameter} from '../../../services/reporting/domain/query-parameter.model';
+import {ReportingQueryParamsComponent} from '../queryParams/query-params.component';
+import {DisplayableField} from '../../../services/reporting/domain/displayable-field.model';
+import {ReportingDisplayableFieldsComponent} from '../displayableFields/displayable-fields.component';
+
+export interface GenerateReportEvent {
+ queryParameter: QueryParameter[];
+ displayableFields: DisplayableField[];
+}
+
+@Component({
+ selector: 'fims-reporting-criteria',
+ templateUrl: './criteria.component.html'
+})
+export class ReportingCriteriaComponent {
+
+ mandatoryParams: QueryParameter[];
+
+ optionalParams: QueryParameter[];
+
+ @ViewChild('mandatoryComponent') mandatoryComponent: ReportingQueryParamsComponent;
+
+ @ViewChild('optionalComponent') optionalComponent: ReportingQueryParamsComponent;
+
+ @ViewChild('displayableFieldComponent') displayableFieldsComponent: ReportingDisplayableFieldsComponent;
+
+ @Input() set queryParameter(queryParameter: QueryParameter[]) {
+ if (!queryParameter) {
+ return;
+ }
+
+ this.mandatoryParams = queryParameter.filter(param => param.mandatory);
+ this.optionalParams = queryParameter.filter(param => !param.mandatory);
+ };
+
+ @Input() displayableFields: DisplayableField[];
+
+ @Output() onGenerateReport = new EventEmitter<GenerateReportEvent>();
+
+ get valid(): boolean {
+ return this.mandatoryComponent.valid && this.optionalComponent.valid;
+ }
+
+ generateReport(): void {
+ const queryParameter: QueryParameter[] = [
+ ...this.mandatoryComponent.formData,
+ ...this.optionalComponent.formData
+ ];
+
+ const displayableFields: DisplayableField[] = this.displayableFieldsComponent.formData;
+
+ this.onGenerateReport.emit({
+ queryParameter,
+ displayableFields
+ });
+ }
+
+}
diff --git a/src/app/reporting/detail/displayableFields/displayable-fields.component.html b/src/app/reporting/detail/displayableFields/displayable-fields.component.html
new file mode 100644
index 0000000..04b36de
--- /dev/null
+++ b/src/app/reporting/detail/displayableFields/displayable-fields.component.html
@@ -0,0 +1,44 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<div class="mat-body-1">Locked</div>
+<td-chips [items]="[]"
+ [ngModel]="mandatoryFields"
+ [readOnly]="true"
+ stacked>
+
+ <ng-template td-chip let-chip="chip">
+ <mat-icon>lock_outline</mat-icon> {{chip.name}}
+ </ng-template>
+</td-chips>
+<div class="mat-body-1">Additional fields</div>
+<td-chips [items]="filteredOptionalFields"
+ [formControl]="optionalFormControl"
+ (inputChange)="filterOptionalFields($event)"
+ placeholder="{{'Enter field name and hit enter' | translate}}"
+ stacked
+ requireMatch>
+ <ng-template td-chip let-chip="chip">
+ <div class="tc-grey-100 bgc-teal-700" td-chip-avatar>{{chip.name.substring(0, 1).toUpperCase()}}</div> {{chip.name}}
+ </ng-template>
+ <ng-template td-autocomplete-option let-option="option">
+ <div layout="row" layout-align="start center">
+ {{option.name}}
+ </div>
+ </ng-template>
+</td-chips>
+
diff --git a/src/app/reporting/detail/displayableFields/displayable-fields.component.ts b/src/app/reporting/detail/displayableFields/displayable-fields.component.ts
new file mode 100644
index 0000000..b0ee498
--- /dev/null
+++ b/src/app/reporting/detail/displayableFields/displayable-fields.component.ts
@@ -0,0 +1,87 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {DisplayableField} from '../../../services/reporting/domain/displayable-field.model';
+import {Component, Input, OnInit} from '@angular/core';
+import {FormControl} from '@angular/forms';
+
+@Component({
+ selector: 'fims-reporting-displayable-fields',
+ templateUrl: './displayable-fields.component.html'
+})
+export class ReportingDisplayableFieldsComponent implements OnInit {
+
+ private _displayableFields: DisplayableField[];
+
+ optionalFormControl: FormControl;
+
+ filteredOptionalFields: DisplayableField[];
+
+ mandatoryFields: DisplayableField[];
+
+ optionalFields: DisplayableField[];
+
+ @Input() set displayableFields(displayableFields: DisplayableField[]) {
+ if (!displayableFields) {
+ return;
+ }
+
+ this._displayableFields = displayableFields;
+
+ this.mandatoryFields = displayableFields.filter(field => field.mandatory);
+ this.optionalFields = displayableFields.filter(field => !field.mandatory);
+ };
+
+ constructor() {}
+
+ ngOnInit(): void {
+ this.optionalFormControl = new FormControl([]);
+ }
+
+ filterOptionalFields(value: string): void {
+ this.filteredOptionalFields = this.optionalFields.filter(field => {
+ if (value) {
+ return field.name.toLowerCase().indexOf(value.toLowerCase()) > -1;
+ } else {
+ return false;
+ }
+ }).filter((filteredObj: any) => {
+ return this.optionalFormControl.value ? this.optionalFormControl.value.indexOf(filteredObj) < 0 : true;
+ });
+ }
+
+ get formData(): DisplayableField[] {
+ const allDisplayableFields = [
+ ...this.mandatoryFields,
+ ...this.optionalFormControl.value
+ ];
+
+ // Reuse input displayable field to keep order
+ return this.displayableFields.filter(field => this.hasDisplayableField(field.name, allDisplayableFields));
+ }
+
+ private hasDisplayableField(name: string, displayableField: DisplayableField[]): boolean {
+ const found = displayableField.find(field => field.name === name);
+ return !!found;
+ }
+
+ get displayableFields(): DisplayableField[] {
+ return this._displayableFields;
+ }
+
+}
diff --git a/src/app/reporting/detail/queryParams/abstract-value-accessor.ts b/src/app/reporting/detail/queryParams/abstract-value-accessor.ts
new file mode 100644
index 0000000..9287659
--- /dev/null
+++ b/src/app/reporting/detail/queryParams/abstract-value-accessor.ts
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ControlValueAccessor} from '@angular/forms';
+
+const noop: any = () => {
+ // empty method
+};
+
+export abstract class AbstractControlValueAccessor implements ControlValueAccessor {
+
+ protected _value: any = undefined;
+
+ get value(): any { return this._value; }
+
+ set value(v: any) {
+ if (v !== this._value) {
+ this._value = v;
+ this.onChange(v);
+ }
+ }
+
+ writeValue(value: any): void {
+ this.value = value;
+ }
+
+ registerOnChange(fn: any): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ this.onTouched = fn;
+ }
+
+ onChange = (_: any) => noop;
+ onTouched = () => noop;
+}
diff --git a/src/app/reporting/detail/queryParams/between/between.component.html b/src/app/reporting/detail/queryParams/between/between.component.html
new file mode 100644
index 0000000..4c053b6
--- /dev/null
+++ b/src/app/reporting/detail/queryParams/between/between.component.html
@@ -0,0 +1,41 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<div layout="column" [formGroup]="formGroup">
+ <div class="pad-bottom-xs">
+ <div class="mat-body-1">{{placeholder}}</div>
+ <div layout="row">
+ <mat-form-field floatPlaceholder="always">
+ <input matInput
+ formControlName="start"
+ placeholder="Start"
+ [attr.type]="type"
+ flex>
+ <mat-error *ngIf="formGroup.get('start').hasError('required')" translate>Required</mat-error>
+ </mat-form-field>
+ <mat-form-field floatPlaceholder="always">
+ <input matInput
+ formControlName="end"
+ placeholder="End"
+ [attr.type]="type"
+ flex>
+ <mat-error *ngIf="formGroup.get('end').hasError('required')" translate>Required</mat-error>
+ <mat-hint class="tc-red-500" *ngIf="formGroup.hasError('rangeInvalid') || formGroup.hasError('greaterThan')" translate>Invalid range</mat-hint>
+ </mat-form-field>
+ </div>
+ </div>
+</div>
diff --git a/src/app/reporting/detail/queryParams/between/between.component.spec.ts b/src/app/reporting/detail/queryParams/between/between.component.spec.ts
new file mode 100644
index 0000000..252f657
--- /dev/null
+++ b/src/app/reporting/detail/queryParams/between/between.component.spec.ts
@@ -0,0 +1,97 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {FormControl, ReactiveFormsModule} from '@angular/forms';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {ReportingBetweenParamComponent} from './between.component';
+import {Component, ViewChild} from '@angular/core';
+import {TranslateModule} from '@ngx-translate/core';
+import {MatInputModule} from '@angular/material';
+
+describe('Test between component', () => {
+
+ let fixture: ComponentFixture<DateTestComponent>;
+
+ let testComponent: DateTestComponent;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot(),
+ ReactiveFormsModule,
+ MatInputModule,
+ NoopAnimationsModule
+ ],
+ declarations: [
+ DateTestComponent,
+ ReportingBetweenParamComponent
+ ]
+ });
+
+ fixture = TestBed.createComponent(DateTestComponent);
+ testComponent = fixture.componentInstance;
+
+ fixture.detectChanges();
+ });
+
+ it('should return same value', () => {
+ const value = '2017-01-01..2017-01-01';
+
+ testComponent.formControl.setValue(value);
+
+ fixture.detectChanges();
+
+ expect(testComponent.formControl.value).toEqual(value);
+ });
+
+ it('should mark control as valid when valid range', () => {
+ const start = '2017-01-01';
+ const end = '2017-01-01';
+
+ testComponent.formControl.setValue(`${start}..${end}`);
+
+ fixture.detectChanges();
+
+ expect(testComponent.formControl.valid).toBeTruthy();
+ });
+
+ it('should mark control as invalid when invalid range', () => {
+ const start = '2017-01-02';
+ const end = '2017-01-01';
+
+ testComponent.formControl.setValue(`${start}..${end}`);
+
+ fixture.detectChanges();
+
+ expect(testComponent.formControl.invalid).toBeTruthy();
+ });
+
+});
+
+@Component({
+ template: `
+ <fims-reporting-between-param #paramComponent [formControl]="formControl" [required]="true" type="DATE">
+ </fims-reporting-between-param>`
+})
+class DateTestComponent {
+
+ @ViewChild('paramComponent') paramComponent: ReportingBetweenParamComponent;
+
+ formControl: FormControl = new FormControl();
+}
diff --git a/src/app/reporting/detail/queryParams/between/between.component.ts b/src/app/reporting/detail/queryParams/between/between.component.ts
new file mode 100644
index 0000000..893baed
--- /dev/null
+++ b/src/app/reporting/detail/queryParams/between/between.component.ts
@@ -0,0 +1,131 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, forwardRef, Input, OnDestroy, OnInit} from '@angular/core';
+import {Type} from '../../../../services/reporting/domain/type.model';
+import {
+ AbstractControl,
+ ControlValueAccessor,
+ FormBuilder,
+ FormGroup,
+ NG_VALIDATORS,
+ NG_VALUE_ACCESSOR,
+ ValidationErrors,
+ Validator,
+ Validators
+} from '@angular/forms';
+import {AbstractControlValueAccessor} from '../abstract-value-accessor';
+import {Observable} from 'rxjs/Observable';
+import {FimsValidators} from '../../../../common/validator/validators';
+import {Subscription} from 'rxjs/Subscription';
+import {Operator} from '../../../../services/reporting/domain/query-parameter.model';
+import {createPlaceholder} from '../query-params.helper';
+
+export const REPORTING_BETWEEN_PARAM_CONTROL_VALUE_ACCESSOR: any = {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => ReportingBetweenParamComponent),
+ multi: true,
+};
+
+export const REPORTING_BETWEEN_PARAM_VALIDATOR: any = {
+ provide: NG_VALIDATORS,
+ useExisting: forwardRef(() => ReportingBetweenParamComponent),
+ multi: true,
+};
+
+@Component({
+ providers: [ REPORTING_BETWEEN_PARAM_CONTROL_VALUE_ACCESSOR, REPORTING_BETWEEN_PARAM_VALIDATOR ],
+ selector: 'fims-reporting-between-param',
+ templateUrl: './between.component.html'
+})
+export class ReportingBetweenParamComponent extends AbstractControlValueAccessor
+ implements ControlValueAccessor, Validator, OnInit, OnDestroy {
+
+ changeSubscription: Subscription;
+
+ formGroup: FormGroup;
+
+ @Input() label: string;
+
+ @Input() required: boolean;
+
+ @Input() type: Type;
+
+ @Input() operator: Operator;
+
+ constructor(private formBuilder: FormBuilder) {
+ super();
+ }
+
+ ngOnInit(): void {
+ const validator = this.type === 'DATE' ? FimsValidators.matchRange('start', 'end') : FimsValidators.greaterThan('start', 'end');
+
+ this.formGroup = this.formBuilder.group({
+ start: ['', this.required ? [Validators.required] : []],
+ end: ['', this.required ? [Validators.required] : []]
+ }, { validator });
+
+ this.changeSubscription = Observable.combineLatest(
+ this.formGroup.get('start').valueChanges,
+ this.formGroup.get('end').valueChanges,
+ (start, end) => ({
+ start,
+ end
+ })
+ ).filter(result => result.start && result.end)
+ .subscribe(result => this.value = this.concatValue(result.start, result.end));
+ }
+
+ ngOnDestroy(): void {
+ this.changeSubscription.unsubscribe();
+ }
+
+ writeValue(value: string): void {
+ if (value) {
+ this.formGroup.setValue(this.splitValue(value));
+ } else {
+ this.formGroup.setValue({
+ start: null,
+ end: null
+ });
+ }
+ }
+
+ private splitValue(value: string): any {
+ const values: string[] = value.split('..');
+
+ if (values.length === 2) {
+ return {
+ start: values[0],
+ end: values[1]
+ };
+ }
+ }
+
+ private concatValue(start: string, end: string): string {
+ return `${start}..${end}`;
+ }
+
+ validate(c: AbstractControl): ValidationErrors {
+ return this.formGroup.errors;
+ }
+
+ get placeholder(): string {
+ return createPlaceholder(this.label, this.required);
+ }
+}
diff --git a/src/app/reporting/detail/queryParams/in/in.component.html b/src/app/reporting/detail/queryParams/in/in.component.html
new file mode 100644
index 0000000..edc54c9
--- /dev/null
+++ b/src/app/reporting/detail/queryParams/in/in.component.html
@@ -0,0 +1,27 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<div class="pad-bottom-xs">
+ <div class="mat-body-1">{{placeholder}}</div>
+ <td-chips [items]="[]"
+ [formControl]="formControl"
+ placeholder="{{'Enter value and hit enter' | translate}}">
+ <ng-template td-chip let-chip="chip">
+ <div class="tc-grey-100 bgc-teal-700" td-chip-avatar>{{chip.substring(0, 1).toUpperCase()}}</div> {{chip}}
+ </ng-template>
+ </td-chips>
+</div>
diff --git a/src/app/reporting/detail/queryParams/in/in.component.spec.ts b/src/app/reporting/detail/queryParams/in/in.component.spec.ts
new file mode 100644
index 0000000..5223ed6
--- /dev/null
+++ b/src/app/reporting/detail/queryParams/in/in.component.spec.ts
@@ -0,0 +1,87 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {CovalentChipsModule} from '@covalent/core';
+import {ReportingInParamComponent} from './in.component';
+import {Component, ViewChild} from '@angular/core';
+import {FormControl, ReactiveFormsModule} from '@angular/forms';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {TranslateModule} from '@ngx-translate/core';
+
+describe('Test in component', () => {
+
+ let fixture: ComponentFixture<TestComponent>;
+
+ let testComponent: TestComponent;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot(),
+ ReactiveFormsModule,
+ CovalentChipsModule,
+ NoopAnimationsModule
+ ],
+ declarations: [
+ TestComponent,
+ ReportingInParamComponent
+ ]
+ });
+
+ fixture = TestBed.createComponent(TestComponent);
+ testComponent = fixture.componentInstance;
+
+ fixture.detectChanges();
+ });
+
+ it('should set comma separated list', () => {
+ testComponent.paramComponent.formControl.setValue(['test1', 'test2']);
+
+ fixture.detectChanges();
+
+ expect(testComponent.formControl.value).toEqual('test1,test2');
+ });
+
+ it('should mark control as valid when value available', () => {
+ testComponent.paramComponent.formControl.setValue(['test1']);
+
+ fixture.detectChanges();
+
+ expect(testComponent.formControl.valid).toBeTruthy();
+ });
+
+ it('should mark control as invalid when no value available', () => {
+ testComponent.paramComponent.formControl.setValue([]);
+
+ fixture.detectChanges();
+
+ expect(testComponent.formControl.invalid).toBeTruthy();
+ });
+
+});
+
+@Component({
+ template: '<fims-reporting-in-param #paramComponent [formControl]="formControl" [required]="true"></fims-reporting-in-param>'
+})
+class TestComponent {
+
+ @ViewChild('paramComponent') paramComponent: ReportingInParamComponent;
+
+ formControl: FormControl = new FormControl();
+}
diff --git a/src/app/reporting/detail/queryParams/in/in.component.ts b/src/app/reporting/detail/queryParams/in/in.component.ts
new file mode 100644
index 0000000..568a3ae
--- /dev/null
+++ b/src/app/reporting/detail/queryParams/in/in.component.ts
@@ -0,0 +1,89 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, forwardRef, Input, OnDestroy, OnInit} from '@angular/core';
+import {Operator} from '../../../../services/reporting/domain/query-parameter.model';
+import {Type} from '../../../../services/reporting/domain/type.model';
+import {
+ AbstractControl,
+ ControlValueAccessor,
+ FormControl,
+ NG_VALIDATORS,
+ NG_VALUE_ACCESSOR,
+ ValidationErrors,
+ Validator,
+ Validators
+} from '@angular/forms';
+import {AbstractControlValueAccessor} from '../abstract-value-accessor';
+import {Subscription} from 'rxjs/Subscription';
+import {createPlaceholder} from '../query-params.helper';
+
+export const REPORTING_IN_PARAM_CONTROL_VALUE_ACCESSOR: any = {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => ReportingInParamComponent),
+ multi: true,
+};
+
+export const REPORTING_IN_PARAM_VALIDATOR: any = {
+ provide: NG_VALIDATORS,
+ useExisting: forwardRef(() => ReportingInParamComponent),
+ multi: true,
+};
+
+@Component({
+ providers: [ REPORTING_IN_PARAM_CONTROL_VALUE_ACCESSOR, REPORTING_IN_PARAM_VALIDATOR ],
+ selector: 'fims-reporting-in-param',
+ templateUrl: './in.component.html'
+})
+export class ReportingInParamComponent extends AbstractControlValueAccessor implements ControlValueAccessor, Validator, OnInit, OnDestroy {
+
+ private changeSubscription: Subscription;
+
+ formControl: FormControl;
+
+ @Input() label: string;
+
+ @Input() required: boolean;
+
+ @Input() type: Type;
+
+ @Input() operator: Operator;
+
+ constructor() {
+ super();
+ }
+
+ ngOnInit(): void {
+ this.formControl = new FormControl([], this.required ? [Validators.required] : []);
+
+ this.changeSubscription = this.formControl.valueChanges
+ .subscribe((value: string[]) => this.value = value.join(','));
+ }
+
+ ngOnDestroy(): void {
+ this.changeSubscription.unsubscribe();
+ }
+
+ validate(c: AbstractControl): ValidationErrors {
+ return this.formControl.errors;
+ }
+
+ get placeholder(): string {
+ return createPlaceholder(this.label, this.required);
+ }
+}
diff --git a/src/app/reporting/detail/queryParams/input/input.component.html b/src/app/reporting/detail/queryParams/input/input.component.html
new file mode 100644
index 0000000..0e66355
--- /dev/null
+++ b/src/app/reporting/detail/queryParams/input/input.component.html
@@ -0,0 +1,26 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<mat-form-field>
+ <span matPrefix class="pad-right-xs">{{prefix}}</span>
+ <input matInput
+ [formControl]="formControl"
+ [placeholder]="placeholder"
+ [attr.type]="type"
+ flex>
+ <mat-error *ngIf="formControl.hasError('required')" translate>Required</mat-error>
+</mat-form-field>
diff --git a/src/app/reporting/detail/queryParams/input/input.component.ts b/src/app/reporting/detail/queryParams/input/input.component.ts
new file mode 100644
index 0000000..fa4021d
--- /dev/null
+++ b/src/app/reporting/detail/queryParams/input/input.component.ts
@@ -0,0 +1,100 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, forwardRef, Input, OnDestroy, OnInit} from '@angular/core';
+import {Type} from '../../../../services/reporting/domain/type.model';
+import {
+ AbstractControl,
+ ControlValueAccessor,
+ FormControl,
+ NG_VALIDATORS,
+ NG_VALUE_ACCESSOR,
+ ValidationErrors,
+ Validator,
+ Validators
+} from '@angular/forms';
+import {Subscription} from 'rxjs/Subscription';
+import {AbstractControlValueAccessor} from '../abstract-value-accessor';
+import {Operator} from '../../../../services/reporting/domain/query-parameter.model';
+import {createPlaceholder} from '../query-params.helper';
+
+const OperatorPrefixMap = {
+ 'EQUALS': 'equals',
+ 'LIKE': 'contains',
+ 'GREATER': 'is greater than',
+ 'LESSER': 'is lesser than'
+};
+
+export const REPORTING_EQUALS_PARAM_CONTROL_VALUE_ACCESSOR: any = {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => ReportingInputParamComponent),
+ multi: true,
+};
+
+export const REPORTING_INPUT_PARAM_VALIDATOR: any = {
+ provide: NG_VALIDATORS,
+ useExisting: forwardRef(() => ReportingInputParamComponent),
+ multi: true,
+};
+
+@Component({
+ providers: [ REPORTING_EQUALS_PARAM_CONTROL_VALUE_ACCESSOR, REPORTING_INPUT_PARAM_VALIDATOR ],
+ templateUrl: './input.component.html'
+})
+export class ReportingInputParamComponent extends AbstractControlValueAccessor
+ implements ControlValueAccessor, Validator, OnInit, OnDestroy {
+
+ private changeSubscription: Subscription;
+
+ formControl: FormControl;
+
+ @Input() label: string;
+
+ @Input() required: boolean;
+
+ @Input() type: Type;
+
+ @Input() operator: Operator;
+
+ constructor() {
+ super();
+ }
+
+ ngOnInit(): void {
+ this.formControl = new FormControl([], this.required ? [Validators.required] : []);
+
+ this.changeSubscription = this.formControl.valueChanges
+ .subscribe(value => this.value = value);
+ }
+
+ ngOnDestroy(): void {
+ this.changeSubscription.unsubscribe();
+ }
+
+ validate(c: AbstractControl): ValidationErrors {
+ return this.formControl.errors;
+ }
+
+ get placeholder(): string {
+ return createPlaceholder(this.label, this.required);
+ }
+
+ get prefix(): string {
+ return OperatorPrefixMap[this.operator];
+ }
+}
diff --git a/src/app/reporting/detail/queryParams/query-param.component.ts b/src/app/reporting/detail/queryParams/query-param.component.ts
new file mode 100644
index 0000000..f6a6425
--- /dev/null
+++ b/src/app/reporting/detail/queryParams/query-param.component.ts
@@ -0,0 +1,119 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+ Component,
+ ComponentFactoryResolver,
+ ComponentRef,
+ Directive,
+ forwardRef,
+ Input,
+ OnInit,
+ ViewChild,
+ ViewContainerRef
+} from '@angular/core';
+import {Operator} from '../../../services/reporting/domain/query-parameter.model';
+import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
+import {Type} from '../../../services/reporting/domain/type.model';
+import {AbstractControlValueAccessor} from './abstract-value-accessor';
+import {ReportingInputParamComponent} from './input/input.component';
+import {ReportingInParamComponent} from './in/in.component';
+import {ReportingBetweenParamComponent} from './between/between.component';
+
+export const ELEMENT_INPUT_CONTROL_VALUE_ACCESSOR: any = {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => ReportingQueryParamComponent),
+ multi: true,
+};
+
+@Directive({
+ selector: '[fimsQueryParamContainer]',
+})
+export class FimsQueryParamDirective {
+ constructor(public viewContainer: ViewContainerRef) { }
+}
+
+@Component({
+ providers: [ELEMENT_INPUT_CONTROL_VALUE_ACCESSOR ],
+ selector: 'fims-reporting-query-param',
+ template: '<div fimsQueryParamContainer></div>'
+})
+export class ReportingQueryParamComponent extends AbstractControlValueAccessor implements ControlValueAccessor, OnInit {
+
+ @Input() label: string;
+
+ @Input() required: boolean;
+
+ @Input() operator: Operator;
+
+ @Input() type: Type;
+
+ @ViewChild(FimsQueryParamDirective) childElement: FimsQueryParamDirective;
+
+ set value(v: any) {
+ if (v !== this._value) {
+ this._value = v;
+ this.onChange(v);
+ }
+ }
+
+ constructor(private componentFactoryResolver: ComponentFactoryResolver) {
+ super();
+ }
+
+ ngOnInit(): void {
+ const ref: ComponentRef<any> = this.componentFactoryResolver
+ .resolveComponentFactory(this.getComponentType(this.operator))
+ .create(this.childElement.viewContainer.injector);
+
+ this.childElement.viewContainer.insert(ref.hostView);
+
+ ref.instance.label = this.label;
+ ref.instance.required = this.required;
+ ref.instance.type = this.type;
+ ref.instance.operator = this.operator;
+
+ ref.instance.registerOnChange((value: any) => {
+ this.value = value;
+ });
+ }
+
+ private getComponentType(operator: Operator): any {
+ switch (operator) {
+ case 'GREATER':
+ case 'LESSER':
+ case 'EQUALS':
+ case 'LIKE': {
+ return ReportingInputParamComponent;
+ }
+
+ case 'IN': {
+ return ReportingInParamComponent;
+ }
+
+ case 'BETWEEN': {
+ return ReportingBetweenParamComponent;
+ }
+
+ default:
+ console.error(`Could not find component for operator: ${operator}`);
+ break;
+ }
+ }
+}
diff --git a/src/app/reporting/detail/queryParams/query-params.component.html b/src/app/reporting/detail/queryParams/query-params.component.html
new file mode 100644
index 0000000..a401e01
--- /dev/null
+++ b/src/app/reporting/detail/queryParams/query-params.component.html
@@ -0,0 +1,27 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<form [formGroup]="form">
+ <ng-container *ngFor="let queryParam of queryParams">
+ <fims-reporting-query-param [formControlName]="queryParam.name"
+ [required]="queryParam.mandatory"
+ [label]="queryParam.name"
+ [type]="queryParam.type"
+ [operator]="queryParam.operator">
+ </fims-reporting-query-param>
+ </ng-container>
+</form>
diff --git a/src/app/reporting/detail/queryParams/query-params.component.spec.ts b/src/app/reporting/detail/queryParams/query-params.component.spec.ts
new file mode 100644
index 0000000..e519550
--- /dev/null
+++ b/src/app/reporting/detail/queryParams/query-params.component.spec.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {By} from '@angular/platform-browser';
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {ReportingQueryParamsComponent} from './query-params.component';
+import {Component, NO_ERRORS_SCHEMA} from '@angular/core';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {QueryParameter} from '../../../services/reporting/domain/query-parameter.model';
+import {FimsSharedModule} from '../../../common/common.module';
+
+describe('Test reporting query params component', () => {
+
+ let fixture: ComponentFixture<ReportingQueryParamsComponent>;
+
+ let testComponent: ReportingQueryParamsComponent;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [
+ ReportingQueryParamsComponent,
+ TestComponent
+ ],
+ imports: [
+ FimsSharedModule,
+ NoopAnimationsModule
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
+ });
+
+ fixture = TestBed.createComponent(ReportingQueryParamsComponent);
+ testComponent = fixture.componentInstance;
+
+ fixture.detectChanges();
+ });
+
+ xit('should render fims-reporting-query-param on the page', () => {
+ const listItems = fixture.debugElement.queryAll(By.css('fims-reporting-query-param'));
+
+ expect(listItems.length).toBe(2);
+ });
+
+});
+
+@Component({
+ template: '<fims-reporting-query-params #mandatoryComponent [queryParams]="queryParams"></fims-reporting-query-params>'
+})
+class TestComponent {
+ queryParams: QueryParameter[] = [
+ { name: 'testa', mandatory: true, type: 'TEXT', operator: 'EQUALS' },
+ { name: 'testb', mandatory: true, type: 'TEXT', operator: 'EQUALS' }
+ ];
+}
diff --git a/src/app/reporting/detail/queryParams/query-params.component.ts b/src/app/reporting/detail/queryParams/query-params.component.ts
new file mode 100644
index 0000000..d9f131b
--- /dev/null
+++ b/src/app/reporting/detail/queryParams/query-params.component.ts
@@ -0,0 +1,56 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input} from '@angular/core';
+import {QueryParameter} from '../../../services/reporting/domain/query-parameter.model';
+import {FormControl, FormGroup} from '@angular/forms';
+import {FormComponent} from '../../../common/forms/form.component';
+
+@Component({
+ selector: 'fims-reporting-query-params',
+ templateUrl: './query-params.component.html'
+})
+export class ReportingQueryParamsComponent extends FormComponent<QueryParameter[]> {
+
+ private _queryParams: QueryParameter[];
+
+ @Input() set queryParams(queryParams: QueryParameter[]) {
+ if (!queryParams) {
+ return;
+ }
+
+ this._queryParams = queryParams;
+
+ queryParams.forEach(queryParam => this.addFormControl(this.form, queryParam));
+ };
+
+ private addFormControl(form: FormGroup, queryParam: QueryParameter): void {
+ form.addControl(queryParam.name, new FormControl(''));
+ }
+
+ get formData(): QueryParameter[] {
+ return this.queryParams.map(queryParam => Object.assign({}, queryParam, {
+ value: this.form.get(queryParam.name).value
+ })
+ );
+ }
+
+ get queryParams(): QueryParameter[] {
+ return this._queryParams;
+ }
+}
diff --git a/src/app/reporting/detail/queryParams/query-params.helper.ts b/src/app/reporting/detail/queryParams/query-params.helper.ts
new file mode 100644
index 0000000..d1cadce
--- /dev/null
+++ b/src/app/reporting/detail/queryParams/query-params.helper.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export function createPlaceholder(label: string, required: boolean): string {
+ const asterisk = required ? '' : '*';
+ return `${label}${asterisk}`;
+}
diff --git a/src/app/reporting/detail/report-page/report-page.component.html b/src/app/reporting/detail/report-page/report-page.component.html
new file mode 100644
index 0000000..b80d0ff
--- /dev/null
+++ b/src/app/reporting/detail/report-page/report-page.component.html
@@ -0,0 +1,42 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<table td-data-table>
+ <thead>
+ <tr td-data-table-column-row>
+ <th td-data-table-column *ngFor="let columnName of reportPage?.header?.columnNames">
+ <span>{{columnName}}</span>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr td-data-table-row *ngFor="let row of reportPage?.rows">
+ <td td-data-table-cell *ngFor="let value of row.values">
+ {{cellValue(value.values)}}
+ </td>
+ </tr>
+ <tr td-data-table-row>
+ <td td-data-table-cell *ngFor="let value of reportPage?.footer?.values">
+ {{cellValue(value.values)}}
+ </td>
+ </tr>
+ </tbody>
+</table>
+
+<div class="mat-padding" *ngIf="!reportPage || reportPage.rows?.length === 0" layout="row" layout-align="center center">
+ <h3 translate>No results to display.</h3>
+</div>
diff --git a/src/app/reporting/detail/report-page/report-page.component.ts b/src/app/reporting/detail/report-page/report-page.component.ts
new file mode 100644
index 0000000..7662f83
--- /dev/null
+++ b/src/app/reporting/detail/report-page/report-page.component.ts
@@ -0,0 +1,33 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input} from '@angular/core';
+import {ReportPage} from '../../../services/reporting/domain/report-page.model';
+
+@Component({
+ selector: 'fims-reporting-report-page',
+ templateUrl: './report-page.component.html'
+})
+export class ReportingReportPageComponent {
+
+ @Input() reportPage: ReportPage;
+
+ cellValue(values: string[]): string {
+ return values.join(', ');
+ }
+}
diff --git a/src/app/reporting/detail/reporting-definition.component.html b/src/app/reporting/detail/reporting-definition.component.html
new file mode 100644
index 0000000..ca40524
--- /dev/null
+++ b/src/app/reporting/detail/reporting-definition.component.html
@@ -0,0 +1,30 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{ 'Report definitions' | translate}}">
+ <fims-two-column-layout>
+ <fims-reporting-criteria #criteria
+ [queryParameter]="queryParameter$ | async"
+ [displayableFields]="displayableFields$ | async"
+ (onGenerateReport)="generateReport($event)"
+ left>
+ </fims-reporting-criteria>
+ <fims-reporting-report-page [reportPage]="reportPage$ | async" right></fims-reporting-report-page>
+ </fims-two-column-layout>
+</fims-layout-card-over>
+
+
diff --git a/src/app/reporting/detail/reporting-definition.component.ts b/src/app/reporting/detail/reporting-definition.component.ts
new file mode 100644
index 0000000..022afa1
--- /dev/null
+++ b/src/app/reporting/detail/reporting-definition.component.ts
@@ -0,0 +1,73 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {ReportDefinition} from '../../services/reporting/domain/report-definition.model';
+import {ActivatedRoute} from '@angular/router';
+import {ReportingService} from '../../services/reporting/reporting.service';
+import {ReportRequest} from '../../services/reporting/domain/report-request.model';
+import {QueryParameter} from '../../services/reporting/domain/query-parameter.model';
+import {ReportPage} from '../../services/reporting/domain/report-page.model';
+import {DisplayableField} from '../../services/reporting/domain/displayable-field.model';
+import {GenerateReportEvent, ReportingCriteriaComponent} from './criteria/criteria.component';
+
+@Component({
+ templateUrl: './reporting-definition.component.html'
+})
+export class ReportingDefinitionComponent implements OnInit {
+
+ private category: string;
+
+ private identifier: string;
+
+ @ViewChild('criteria') criteriaComponent: ReportingCriteriaComponent;
+
+ queryParameter$: Observable<QueryParameter[]>;
+
+ displayableFields$: Observable<DisplayableField[]>;
+
+ reportPage$: Observable<ReportPage>;
+
+ constructor(private route: ActivatedRoute, private reportingService: ReportingService) {}
+
+ ngOnInit(): void {
+ const reportDefinition$: Observable<ReportDefinition> = this.route.params
+ .do(params => {
+ this.category = params['category'];
+ this.identifier = params['identifier'];
+ })
+ .switchMap(params => this.reportingService.findReportDefinition(params['category'], params['identifier']));
+
+ this.queryParameter$ = reportDefinition$
+ .map(reportDefinition => reportDefinition.queryParameters);
+
+ this.displayableFields$ = reportDefinition$
+ .map(reportDefinition => reportDefinition.displayableFields);
+ }
+
+ generateReport(event: GenerateReportEvent): void {
+ const reportRequest: ReportRequest = {
+ displayableFields: event.displayableFields,
+ queryParameters: event.queryParameter
+ };
+
+ this.reportPage$ = this.reportingService.generateReport(this.category, this.identifier, reportRequest);
+ }
+}
diff --git a/src/app/reporting/reporting-definitions.component.html b/src/app/reporting/reporting-definitions.component.html
new file mode 100644
index 0000000..7554cc3
--- /dev/null
+++ b/src/app/reporting/reporting-definitions.component.html
@@ -0,0 +1,26 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<mat-nav-list>
+ <h3 mat-subheader>Reports</h3>
+ <mat-list-item *ngFor="let definition of reportDefinitions$ | async">
+ <a matLine (click)="goToReport(definition.identifier)">{{ definition.name }}</a>
+ <mat-icon>chevron_right</mat-icon>
+ </mat-list-item>
+</mat-nav-list>
+
+
diff --git a/src/app/reporting/reporting-definitions.component.spec.ts b/src/app/reporting/reporting-definitions.component.spec.ts
new file mode 100644
index 0000000..e6b7d8a
--- /dev/null
+++ b/src/app/reporting/reporting-definitions.component.spec.ts
@@ -0,0 +1,97 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ComponentFixture, inject, TestBed} from '@angular/core/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {ActivatedRoute, Router} from '@angular/router';
+import {ReportingService} from '../services/reporting/reporting.service';
+import {Observable} from 'rxjs/Observable';
+import {By} from '@angular/platform-browser';
+import {ReportingDefinitionsComponent} from './reporting-definitions.component';
+import {ReportDefinition} from '../services/reporting/domain/report-definition.model';
+import {ActivatedRouteStub} from '../common/testing/router-stubs';
+import {FimsSharedModule} from '../common/common.module';
+import {MatLine, MatListModule, MatToolbarModule} from '@angular/material';
+
+const definitions: ReportDefinition[] = [
+ { identifier: 'reportOne', name: '', description: '', displayableFields: [], queryParameters: [] },
+ { identifier: 'reportTwo', name: '', description: '', displayableFields: [], queryParameters: [] }
+];
+
+describe('Test reporting definitions component', () => {
+
+ let activatedRoute: ActivatedRouteStub;
+
+ let fixture: ComponentFixture<ReportingDefinitionsComponent>;
+
+ let testComponent: ReportingDefinitionsComponent;
+
+ beforeEach(() => {
+ activatedRoute = new ActivatedRouteStub();
+
+ activatedRoute.testParams = { category: 'categoryOne' };
+
+ TestBed.configureTestingModule({
+ declarations: [
+ ReportingDefinitionsComponent
+ ],
+ imports: [
+ TranslateModule.forRoot(),
+ FimsSharedModule,
+ MatToolbarModule,
+ MatListModule,
+ NoopAnimationsModule
+ ],
+ providers: [
+ {provide: Router, useValue: jasmine.createSpyObj('Router', ['navigate'])},
+ {provide: ActivatedRoute, useValue: activatedRoute },
+ {provide: ReportingService, useValue: jasmine.createSpyObj('reportingService', ['fetchReportDefinitions'])}
+ ]
+ });
+
+ fixture = TestBed.createComponent(ReportingDefinitionsComponent);
+ testComponent = fixture.componentInstance;
+
+ const reportingService = TestBed.get(ReportingService);
+ reportingService.fetchReportDefinitions.and.returnValue(Observable.of(definitions));
+
+ fixture.detectChanges();
+ });
+
+ it('should render mat-list-items on the page', () => {
+ const listItems = fixture.debugElement.queryAll(By.directive(MatLine));
+
+ expect(listItems.length).toBe(2);
+ });
+
+ it('should navigate to report definitions page', inject([Router, ActivatedRoute], (router: Router, route: ActivatedRoute) => {
+ const listItems = fixture.debugElement.queryAll(By.directive(MatLine));
+
+ listItems[1].nativeElement.click();
+
+ fixture.detectChanges();
+
+ expect(router.navigate).toHaveBeenCalledWith(['reports', 'reportTwo'], { relativeTo: route });
+ }));
+
+
+ it('should call the service with the right param', inject([ReportingService], (service: ReportingService) => {
+ expect(service.fetchReportDefinitions).toHaveBeenCalledWith('categoryOne');
+ }));
+});
diff --git a/src/app/reporting/reporting-definitions.component.ts b/src/app/reporting/reporting-definitions.component.ts
new file mode 100644
index 0000000..1f6742d
--- /dev/null
+++ b/src/app/reporting/reporting-definitions.component.ts
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import {ReportingService} from '../services/reporting/reporting.service';
+import {Observable} from 'rxjs/Observable';
+import {ActivatedRoute, Router} from '@angular/router';
+import {ReportDefinition} from '../services/reporting/domain/report-definition.model';
+
+@Component({
+ templateUrl: './reporting-definitions.component.html'
+})
+export class ReportingDefinitionsComponent implements OnInit {
+
+ reportDefinitions$: Observable<ReportDefinition[]>;
+
+ constructor(private router: Router, private route: ActivatedRoute, private reportingService: ReportingService) {}
+
+ ngOnInit(): void {
+ this.reportDefinitions$ = this.route.params
+ .switchMap(params => this.reportingService.fetchReportDefinitions(params['category']));
+ }
+
+ goToReport(identifier: string): void {
+ this.router.navigate(['reports', identifier], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/reporting/reporting.component.html b/src/app/reporting/reporting.component.html
new file mode 100644
index 0000000..7bb5429
--- /dev/null
+++ b/src/app/reporting/reporting.component.html
@@ -0,0 +1,33 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{ 'Report categories' | translate}}">
+ <div layout-gt-xs="row" flex layout-align="center center">
+ <div flex-gt-xs="50">
+ <mat-nav-list>
+ <h3 mat-subheader>Categories</h3>
+ <a mat-list-item *ngFor="let category of categories$ | async" [routerLink]="['categories', category]" routerLinkActive="active">
+ <h3 matLine>{{ category }}</h3>
+ <mat-icon>chevron_right</mat-icon>
+ </a>
+ </mat-nav-list>
+ </div>
+ <div flex-gt-xs="50">
+ <router-outlet></router-outlet>
+ </div>
+ </div>
+</fims-layout-card-over>
diff --git a/src/app/reporting/reporting.component.spec.ts b/src/app/reporting/reporting.component.spec.ts
new file mode 100644
index 0000000..dddf418
--- /dev/null
+++ b/src/app/reporting/reporting.component.spec.ts
@@ -0,0 +1,91 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {ReportingComponent} from './reporting.component';
+import {TranslateModule} from '@ngx-translate/core';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {ActivatedRoute, Router} from '@angular/router';
+import {ReportingService} from '../services/reporting/reporting.service';
+import {Observable} from 'rxjs/Observable';
+import {By} from '@angular/platform-browser';
+import {FimsSharedModule} from '../common/common.module';
+import {RouterLinkStubDirective, RouterOutletStubComponent} from '../common/testing/router-stubs';
+import {MatListModule, MatToolbarModule} from '@angular/material';
+
+describe('Test reporting component', () => {
+
+ const activatedRoute: ActivatedRoute = undefined;
+
+ let fixture: ComponentFixture<ReportingComponent>;
+
+ let testComponent: ReportingComponent;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [
+ ReportingComponent,
+ RouterLinkStubDirective,
+ RouterOutletStubComponent
+ ],
+ imports: [
+ TranslateModule.forRoot(),
+ FimsSharedModule,
+ MatListModule,
+ MatToolbarModule,
+ NoopAnimationsModule
+ ],
+ providers: [
+ {provide: Router, useValue: jasmine.createSpyObj('Router', ['navigate'])},
+ {provide: ActivatedRoute, useValue: activatedRoute},
+ {provide: ReportingService, useValue: jasmine.createSpyObj('reportingService', ['fetchCategories'])}
+ ]
+ });
+
+ fixture = TestBed.createComponent(ReportingComponent);
+ testComponent = fixture.componentInstance;
+
+ const reportingService = TestBed.get(ReportingService);
+ reportingService.fetchCategories.and.returnValue(Observable.of(['categoryOne', 'categoryTwo']));
+
+ fixture.detectChanges();
+ });
+
+ it('should render mat-list-items on the page', () => {
+ const listItems = fixture.debugElement.queryAll(By.css('a[mat-list-item]'));
+
+ expect(listItems.length).toBe(2);
+ });
+
+ it('should navigate to report definitions page', () => {
+ const listItems = fixture.debugElement.queryAll(By.css('a[mat-list-item]'));
+
+ listItems[0].nativeElement.click();
+
+ fixture.detectChanges();
+
+ const linkDebugs = fixture.debugElement
+ .queryAll(By.directive(RouterLinkStubDirective));
+
+ const links = linkDebugs
+ .map(de => de.injector.get(RouterLinkStubDirective) as RouterLinkStubDirective);
+
+ expect(links[0].navigatedTo).toEqual(['categories', 'categoryOne']);
+ });
+
+});
diff --git a/src/app/reporting/reporting.component.ts b/src/app/reporting/reporting.component.ts
new file mode 100644
index 0000000..3080af0
--- /dev/null
+++ b/src/app/reporting/reporting.component.ts
@@ -0,0 +1,36 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import {ReportingService} from '../services/reporting/reporting.service';
+import {Observable} from 'rxjs/Observable';
+
+@Component({
+ templateUrl: './reporting.component.html'
+})
+export class ReportingComponent implements OnInit {
+
+ categories$: Observable<string[]>;
+
+ constructor(private reportingService: ReportingService) {}
+
+ ngOnInit(): void {
+ this.categories$ = this.reportingService.fetchCategories();
+ }
+
+}
diff --git a/src/app/reporting/reporting.module.ts b/src/app/reporting/reporting.module.ts
new file mode 100644
index 0000000..ad9d428
--- /dev/null
+++ b/src/app/reporting/reporting.module.ts
@@ -0,0 +1,91 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {CovalentChipsModule, CovalentDataTableModule, CovalentExpansionPanelModule, CovalentStepsModule} from '@covalent/core';
+import {
+ MatButtonModule,
+ MatCardModule,
+ MatIconModule,
+ MatInputModule,
+ MatListModule,
+ MatTabsModule,
+ MatToolbarModule
+} from '@angular/material';
+import {CommonModule} from '@angular/common';
+import {TranslateModule} from '@ngx-translate/core';
+import {FimsSharedModule} from '../common/common.module';
+import {NgModule, Type} from '@angular/core';
+import {RouterModule} from '@angular/router';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {ReportingRoutes} from './reporting.routes';
+import {ReportingDefinitionsComponent} from './reporting-definitions.component';
+import {ReportingDefinitionComponent} from './detail/reporting-definition.component';
+import {ReportingQueryParamsComponent} from './detail/queryParams/query-params.component';
+import {FimsQueryParamDirective, ReportingQueryParamComponent} from './detail/queryParams/query-param.component';
+import {ReportingBetweenParamComponent} from './detail/queryParams/between/between.component';
+import {ReportingInputParamComponent} from './detail/queryParams/input/input.component';
+import {ReportingInParamComponent} from './detail/queryParams/in/in.component';
+import {ReportingComponent} from './reporting.component';
+import {ReportingReportPageComponent} from './detail/report-page/report-page.component';
+import {ReportingDisplayableFieldsComponent} from './detail/displayableFields/displayable-fields.component';
+import {ReportingCriteriaComponent} from './detail/criteria/criteria.component';
+
+const QUERY_PARAM_COMPONENTS: Type<any>[] = [
+ ReportingBetweenParamComponent,
+ ReportingInputParamComponent,
+ ReportingInParamComponent
+];
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(ReportingRoutes),
+ FimsSharedModule,
+ TranslateModule,
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ MatCardModule,
+ MatIconModule,
+ MatToolbarModule,
+ MatInputModule,
+ MatButtonModule,
+ MatTabsModule,
+ MatListModule,
+ CovalentStepsModule,
+ CovalentDataTableModule,
+ CovalentChipsModule,
+ CovalentExpansionPanelModule
+ ],
+ declarations: [
+ ReportingComponent,
+ ReportingDefinitionsComponent,
+ ReportingDefinitionComponent,
+ ReportingCriteriaComponent,
+ ReportingQueryParamsComponent,
+ ReportingQueryParamComponent,
+ ReportingReportPageComponent,
+ ReportingDisplayableFieldsComponent,
+ QUERY_PARAM_COMPONENTS,
+ FimsQueryParamDirective
+ ],
+ entryComponents: [
+ QUERY_PARAM_COMPONENTS
+ ]
+})
+export class ReportingModule {}
diff --git a/src/app/reporting/reporting.routes.ts b/src/app/reporting/reporting.routes.ts
new file mode 100644
index 0000000..300bd2e
--- /dev/null
+++ b/src/app/reporting/reporting.routes.ts
@@ -0,0 +1,39 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Routes} from '@angular/router';
+import {ReportingComponent} from './reporting.component';
+import {ReportingDefinitionsComponent} from './reporting-definitions.component';
+import {ReportingDefinitionComponent} from './detail/reporting-definition.component';
+
+export const ReportingRoutes: Routes = [
+ {
+ path: '',
+ component: ReportingComponent,
+ children: [
+ {
+ path: 'categories/:category',
+ component: ReportingDefinitionsComponent
+ }
+ ]
+ },
+ {
+ path: 'categories/:category/reports/:identifier',
+ component: ReportingDefinitionComponent
+ }
+];
diff --git a/src/app/roles/components/permission-list-item.component.html b/src/app/roles/components/permission-list-item.component.html
new file mode 100644
index 0000000..1d311e4
--- /dev/null
+++ b/src/app/roles/components/permission-list-item.component.html
@@ -0,0 +1,26 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<mat-list-item>
+ <mat-icon matListAvatar>lock</mat-icon>
+ <h3 matLine>{{formPermission.label}}</h3>
+ <h4 matLine></h4>
+ <p matLine></p>
+ <mat-checkbox [(ngModel)]="formPermission.read" class="list-checkbox" [disabled]="readOnly" translate>Read</mat-checkbox>
+ <mat-checkbox [(ngModel)]="formPermission.change" class="list-checkbox" [disabled]="readOnly" translate>Change</mat-checkbox>
+ <mat-checkbox [(ngModel)]="formPermission.remove" class="list-checkbox" [disabled]="readOnly" translate>Delete</mat-checkbox>
+</mat-list-item>
diff --git a/src/app/roles/components/permission-list-item.component.scss b/src/app/roles/components/permission-list-item.component.scss
new file mode 100644
index 0000000..db936c4
--- /dev/null
+++ b/src/app/roles/components/permission-list-item.component.scss
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+
+.list-checkbox{
+ margin: 10px
+}
diff --git a/src/app/roles/components/permission-list-item.component.ts b/src/app/roles/components/permission-list-item.component.ts
new file mode 100644
index 0000000..a764fd5
--- /dev/null
+++ b/src/app/roles/components/permission-list-item.component.ts
@@ -0,0 +1,40 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input} from '@angular/core';
+import {FormPermission} from '../model/form-permission.model';
+
+@Component({
+ selector: 'fims-permission-list-item',
+ templateUrl: './permission-list-item.component.html',
+ styleUrls: ['./permission-list-item.component.scss']
+})
+export class PermissionListItemComponent {
+
+ private _readOnly: boolean;
+
+ @Input() formPermission: FormPermission;
+
+ @Input() set readOnly(readOnly: boolean) {
+ this._readOnly = readOnly;
+ };
+
+ get readOnly(): boolean {
+ return this._readOnly || this.formPermission.readOnly;
+ }
+}
diff --git a/src/app/roles/detail/role.detail.component.html b/src/app/roles/detail/role.detail.component.html
new file mode 100644
index 0000000..dc81837
--- /dev/null
+++ b/src/app/roles/detail/role.detail.component.html
@@ -0,0 +1,31 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{ role.identifier }}" [navigateBackTo]="'../../'" *ngIf="(role$ | async) as role">
+ <fims-layout-card-over-header-menu>
+ <button mat-icon-button (click)="deleteRole(role)" title="{{'Delete this role' | translate}}" *hasPermission="{ id: 'identity_roles', accessLevel: 'DELETE' }"><mat-icon>delete</mat-icon></button>
+ </fims-layout-card-over-header-menu>
+ <mat-list>
+ <h3 mat-subheader translate>Permissions</h3>
+ <ng-container *ngFor="let group of permissionGroup$ | async">
+ <h3 mat-subheader translate>{{group.groupId}}</h3>
+ <fims-permission-list-item *ngFor="let permission of group.formPermissions" [formPermission]="permission" [readOnly]="true"></fims-permission-list-item>
+ <mat-divider></mat-divider>
+ </ng-container>
+ </mat-list>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Edit role' | translate}}" icon="mode_edit" [link]="['edit']" [permission]="{ id: 'identity_roles', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/roles/detail/role.detail.component.ts b/src/app/roles/detail/role.detail.component.ts
new file mode 100644
index 0000000..75267f5
--- /dev/null
+++ b/src/app/roles/detail/role.detail.component.ts
@@ -0,0 +1,85 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {RolesStore} from '../store/index';
+import * as fromRoles from '../store';
+import {Role} from '../../services/identity/domain/role.model';
+import {DELETE, SelectAction} from '../store/role.actions';
+import {Subscription} from 'rxjs/Subscription';
+import {ActivatedRoute} from '@angular/router';
+import {IdentityService} from '../../services/identity/identity.service';
+import {PermittableGroup} from '../../services/anubis/permittable-group.model';
+import {Observable} from 'rxjs/Observable';
+import {FormPermissionService} from '../helper/form-permission.service';
+import {TdDialogService} from '@covalent/core';
+import {FormPermissionGroup} from '../model/form-permission-group.model';
+
+@Component({
+ templateUrl: './role.detail.component.html'
+})
+export class RoleDetailComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ role$: Observable<Role>;
+
+ permissionGroup$: Observable<FormPermissionGroup[]>;
+
+ constructor(private route: ActivatedRoute, private identityService: IdentityService, private store: RolesStore,
+ private formPermissionService: FormPermissionService, private dialogService: TdDialogService) {}
+
+ ngOnInit(): void {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectAction(params['id']))
+ .subscribe(this.store);
+
+ this.role$ = this.store.select(fromRoles.getSelectedRole)
+ .filter(role => !!role);
+
+ this.permissionGroup$ = Observable.combineLatest(
+ this.identityService.getPermittableGroups(),
+ this.role$,
+ (groups: PermittableGroup[], role: Role) => this.formPermissionService.mapToFormPermissions(groups, role.permissions)
+ );
+ }
+
+ confirmDeletion(): Observable<boolean> {
+ return this.dialogService.openConfirm({
+ message: 'Do you want to delete this role?',
+ title: 'Confirm deletion',
+ acceptButton: 'DELETE ROLE',
+ }).afterClosed();
+ }
+
+ deleteRole(role: Role): void {
+ this.confirmDeletion()
+ .filter(accept => accept)
+ .subscribe(() => {
+ this.store.dispatch({ type: DELETE, payload: {
+ role,
+ activatedRoute: this.route
+ } });
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ }
+}
diff --git a/src/app/roles/form/create/create.form.component.html b/src/app/roles/form/create/create.form.component.html
new file mode 100644
index 0000000..50c3189
--- /dev/null
+++ b/src/app/roles/form/create/create.form.component.html
@@ -0,0 +1,24 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="Create new role">
+ <fims-role-form-component #form
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [role]="role">
+ </fims-role-form-component>
+</fims-layout-card-over>
diff --git a/src/app/roles/form/create/create.form.component.ts b/src/app/roles/form/create/create.form.component.ts
new file mode 100644
index 0000000..8b1d4d1
--- /dev/null
+++ b/src/app/roles/form/create/create.form.component.ts
@@ -0,0 +1,72 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Role} from '../../../services/identity/domain/role.model';
+import * as fromRoles from '../../store';
+import {Subscription} from 'rxjs/Subscription';
+import {Error} from '../../../services/domain/error.model';
+import {RolesStore} from '../../store/index';
+import {CREATE, RESET_FORM} from '../../store/role.actions';
+
+@Component({
+ templateUrl: './create.form.component.html'
+})
+export class CreateRoleFormComponent implements OnInit, OnDestroy {
+
+ private formStateSubscription: Subscription;
+
+ role: Role = { identifier: '', permissions: []};
+
+ @ViewChild('form') formComponent;
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: RolesStore) {}
+
+ ngOnInit(): void {
+ this.formStateSubscription = this.store.select(fromRoles.getRoleFormError)
+ .filter((error: Error) => !!error)
+ .subscribe((error: Error) => {
+ const detailForm = this.formComponent.detailForm;
+ const errors = detailForm.get('identifier').errors || {};
+ errors['unique'] = true;
+ detailForm.get('identifier').setErrors(errors);
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.formStateSubscription.unsubscribe();
+
+ this.store.dispatch({ type: RESET_FORM });
+ }
+
+ onSave(role: Role): void {
+ this.store.dispatch({ type: CREATE, payload: {
+ role,
+ activatedRoute: this.route
+ } });
+ }
+
+ onCancel(): void {
+ this.navigateAway();
+ }
+
+ navigateAway(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/roles/form/edit/edit.form.component.html b/src/app/roles/form/edit/edit.form.component.html
new file mode 100644
index 0000000..f669a51
--- /dev/null
+++ b/src/app/roles/form/edit/edit.form.component.html
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Edit role' | translate}}">
+ <fims-role-form-component #form
+ [editMode]="true"
+ (onSave)="onSave($event)"
+ (onCancel)="onCancel()"
+ [role]="role">
+ </fims-role-form-component>
+</fims-layout-card-over>
diff --git a/src/app/roles/form/edit/edit.form.component.ts b/src/app/roles/form/edit/edit.form.component.ts
new file mode 100644
index 0000000..6134f1e
--- /dev/null
+++ b/src/app/roles/form/edit/edit.form.component.ts
@@ -0,0 +1,65 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Role} from '../../../services/identity/domain/role.model';
+import * as fromRoles from '../../store';
+import {Subscription} from 'rxjs/Subscription';
+import {SelectAction, UPDATE} from '../../store/role.actions';
+import {RolesStore} from '../../store/index';
+
+@Component({
+ templateUrl: './edit.form.component.html'
+})
+export class EditRoleFormComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+ private roleSubscription: Subscription;
+
+ role: Role;
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: RolesStore) {}
+
+ ngOnInit(): void {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectAction(params['id']))
+ .subscribe(this.store);
+
+ this.roleSubscription = this.store.select(fromRoles.getSelectedRole).subscribe((role: Role) => {
+ this.role = role;
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ this.roleSubscription.unsubscribe();
+ }
+
+ onSave(role: Role): void {
+ this.store.dispatch({ type: UPDATE, payload: {
+ role,
+ activatedRoute: this.route
+ } });
+ }
+
+ onCancel(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+
+}
diff --git a/src/app/roles/form/form.component.html b/src/app/roles/form/form.component.html
new file mode 100644
index 0000000..0c2c3ac
--- /dev/null
+++ b/src/app/roles/form/form.component.html
@@ -0,0 +1,48 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<div class="inset">
+ <div layout="row">
+ <form [formGroup]="detailForm">
+ <div layout="row">
+ <fims-id-input [form]="detailForm" controlName="identifier" [readonly]="editMode"></fims-id-input>
+ </div>
+ </form>
+ </div>
+ <div layout="row" layout-margin>
+ <mat-list>
+ <mat-list-item>
+ <h3 matLine></h3>
+ <h4 matLine></h4>
+ <p matLine></p>
+ <mat-checkbox [(ngModel)]="allRead" class="list-checkbox" translate>Read</mat-checkbox>
+ <mat-checkbox [(ngModel)]="allChange" class="list-checkbox" translate>Change</mat-checkbox>
+ <mat-checkbox [(ngModel)]="allRemove" class="list-checkbox" translate>Delete</mat-checkbox>
+ </mat-list-item>
+ <mat-divider></mat-divider>
+ <ng-container *ngFor="let group of permissionGroups">
+ <h3 mat-subheader translate>{{group.groupId}}</h3>
+ <fims-permission-list-item *ngFor="let permission of group.formPermissions" [formPermission]="permission" [readOnly]="false"></fims-permission-list-item>
+ <mat-divider></mat-divider>
+ </ng-container>
+ </mat-list>
+ </div>
+ <div layout="row" layout-margin>
+ <button type="submit" mat-raised-button color="primary" [disabled]="detailForm.invalid" (click)="save()">{{'SAVE ROLE' | translate}}</button>
+ <button (click)="cancel()" mat-button>{{'CANCEL' | translate}}</button>
+ </div>
+</div>
diff --git a/src/app/roles/form/form.component.scss b/src/app/roles/form/form.component.scss
new file mode 100644
index 0000000..a84a439
--- /dev/null
+++ b/src/app/roles/form/form.component.scss
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+.list-checkbox{
+ margin: 10px
+}
diff --git a/src/app/roles/form/form.component.spec.ts b/src/app/roles/form/form.component.spec.ts
new file mode 100644
index 0000000..f3a5805
--- /dev/null
+++ b/src/app/roles/form/form.component.spec.ts
@@ -0,0 +1,143 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {RoleFormComponent} from './form.component';
+import {PermittableGroup} from '../../services/anubis/permittable-group.model';
+import {Observable} from 'rxjs/Observable';
+import {IdentityService} from '../../services/identity/identity.service';
+import {Role} from '../../services/identity/domain/role.model';
+import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
+import {IdInputComponent} from '../../common/id-input/id-input.component';
+import {PermittableGroupIdMapper} from '../../services/security/authz/permittable-group-id-mapper';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {FormPermissionService} from '../helper/form-permission.service';
+import {PermissionListItemComponent} from '../components/permission-list-item.component';
+import {MatCheckboxModule, MatIconModule, MatInputModule} from '@angular/material';
+
+class FakeLoader implements TranslateLoader {
+ getTranslation(lang: string): Observable<any> {
+ return Observable.of({});
+ }
+}
+
+describe('Test roles form', () => {
+
+ let fixture: ComponentFixture<RoleFormComponent>;
+ let component: RoleFormComponent;
+
+ const officePermittable: PermittableGroup = {
+ identifier: 'office__v1__offices',
+ permittables: [
+ {path: '/offices', method: 'POST'},
+ {path: '/offices', method: 'PUT'}
+ ]
+ };
+
+ const identityService = {
+ getPermittableGroups(): Observable<PermittableGroup[]> {
+ const permittableGroups: PermittableGroup[] = [];
+ permittableGroups.push(officePermittable);
+ return Observable.of(permittableGroups);
+ }
+ };
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [
+ RoleFormComponent,
+ IdInputComponent,
+ PermissionListItemComponent
+ ],
+ imports: [
+ NoopAnimationsModule,
+ TranslateModule.forRoot(),
+ MatInputModule,
+ MatIconModule,
+ MatCheckboxModule,
+ FormsModule,
+ ReactiveFormsModule
+ ],
+ providers: [
+ {provide: IdentityService, useValue: identityService},
+ FormPermissionService,
+ PermittableGroupIdMapper
+ ]
+ });
+
+ fixture = TestBed.createComponent(RoleFormComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should save same role with given permission groups', async(() => {
+ component.role = {
+ identifier: 'test',
+ permissions: [{
+ permittableEndpointGroupIdentifier: officePermittable.identifier,
+ allowedOperations: ['READ', 'CHANGE']
+ }]
+ };
+
+ fixture.detectChanges();
+
+ // Wait for async service call
+ fixture.whenStable().then(() => {
+ component.onSave.subscribe((role: Role) => {
+ const expected: Role = {
+ identifier: 'test',
+ permissions: [{
+ permittableEndpointGroupIdentifier: officePermittable.identifier,
+ allowedOperations: ['READ', 'CHANGE']
+ }]
+ };
+ expect(JSON.stringify(role)).toBe(JSON.stringify(expected));
+ });
+ component.save();
+ });
+ }));
+
+ it('should save changed role when changed', async(() => {
+ component.role = {identifier: 'test', permissions: [
+ {
+ permittableEndpointGroupIdentifier: officePermittable.identifier,
+ allowedOperations: ['READ', 'CHANGE', 'DELETE']
+ }
+ ]};
+
+ fixture.detectChanges();
+
+ // Wait for async service call
+ fixture.whenStable().then(() => {
+ const formPermission = component.formPermissions[0];
+ formPermission.change = false;
+
+ component.onSave.subscribe((role: Role) => {
+ const expected: Role = {
+ identifier: 'test',
+ permissions: [{
+ permittableEndpointGroupIdentifier: officePermittable.identifier,
+ allowedOperations: ['READ']
+ }]
+ };
+ expect(JSON.stringify(role)).toBe(JSON.stringify(expected));
+ });
+ component.save();
+ });
+ }));
+});
diff --git a/src/app/roles/form/form.component.ts b/src/app/roles/form/form.component.ts
new file mode 100644
index 0000000..720b3a8
--- /dev/null
+++ b/src/app/roles/form/form.component.ts
@@ -0,0 +1,124 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnDestroy, OnInit, Output} from '@angular/core';
+import {FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {Role} from '../../services/identity/domain/role.model';
+import {PermittableGroup} from '../../services/anubis/permittable-group.model';
+import {IdentityService} from '../../services/identity/identity.service';
+import {FormPermission} from '../model/form-permission.model';
+import {FimsValidators} from '../../common/validator/validators';
+import {FormPermissionService} from '../helper/form-permission.service';
+import {FormPermissionGroup} from '../model/form-permission-group.model';
+import {Subscription} from 'rxjs/Subscription';
+
+@Component({
+ selector: 'fims-role-form-component',
+ templateUrl: './form.component.html',
+ styleUrls: ['./form.component.scss']
+})
+export class RoleFormComponent implements OnInit, OnDestroy {
+
+ private permittableGroupSubscription: Subscription;
+
+ private _role: Role;
+
+ permissionGroups: FormPermissionGroup[] = [];
+
+ formPermissions: FormPermission[] = [];
+
+ detailForm: FormGroup;
+
+ @Input() editMode: boolean;
+
+ @Input('role') set role(role: Role) {
+ this._role = role;
+ this.prepareForm(role);
+ }
+
+ @Output('onSave') onSave = new EventEmitter<Role>();
+
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder, private identityService: IdentityService,
+ private formPermissionService: FormPermissionService) {}
+
+ ngOnInit(): void {
+ this.permittableGroupSubscription = this.identityService.getPermittableGroups()
+ .subscribe((groups: PermittableGroup[]) => {
+ this.permissionGroups = this.formPermissionService.mapToFormPermissions(groups, this._role.permissions);
+
+ this.permissionGroups.forEach(group => {
+ this.formPermissions.push(...group.formPermissions);
+ });
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.permittableGroupSubscription.unsubscribe();
+ }
+
+ private prepareForm(role: Role): void {
+ this.detailForm = this.formBuilder.group({
+ identifier: [role.identifier, [Validators.required, Validators.minLength(3), Validators.maxLength(32), FimsValidators.urlSafe]]
+ });
+ }
+
+ save(): void {
+ const identifier = this.detailForm.get('identifier').value;
+ this.onSave.emit(this.formPermissionService.mapToRole(identifier, this.formPermissions));
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+ set allRead(checked: boolean) {
+ for (const formPermission of this.formPermissions) {
+ formPermission.read = checked;
+ }
+ }
+
+ set allChange(checked: boolean) {
+ for (const formPermission of this.formPermissions) {
+ formPermission.change = checked;
+ }
+ }
+
+ set allRemove(checked: boolean) {
+ for (const formPermission of this.formPermissions) {
+ formPermission.remove = checked;
+ }
+ }
+
+ get allRead(): boolean {
+ const found: FormPermission = this.formPermissions.find(formPermission => !formPermission.read);
+ return !found;
+ }
+
+ get allChange(): boolean {
+ const found: FormPermission = this.formPermissions.find(formPermission => !formPermission.change);
+ return !found;
+ }
+
+ get allRemove(): boolean {
+ const found: FormPermission = this.formPermissions.find(formPermission => !formPermission.remove);
+ return !found;
+ }
+
+}
diff --git a/src/app/roles/helper/form-permission.service.ts b/src/app/roles/helper/form-permission.service.ts
new file mode 100644
index 0000000..3e0e496
--- /dev/null
+++ b/src/app/roles/helper/form-permission.service.ts
@@ -0,0 +1,123 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {FormPermission} from '../model/form-permission.model';
+import {AllowedOperation, Permission} from '../../services/identity/domain/permission.model';
+import {PermittableGroup} from '../../services/anubis/permittable-group.model';
+import {FimsPermissionDescriptor} from '../../services/security/authz/fims-permission-descriptor';
+import {Injectable} from '@angular/core';
+import {PermittableGroupIdMapper} from '../../services/security/authz/permittable-group-id-mapper';
+import {Role} from '../../services/identity/domain/role.model';
+import {FormPermissionGroup} from '../model/form-permission-group.model';
+
+@Injectable()
+export class FormPermissionService {
+
+ constructor(private idMapper: PermittableGroupIdMapper) {}
+
+ mapToFormPermissions(groups: PermittableGroup[], permissions: Permission[]): FormPermissionGroup[] {
+ const formGroups: FormPermissionGroup[] = [];
+
+ for (const permittableGroup of groups) {
+ const groupId = this.groupId(permittableGroup);
+
+ let foundGroup = formGroups.find(group => group.groupId === groupId);
+
+ if (!foundGroup) {
+ foundGroup = {
+ groupId,
+ formPermissions: []
+ };
+ formGroups.push(foundGroup);
+ }
+
+ const foundPermission: Permission = this.findPermission(permittableGroup.identifier, permissions);
+
+ const descriptor: FimsPermissionDescriptor = this.idMapper.map(permittableGroup.identifier);
+
+ if (!descriptor) {
+ continue;
+ }
+
+ const formPermission = new FormPermission(permittableGroup.identifier);
+
+ formPermission.label = descriptor.label;
+ formPermission.readOnly = descriptor.readOnly;
+
+ if (foundPermission) {
+ formPermission.read = this.hasOperation(foundPermission.allowedOperations, 'READ');
+ formPermission.change = this.hasOperation(foundPermission.allowedOperations, 'CHANGE');
+ formPermission.remove = this.hasOperation(foundPermission.allowedOperations, 'DELETE');
+ }
+ foundGroup.formPermissions.push(formPermission);
+ }
+
+ this.sortGroups(formGroups);
+
+ return formGroups;
+ }
+
+ private sortGroups(formGroups: FormPermissionGroup[]) {
+ formGroups.sort((a: FormPermissionGroup, b: FormPermissionGroup) => a.groupId.localeCompare(b.groupId));
+ formGroups.forEach(group => group.formPermissions.sort((a: FormPermission, b: FormPermission) => a.label.localeCompare(b.label)));
+ }
+
+ private groupId(group: PermittableGroup): string {
+ const identifier = group.identifier;
+ return identifier.substring(0, identifier.indexOf('_')).toUpperCase();
+ }
+
+ private hasOperation(allowedOperations: AllowedOperation[], operation: AllowedOperation): boolean {
+ return allowedOperations.indexOf(operation) > -1;
+ }
+
+ private findPermission(identifier: string, permissions: Permission[]): Permission {
+ return permissions.find((permission: Permission) => permission.permittableEndpointGroupIdentifier === identifier);
+ }
+
+ mapToRole(identifier: string, formPermissions: FormPermission[]): Role {
+ const permissions: Permission[] = [];
+
+ for (const formPermission of formPermissions) {
+ const allowedOperations: AllowedOperation[] = [];
+
+ if (formPermission.read) {
+ allowedOperations.push('READ');
+ }
+
+ if (formPermission.change) {
+ allowedOperations.push('CHANGE');
+ }
+
+ if (formPermission.remove) {
+ allowedOperations.push('DELETE');
+ }
+
+ permissions.push({
+ permittableEndpointGroupIdentifier: formPermission.groupIdentifier,
+ allowedOperations: allowedOperations
+ });
+ }
+
+ return {
+ identifier: identifier,
+ permissions: permissions
+ };
+ }
+}
+
diff --git a/src/app/roles/model/form-permission-group.model.ts b/src/app/roles/model/form-permission-group.model.ts
new file mode 100644
index 0000000..913ceac
--- /dev/null
+++ b/src/app/roles/model/form-permission-group.model.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {FormPermission} from './form-permission.model';
+
+export interface FormPermissionGroup {
+ groupId: string;
+ formPermissions: FormPermission[];
+}
diff --git a/src/app/roles/model/form-permission.model.ts b/src/app/roles/model/form-permission.model.ts
new file mode 100644
index 0000000..6eaaacf
--- /dev/null
+++ b/src/app/roles/model/form-permission.model.ts
@@ -0,0 +1,93 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export class FormPermission {
+
+ private _groupIdentifier: string;
+ private _read = false;
+ private _change = false;
+ private _remove = false;
+
+ private _label = '';
+ private _readOnly = true;
+
+ constructor(groupIdentifier: string) {
+ this._groupIdentifier = groupIdentifier;
+ }
+
+ get groupIdentifier(): string {
+ return this._groupIdentifier;
+ }
+
+ get label(): string {
+ return this._label;
+ }
+
+ set label(value: string) {
+ this._label = value;
+ }
+
+ get readOnly(): boolean {
+ return this._readOnly;
+ }
+
+ set readOnly(value: boolean) {
+ this._readOnly = value;
+ }
+
+ get read(): boolean {
+ return this._read;
+ }
+
+ set read(value: boolean) {
+ this._read = value;
+
+ if (!value) {
+ this.change = false;
+ this.remove = false;
+ }
+ }
+
+ get change(): boolean {
+ return this._change;
+ }
+
+ set change(value: boolean) {
+ this._change = value;
+
+ if (!value) {
+ this.remove = false;
+ } else {
+ this.read = true;
+ }
+ }
+
+ get remove(): boolean {
+ return this._remove;
+ }
+
+ set remove(value: boolean) {
+ this._remove = value;
+
+ if (value) {
+ this.read = true;
+ this.change = true;
+ }
+ }
+}
diff --git a/src/app/roles/role-exists.guard.ts b/src/app/roles/role-exists.guard.ts
new file mode 100644
index 0000000..ff085ac
--- /dev/null
+++ b/src/app/roles/role-exists.guard.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Store} from '@ngrx/store';
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import {Injectable} from '@angular/core';
+import * as fromRoles from './store';
+import {Observable} from 'rxjs/Observable';
+import {of} from 'rxjs/observable/of';
+import {IdentityService} from '../services/identity/identity.service';
+import {LoadAction} from './store/role.actions';
+import {ExistsGuardService} from '../common/guards/exists-guard';
+
+@Injectable()
+export class RoleExistsGuard implements CanActivate {
+
+ constructor(private store: Store<fromRoles.State>,
+ private identityService: IdentityService,
+ private existsGuardService: ExistsGuardService) {}
+
+ hasRoleInStore(id: string): Observable<boolean> {
+ const timestamp$ = this.store.select(fromRoles.getRolesLoadedAt)
+ .map(loadedAt => loadedAt[id]);
+
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ hasRoleInApi(id: string): Observable<boolean> {
+ const getRole$ = this.identityService.getRole(id)
+ .map(roleEntity => new LoadAction({
+ resource: roleEntity
+ }))
+ .do((action: LoadAction) => this.store.dispatch(action))
+ .map(office => !!office);
+
+ return this.existsGuardService.routeTo404OnError(getRole$);
+ }
+
+ hasRole(id: string): Observable<boolean> {
+ return this.hasRoleInStore(id)
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+
+ return this.hasRoleInApi(id);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasRole(route.params['id']);
+ }
+}
diff --git a/src/app/roles/role.component.html b/src/app/roles/role.component.html
new file mode 100644
index 0000000..b4acc22
--- /dev/null
+++ b/src/app/roles/role.component.html
@@ -0,0 +1,21 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Manage roles' | translate}}">
+ <fims-data-table flex (onActionCellClick)="rowSelect($event)" [columns]="columns" [data]="rolesData$ | async" [loading]="loading$ | async"></fims-data-table>
+</fims-layout-card-over>
+<fims-fab-button title="{{'Create new role' | translate}}" icon="add" [link]="['create']" [permission]="{ id: 'identity_roles', accessLevel: 'CHANGE'}"></fims-fab-button>
diff --git a/src/app/roles/role.component.ts b/src/app/roles/role.component.ts
new file mode 100644
index 0000000..cfc8433
--- /dev/null
+++ b/src/app/roles/role.component.ts
@@ -0,0 +1,61 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnInit} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Role} from '../services/identity/domain/role.model';
+import {TableData} from '../common/data-table/data-table.component';
+import * as fromRoot from '../store';
+import {Observable} from 'rxjs/Observable';
+import {SEARCH} from '../store/role/role.actions';
+import {RolesStore} from './store/index';
+
+@Component({
+ templateUrl: './role.component.html'
+})
+export class RoleComponent implements OnInit {
+
+ rolesData$: Observable<TableData>;
+
+ loading$: Observable<boolean>;
+
+ columns: any[] = [
+ { name: 'identifier', label: 'Id' }
+ ];
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: RolesStore) {}
+
+ ngOnInit(): void {
+ this.rolesData$ = this.store.select(fromRoot.getRoleSearchResults)
+ .map(rolePage => ({
+ data: rolePage.roles,
+ totalElements: rolePage.totalElements,
+ totalPages: rolePage.totalPages
+ })
+ );
+
+ this.loading$ = this.store.select(fromRoot.getRoleSearchLoading);
+
+ this.store.dispatch({ type: SEARCH });
+ }
+
+ rowSelect(role: Role): void {
+ this.router.navigate(['detail', role.identifier], { relativeTo: this.route });
+ }
+
+}
diff --git a/src/app/roles/role.module.ts b/src/app/roles/role.module.ts
new file mode 100644
index 0000000..7f97f4c
--- /dev/null
+++ b/src/app/roles/role.module.ts
@@ -0,0 +1,76 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {NgModule} from '@angular/core';
+import {RouterModule} from '@angular/router';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {RoleComponent} from './role.component';
+import {RoleRoutes} from './role.routing';
+import {RoleFormComponent} from './form/form.component';
+import {CreateRoleFormComponent} from './form/create/create.form.component';
+import {EditRoleFormComponent} from './form/edit/edit.form.component';
+import {FimsSharedModule} from '../common/common.module';
+import {RoleExistsGuard} from './role-exists.guard';
+import {RolesStore, roleStoreFactory} from './store/index';
+import {Store} from '@ngrx/store';
+import {RoleNotificationEffects} from './store/effects/notification.effects';
+import {EffectsModule} from '@ngrx/effects';
+import {RoleRouteEffects} from './store/effects/route.effects';
+import {RoleApiEffects} from './store/effects/service.effects';
+import {RoleDetailComponent} from './detail/role.detail.component';
+import {FormPermissionService} from './helper/form-permission.service';
+import {PermissionListItemComponent} from './components/permission-list-item.component';
+import {MatButtonModule, MatCheckboxModule, MatIconModule, MatInputModule, MatListModule, MatToolbarModule} from '@angular/material';
+import {CommonModule} from '@angular/common';
+import {TranslateModule} from '@ngx-translate/core';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(RoleRoutes),
+ FimsSharedModule,
+ TranslateModule,
+ CommonModule,
+ ReactiveFormsModule,
+ FormsModule,
+ MatCheckboxModule,
+ MatIconModule,
+ MatListModule,
+ MatToolbarModule,
+ MatInputModule,
+ MatButtonModule,
+
+ EffectsModule.run(RoleApiEffects),
+ EffectsModule.run(RoleRouteEffects),
+ EffectsModule.run(RoleNotificationEffects)
+ ],
+ declarations: [
+ RoleComponent,
+ RoleFormComponent,
+ CreateRoleFormComponent,
+ EditRoleFormComponent,
+ RoleDetailComponent,
+ PermissionListItemComponent
+ ],
+ providers: [
+ FormPermissionService,
+ RoleExistsGuard,
+ { provide: RolesStore, useFactory: roleStoreFactory, deps: [Store]}
+ ],
+ entryComponents: []
+})
+export class RoleModule {}
diff --git a/src/app/roles/role.routing.ts b/src/app/roles/role.routing.ts
new file mode 100644
index 0000000..9cfb28a
--- /dev/null
+++ b/src/app/roles/role.routing.ts
@@ -0,0 +1,49 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Routes} from '@angular/router';
+import {RoleComponent} from './role.component';
+import {CreateRoleFormComponent} from './form/create/create.form.component';
+import {EditRoleFormComponent} from './form/edit/edit.form.component';
+import {RoleExistsGuard} from './role-exists.guard';
+import {RoleDetailComponent} from './detail/role.detail.component';
+
+export const RoleRoutes: Routes = [
+ {
+ path: '',
+ component: RoleComponent,
+ data: {title: 'Manage roles and permissions', hasPermission: {id: 'identity_roles', accessLevel: 'READ'}}
+ },
+ {
+ path: 'create',
+ component: CreateRoleFormComponent,
+ data: {title: 'Create new role', hasPermission: {id: 'identity_roles', accessLevel: 'CHANGE'}}
+ },
+ {
+ path: 'detail/:id',
+ component: RoleDetailComponent,
+ canActivate: [RoleExistsGuard],
+ data: {title: 'View role', hasPermission: {id: 'identity_roles', accessLevel: 'READ'}}
+ },
+ {
+ path: 'detail/:id/edit',
+ component: EditRoleFormComponent,
+ canActivate: [RoleExistsGuard],
+ data: {title: 'Edit role', hasPermission: {id: 'identity_roles', accessLevel: 'CHANGE'}}
+ }
+];
diff --git a/src/app/roles/store/effects/notification.effects.ts b/src/app/roles/store/effects/notification.effects.ts
new file mode 100644
index 0000000..3103349
--- /dev/null
+++ b/src/app/roles/store/effects/notification.effects.ts
@@ -0,0 +1,48 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as roleActions from '../role.actions';
+import {NotificationService, NotificationType} from '../../../services/notification/notification.service';
+
+@Injectable()
+export class RoleNotificationEffects {
+
+ @Effect({ dispatch: false })
+ createRoleSuccess$: Observable<Action> = this.actions$
+ .ofType(roleActions.CREATE_SUCCESS, roleActions.UPDATE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Role is going to be saved'
+ }));
+
+ @Effect({ dispatch: false })
+ deleteRoleSuccess$: Observable<Action> = this.actions$
+ .ofType(roleActions.DELETE_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Role is going to be deleted'
+ }));
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+}
+
diff --git a/src/app/roles/store/effects/route.effects.ts b/src/app/roles/store/effects/route.effects.ts
new file mode 100644
index 0000000..92736df
--- /dev/null
+++ b/src/app/roles/store/effects/route.effects.ts
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as roleActions from '../role.actions';
+import {Router} from '@angular/router';
+
+@Injectable()
+export class RoleRouteEffects {
+
+ @Effect({ dispatch: false })
+ createRoleSuccess$: Observable<Action> = this.actions$
+ .ofType(roleActions.CREATE_SUCCESS, roleActions.UPDATE_SUCCESS)
+ .map(action => action.payload)
+ .do(payload => this.router.navigate(['../'], { relativeTo: payload.activatedRoute }));
+
+ @Effect({ dispatch: false })
+ deleteRoleSuccess$: Observable<Action> = this.actions$
+ .ofType(roleActions.DELETE_SUCCESS)
+ .map(action => action.payload)
+ .do((payload) => this.router.navigate(['../../'], { relativeTo: payload.activatedRoute }));
+
+ constructor(private actions$: Actions, private router: Router) { }
+}
diff --git a/src/app/roles/store/effects/service.effects.ts b/src/app/roles/store/effects/service.effects.ts
new file mode 100644
index 0000000..732d4ee
--- /dev/null
+++ b/src/app/roles/store/effects/service.effects.ts
@@ -0,0 +1,71 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+import * as roleActions from '../role.actions';
+import {IdentityService} from '../../../services/identity/identity.service';
+
+@Injectable()
+export class RoleApiEffects {
+
+ @Effect()
+ createRole$: Observable<Action> = this.actions$
+ .ofType(roleActions.CREATE)
+ .map((action: roleActions.CreateRoleAction) => action.payload)
+ .mergeMap(payload =>
+ this.identityService.createRole(payload.role)
+ .map(() => new roleActions.CreateRoleSuccessAction({
+ resource: payload.role,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new roleActions.CreateRoleFailAction(error)))
+ );
+
+ @Effect()
+ updateRole$: Observable<Action> = this.actions$
+ .ofType(roleActions.UPDATE)
+ .map((action: roleActions.UpdateRoleAction) => action.payload)
+ .mergeMap(payload =>
+ this.identityService.changeRole(payload.role)
+ .map(() => new roleActions.UpdateRoleSuccessAction({
+ resource: payload.role,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new roleActions.UpdateRoleFailAction(error)))
+ );
+
+ @Effect()
+ deleteRole$: Observable<Action> = this.actions$
+ .ofType(roleActions.DELETE)
+ .map((action: roleActions.DeleteRoleAction) => action.payload)
+ .mergeMap(payload =>
+ this.identityService.deleteRole(payload.role.identifier)
+ .map(() => new roleActions.DeleteRoleSuccessAction({
+ resource: payload.role,
+ activatedRoute: payload.activatedRoute
+ }))
+ .catch((error) => of(new roleActions.DeleteRoleFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private identityService: IdentityService) { }
+
+}
diff --git a/src/app/roles/store/index.ts b/src/app/roles/store/index.ts
new file mode 100644
index 0000000..6e36b8c
--- /dev/null
+++ b/src/app/roles/store/index.ts
@@ -0,0 +1,51 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as fromRoot from '../../store';
+import {ActionReducer, Store} from '@ngrx/store';
+import {createReducer} from '../../store/index';
+import {createSelector} from 'reselect';
+import {createResourceReducer, getResourceLoadedAt, getResourceSelected, ResourceState} from '../../common/store/resource.reducer';
+import {createFormReducer, FormState, getFormError} from '../../common/store/form.reducer';
+
+export interface State extends fromRoot.State {
+ roles: ResourceState;
+ roleForm: FormState;
+}
+
+const reducers = {
+ roles: createResourceReducer('Role'),
+ roleForm: createFormReducer('Role'),
+};
+
+export const roleModuleReducer: ActionReducer<State> = createReducer(reducers);
+
+export class RolesStore extends Store<State> {}
+
+export function roleStoreFactory(appStore: Store<fromRoot.State>) {
+ appStore.replaceReducer(roleModuleReducer);
+ return appStore;
+}
+
+export const getRolesState = (state: State) => state.roles;
+
+export const getRoleFormState = (state: State) => state.roleForm;
+export const getRoleFormError = createSelector(getRoleFormState, getFormError);
+
+export const getRolesLoadedAt = createSelector(getRolesState, getResourceLoadedAt);
+export const getSelectedRole = createSelector(getRolesState, getResourceSelected);
diff --git a/src/app/roles/store/role.actions.ts b/src/app/roles/store/role.actions.ts
new file mode 100644
index 0000000..ff5d1ed
--- /dev/null
+++ b/src/app/roles/store/role.actions.ts
@@ -0,0 +1,137 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Action} from '@ngrx/store';
+import {type} from '../../store/util';
+import {Role} from '../../services/identity/domain/role.model';
+import {Error} from '../../services/domain/error.model';
+import {
+ CreateResourceSuccessPayload,
+ DeleteResourceSuccessPayload,
+ LoadResourcePayload,
+ SelectResourcePayload,
+ UpdateResourceSuccessPayload
+} from '../../common/store/resource.reducer';
+import {RoutePayload} from '../../common/store/route-payload';
+
+export const LOAD = type('[Role] Load');
+export const SELECT = type('[Role] Select');
+
+export const CREATE = type('[Role] Create');
+export const CREATE_SUCCESS = type('[Role] Create Success');
+export const CREATE_FAIL = type('[Role] Create Fail');
+
+export const UPDATE = type('[Role] Update');
+export const UPDATE_SUCCESS = type('[Role] Update Success');
+export const UPDATE_FAIL = type('[Role] Update Fail');
+
+export const DELETE = type('[Role] Delete');
+export const DELETE_SUCCESS = type('[Role] Delete Success');
+export const DELETE_FAIL = type('[Role] Delete Fail');
+
+export const RESET_FORM = type('[Role] Reset Form');
+
+export interface RoleRoutePayload extends RoutePayload {
+ role: Role;
+}
+
+export class LoadAction implements Action {
+ readonly type = LOAD;
+
+ constructor(public payload: LoadResourcePayload) { }
+}
+
+export class SelectAction implements Action {
+ readonly type = SELECT;
+
+ constructor(public payload: SelectResourcePayload) { }
+}
+
+export class CreateRoleAction implements Action {
+ readonly type = CREATE;
+
+ constructor(public payload: RoleRoutePayload) { }
+}
+
+export class CreateRoleSuccessAction implements Action {
+ readonly type = CREATE_SUCCESS;
+
+ constructor(public payload: CreateResourceSuccessPayload) { }
+}
+
+export class CreateRoleFailAction implements Action {
+ readonly type = CREATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class UpdateRoleAction implements Action {
+ readonly type = UPDATE;
+
+ constructor(public payload: RoleRoutePayload) { }
+}
+
+export class UpdateRoleSuccessAction implements Action {
+ readonly type = UPDATE_SUCCESS;
+
+ constructor(public payload: UpdateResourceSuccessPayload) { }
+}
+
+export class UpdateRoleFailAction implements Action {
+ readonly type = UPDATE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class DeleteRoleAction implements Action {
+ readonly type = DELETE;
+
+ constructor(public payload: RoleRoutePayload) { }
+}
+
+export class DeleteRoleSuccessAction implements Action {
+ readonly type = DELETE_SUCCESS;
+
+ constructor(public payload: DeleteResourceSuccessPayload) { }
+}
+
+export class DeleteRoleFailAction implements Action {
+ readonly type = DELETE_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class ResetRoleFormAction implements Action {
+ readonly type = RESET_FORM;
+
+ constructor() {}
+}
+
+export type Actions
+ = LoadAction
+ | SelectAction
+ | CreateRoleAction
+ | CreateRoleSuccessAction
+ | CreateRoleFailAction
+ | UpdateRoleAction
+ | UpdateRoleSuccessAction
+ | UpdateRoleFailAction
+ | DeleteRoleAction
+ | DeleteRoleSuccessAction
+ | DeleteRoleFailAction
+ | ResetRoleFormAction;
diff --git a/src/app/services/accounting/accounting.service.ts b/src/app/services/accounting/accounting.service.ts
new file mode 100644
index 0000000..cb404bb
--- /dev/null
+++ b/src/app/services/accounting/accounting.service.ts
@@ -0,0 +1,201 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Inject, Injectable} from '@angular/core';
+import {HttpClient} from '../http/http.service';
+import {Ledger} from './domain/ledger.model';
+import {Observable} from 'rxjs/Observable';
+import {Account} from './domain/account.model';
+import {RequestOptionsArgs, URLSearchParams} from '@angular/http';
+import {AccountCommand} from './domain/account-command.model';
+import {JournalEntry} from './domain/journal-entry.model';
+import {TrialBalance} from './domain/trial-balance.model';
+import {AccountEntryPage} from './domain/account-entry-page.model';
+import {AccountPage} from './domain/account-page.model';
+import {FetchRequest} from '../domain/paging/fetch-request.model';
+import {buildDateRangeParam, buildSearchParams} from '../domain/paging/search-param.builder';
+import {LedgerPage} from './domain/ledger-page.model';
+import {ChartOfAccountEntry} from './domain/chart-of-account-entry.model';
+import {TransactionType} from './domain/transaction-type.model';
+import {TransactionTypePage} from './domain/transaction-type-page.model';
+import {AccountType} from './domain/account-type.model';
+import {IncomeStatement} from './domain/income-statement.model';
+import {FinancialCondition} from './domain/financial-condition.model';
+
+@Injectable()
+export class AccountingService {
+
+ constructor(private http: HttpClient, @Inject('accountingBaseUrl') private baseUrl: string) {
+ }
+
+ public createLedger(ledger: Ledger): Observable<void> {
+ return this.http.post(`${this.baseUrl}/ledgers`, ledger);
+ }
+
+ public fetchLedgers(includeSubLedgers = false, fetchRequest?: FetchRequest, type?: AccountType): Observable<LedgerPage> {
+ const params: URLSearchParams = buildSearchParams(fetchRequest);
+
+ params.append('includeSubLedgers', String(includeSubLedgers));
+ params.append('type', type);
+
+ const requestOptions: RequestOptionsArgs = {
+ params
+ };
+
+ return this.http.get(`${this.baseUrl}/ledgers`, requestOptions);
+ }
+
+ public findLedger(identifier: string, silent?: boolean): Observable<Ledger> {
+ return this.http.get(`${this.baseUrl}/ledgers/${identifier}`, {}, silent);
+ }
+
+ public addSubLedger(identifier: string, subLedger: Ledger): Observable<void> {
+ return this.http.post(`${this.baseUrl}/ledgers/${identifier}`, subLedger);
+ }
+
+ public modifyLedger(ledger: Ledger): Observable<void> {
+ return this.http.put(`${this.baseUrl}/ledgers/${ledger.identifier}`, ledger);
+ }
+
+ public deleteLedger(identifier: string): Observable<void> {
+ return this.http.delete(`${this.baseUrl}/ledgers/${identifier}`);
+ }
+
+ public fetchAccountsOfLedger(identifier: string, fetchRequest?: FetchRequest): Observable<AccountPage> {
+ const params: URLSearchParams = buildSearchParams(fetchRequest);
+
+ const requestOptions: RequestOptionsArgs = {
+ params
+ };
+ return this.http.get(`${this.baseUrl}/ledgers/${identifier}/accounts`, requestOptions);
+ }
+
+ public createAccount(account: Account): Observable<void> {
+ return this.http.post(`${this.baseUrl}/accounts`, account);
+ }
+
+ public fetchAccounts(fetchRequest?: FetchRequest, type?: AccountType): Observable<AccountPage> {
+ const params: URLSearchParams = buildSearchParams(fetchRequest);
+
+ params.append('type', type);
+
+ const requestOptions: RequestOptionsArgs = {
+ params
+ };
+ return this.http.get(`${this.baseUrl}/accounts`, requestOptions)
+ .share();
+ }
+
+ public findAccount(identifier: string, silent?: boolean): Observable<Account> {
+ return this.http.get(`${this.baseUrl}/accounts/${identifier}`, {}, silent);
+ }
+
+ public modifyAccount(account: Account): Observable<void> {
+ return this.http.put(`${this.baseUrl}/accounts/${account.identifier}`, account);
+ }
+
+ public deleteAccount(account: Account): Observable<void> {
+ return this.http.delete(`${this.baseUrl}/accounts/${account.identifier}`);
+ }
+
+ public fetchAccountEntries(identifier: string, startDate: string, endDate: string,
+ fetchRequest?: FetchRequest): Observable<AccountEntryPage> {
+ const params: URLSearchParams = buildSearchParams(fetchRequest);
+ const dateRange = buildDateRangeParam(startDate, endDate);
+ params.append('dateRange', dateRange);
+
+ const requestOptions: RequestOptionsArgs = {
+ params
+ };
+ return this.http.get(`${this.baseUrl}/accounts/${identifier}/entries`, requestOptions);
+ }
+
+ public fetchAccountCommands(identifier: string): Observable<AccountCommand[]> {
+ return this.http.get(`${this.baseUrl}/accounts/${identifier}/commands`);
+ }
+
+ public accountCommand(identifier: string, command: AccountCommand): Observable<void> {
+ return this.http.post(`${this.baseUrl}/accounts/${identifier}/commands`, command);
+ }
+
+ public createJournalEntry(journalEntry: JournalEntry): Observable<void> {
+ return this.http.post(`${this.baseUrl}/journal`, journalEntry);
+ }
+
+ public fetchJournalEntries(startDate: string, endDate: string, account?: string, amount?: string): Observable<JournalEntry[]> {
+ const params: URLSearchParams = new URLSearchParams();
+
+ params.append('dateRange', buildDateRangeParam(startDate, endDate));
+ params.append('account', account && account.length > 0 ? account : undefined);
+ params.append('amount', amount && amount.length > 0 ? amount : undefined);
+
+ const requestOptions: RequestOptionsArgs = {
+ params
+ };
+ return this.http.get(`${this.baseUrl}/journal`, requestOptions);
+ }
+
+ public findJournalEntry(transactionIdentifier: string): Observable<JournalEntry> {
+ return this.http.get(`${this.baseUrl}/journal/${transactionIdentifier}`);
+ }
+
+ public getTrialBalance(includeEmptyEntries?: boolean): Observable<TrialBalance> {
+ const params: URLSearchParams = new URLSearchParams();
+ params.append('includeEmptyEntries', includeEmptyEntries ? 'true' : 'false');
+
+ const requestOptions: RequestOptionsArgs = {
+ params
+ };
+ return this.http.get(`${this.baseUrl}/trialbalance`, requestOptions);
+ }
+
+ public getChartOfAccounts(): Observable<ChartOfAccountEntry[]> {
+ return this.http.get(`${this.baseUrl}/chartofaccounts`);
+ }
+
+ public findTransactionType(code: string, silent?: boolean): Observable<Account> {
+ return this.http.get(`${this.baseUrl}/transactiontypes/${code}`, {}, silent);
+ }
+
+ public createTransactionType(transactionType: TransactionType): Observable<void> {
+ return this.http.post(`${this.baseUrl}/transactiontypes`, transactionType);
+ }
+
+ public fetchTransactionTypes(fetchRequest?: FetchRequest): Observable<TransactionTypePage> {
+ const params: URLSearchParams = buildSearchParams(fetchRequest);
+
+ const requestOptions: RequestOptionsArgs = {
+ params
+ };
+
+ return this.http.get(`${this.baseUrl}/transactiontypes`, requestOptions);
+ }
+
+ public changeTransactionType(transactionType: TransactionType): Observable<void> {
+ return this.http.put(`${this.baseUrl}/transactiontypes/${transactionType.code}`, transactionType);
+ }
+
+ public getIncomeStatement(): Observable<IncomeStatement> {
+ return this.http.get(`${this.baseUrl}/incomestatement`);
+ }
+
+ public getFinancialCondition(): Observable<FinancialCondition> {
+ return this.http.get(`${this.baseUrl}/financialcondition`);
+ }
+
+}
diff --git a/src/app/services/accounting/domain/account-command-action.model.ts b/src/app/services/accounting/domain/account-command-action.model.ts
new file mode 100644
index 0000000..aaaab30
--- /dev/null
+++ b/src/app/services/accounting/domain/account-command-action.model.ts
@@ -0,0 +1,19 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export type AccountCommandAction = 'LOCK' | 'UNLOCK' | 'CLOSE' | 'REOPEN';
diff --git a/src/app/services/accounting/domain/account-command.model.ts b/src/app/services/accounting/domain/account-command.model.ts
new file mode 100644
index 0000000..5a577c3
--- /dev/null
+++ b/src/app/services/accounting/domain/account-command.model.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {AccountCommandAction} from './account-command-action.model';
+
+export interface AccountCommand {
+ action: AccountCommandAction;
+ comment: string;
+ createdOn?: string;
+ createdBy?: string;
+}
diff --git a/src/app/services/accounting/domain/account-entry-page.model.ts b/src/app/services/accounting/domain/account-entry-page.model.ts
new file mode 100644
index 0000000..2801db6
--- /dev/null
+++ b/src/app/services/accounting/domain/account-entry-page.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {AccountEntry} from './account-entry.model';
+
+export interface AccountEntryPage {
+ accountEntries: AccountEntry[];
+ totalElements: number;
+ totalPages: number;
+}
diff --git a/src/app/services/accounting/domain/account-entry-type.model.ts b/src/app/services/accounting/domain/account-entry-type.model.ts
new file mode 100644
index 0000000..db9dee9
--- /dev/null
+++ b/src/app/services/accounting/domain/account-entry-type.model.ts
@@ -0,0 +1,19 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export type AccountEntryType = 'DEBIT' | 'CREDIT';
diff --git a/src/app/services/accounting/domain/account-entry.model.ts b/src/app/services/accounting/domain/account-entry.model.ts
new file mode 100644
index 0000000..38b1ef2
--- /dev/null
+++ b/src/app/services/accounting/domain/account-entry.model.ts
@@ -0,0 +1,27 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {AccountEntryType} from './account-entry-type.model';
+
+export interface AccountEntry {
+ type: AccountEntryType;
+ transactionDate: string;
+ message: string;
+ amount: number;
+ balance: number;
+}
diff --git a/src/app/services/accounting/domain/account-page.model.ts b/src/app/services/accounting/domain/account-page.model.ts
new file mode 100644
index 0000000..cef3a9e
--- /dev/null
+++ b/src/app/services/accounting/domain/account-page.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Account} from './account.model';
+
+export interface AccountPage {
+ accounts: Account[];
+ totalElements: number;
+ totalPages: number;
+}
diff --git a/src/app/services/accounting/domain/account-state.model.ts b/src/app/services/accounting/domain/account-state.model.ts
new file mode 100644
index 0000000..11eb6a0
--- /dev/null
+++ b/src/app/services/accounting/domain/account-state.model.ts
@@ -0,0 +1,19 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export type AccountState = 'OPEN' | 'LOCKED' | 'CLOSED';
diff --git a/src/app/services/accounting/domain/account-type.model.ts b/src/app/services/accounting/domain/account-type.model.ts
new file mode 100644
index 0000000..05a0899
--- /dev/null
+++ b/src/app/services/accounting/domain/account-type.model.ts
@@ -0,0 +1,19 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export type AccountType = 'ASSET' | 'LIABILITY' | 'EQUITY' | 'REVENUE' | 'EXPENSE';
diff --git a/src/app/services/accounting/domain/account.model.ts b/src/app/services/accounting/domain/account.model.ts
new file mode 100644
index 0000000..dffa0a2
--- /dev/null
+++ b/src/app/services/accounting/domain/account.model.ts
@@ -0,0 +1,37 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {AccountType} from './account-type.model';
+import {AccountState} from './account-state.model';
+
+export interface Account {
+ type?: AccountType;
+ identifier: string;
+ name: string;
+ holders?: string[];
+ signatureAuthorities?: string[];
+ balance?: number;
+ referenceAccount?: string;
+ ledger: string;
+ alternativeAccountNumber?: string;
+ state?: AccountState;
+ createdOn?: string;
+ createdBy?: string;
+ lastModifiedOn?: string;
+ lastModifiedBy?: string;
+}
diff --git a/src/app/services/accounting/domain/chart-of-account-entry.model.ts b/src/app/services/accounting/domain/chart-of-account-entry.model.ts
new file mode 100644
index 0000000..cf1c472
--- /dev/null
+++ b/src/app/services/accounting/domain/chart-of-account-entry.model.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface ChartOfAccountEntry {
+ code: string;
+ name: string;
+ level: number;
+ description: string;
+ type: string;
+ chartOfAccountEntries: ChartOfAccountEntry[];
+}
diff --git a/src/app/services/accounting/domain/creditor.model.ts b/src/app/services/accounting/domain/creditor.model.ts
new file mode 100644
index 0000000..164de3e
--- /dev/null
+++ b/src/app/services/accounting/domain/creditor.model.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface Creditor {
+ accountNumber: string;
+ amount: string;
+}
diff --git a/src/app/services/accounting/domain/debtor.model.ts b/src/app/services/accounting/domain/debtor.model.ts
new file mode 100644
index 0000000..9aae22c
--- /dev/null
+++ b/src/app/services/accounting/domain/debtor.model.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export interface Debtor {
+ accountNumber: string;
+ amount: string;
+}
diff --git a/src/app/services/accounting/domain/financial-condition-entry.model.ts b/src/app/services/accounting/domain/financial-condition-entry.model.ts
new file mode 100644
index 0000000..7439538
--- /dev/null
+++ b/src/app/services/accounting/domain/financial-condition-entry.model.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface FinancialConditionEntry {
+ description: string;
+ value: string;
+}
diff --git a/src/app/services/accounting/domain/financial-condition-section.model.ts b/src/app/services/accounting/domain/financial-condition-section.model.ts
new file mode 100644
index 0000000..c69f3ce
--- /dev/null
+++ b/src/app/services/accounting/domain/financial-condition-section.model.ts
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {FinancialConditionEntry} from './financial-condition-entry.model';
+
+export type Type = 'ASSET' | 'EQUITY' | 'LIABILITY';
+
+export interface FinancialConditionSection {
+ type: Type;
+ description: string;
+ financialConditionEntries: FinancialConditionEntry[];
+ subtotal: string;
+}
diff --git a/src/app/services/accounting/domain/financial-condition.model.ts b/src/app/services/accounting/domain/financial-condition.model.ts
new file mode 100644
index 0000000..ed58d48
--- /dev/null
+++ b/src/app/services/accounting/domain/financial-condition.model.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {FinancialConditionSection} from './financial-condition-section.model';
+
+export interface FinancialCondition {
+ date: string;
+ financialConditionSections: FinancialConditionSection[];
+ totalAssets: string;
+ totalEquitiesAndLiabilities: string;
+}
diff --git a/src/app/services/accounting/domain/income-statement-entry.model.ts b/src/app/services/accounting/domain/income-statement-entry.model.ts
new file mode 100644
index 0000000..5167d89
--- /dev/null
+++ b/src/app/services/accounting/domain/income-statement-entry.model.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface IncomeStatementEntry {
+ description: string;
+ value: string;
+}
diff --git a/src/app/services/accounting/domain/income-statement-section.model.ts b/src/app/services/accounting/domain/income-statement-section.model.ts
new file mode 100644
index 0000000..3237b03
--- /dev/null
+++ b/src/app/services/accounting/domain/income-statement-section.model.ts
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {IncomeStatementEntry} from './income-statement-entry.model';
+
+export type Type = 'INCOME' | 'EXPENSES';
+
+export interface IncomeStatementSection {
+ type: Type;
+ description: string;
+ incomeStatementEntries: IncomeStatementEntry[];
+ subtotal: string;
+}
diff --git a/src/app/services/accounting/domain/income-statement.model.ts b/src/app/services/accounting/domain/income-statement.model.ts
new file mode 100644
index 0000000..bb9397b
--- /dev/null
+++ b/src/app/services/accounting/domain/income-statement.model.ts
@@ -0,0 +1,27 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {IncomeStatementSection} from './income-statement-section.model';
+
+export interface IncomeStatement {
+ date: string;
+ incomeStatementSections: IncomeStatementSection[];
+ grossProfit: string;
+ totalExpenses: string;
+ netIncome: string;
+}
diff --git a/src/app/services/accounting/domain/journal-entry-state.model.ts b/src/app/services/accounting/domain/journal-entry-state.model.ts
new file mode 100644
index 0000000..6a0d5ae
--- /dev/null
+++ b/src/app/services/accounting/domain/journal-entry-state.model.ts
@@ -0,0 +1,19 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export type JournalEntryState = 'PENDING' | 'PROCESSED';
diff --git a/src/app/services/accounting/domain/journal-entry.model.ts b/src/app/services/accounting/domain/journal-entry.model.ts
new file mode 100644
index 0000000..b31cef3
--- /dev/null
+++ b/src/app/services/accounting/domain/journal-entry.model.ts
@@ -0,0 +1,33 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Debtor} from './debtor.model';
+import {Creditor} from './creditor.model';
+import {JournalEntryState} from './journal-entry-state.model';
+
+export interface JournalEntry {
+ transactionIdentifier: string;
+ transactionDate: string;
+ transactionType: string;
+ clerk: string;
+ note?: string;
+ debtors: Debtor[];
+ creditors: Creditor[];
+ state?: JournalEntryState;
+ message?: string;
+}
diff --git a/src/app/services/accounting/domain/ledger-page.model.ts b/src/app/services/accounting/domain/ledger-page.model.ts
new file mode 100644
index 0000000..80a5c18
--- /dev/null
+++ b/src/app/services/accounting/domain/ledger-page.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Ledger} from './ledger.model';
+
+export interface LedgerPage {
+ ledgers: Ledger[];
+ totalElements: number;
+ totalPages: number;
+}
diff --git a/src/app/services/accounting/domain/ledger.model.ts b/src/app/services/accounting/domain/ledger.model.ts
new file mode 100644
index 0000000..33c844c
--- /dev/null
+++ b/src/app/services/accounting/domain/ledger.model.ts
@@ -0,0 +1,34 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {AccountType} from './account-type.model';
+
+export interface Ledger {
+ parentLedgerIdentifier?: string;
+ type: AccountType;
+ identifier: string;
+ name: string;
+ description?: string;
+ subLedgers: Ledger[];
+ showAccountsInChart: boolean;
+ totalValue?: string;
+ createdOn?: string;
+ createdBy?: string;
+ lastModifiedOn?: string;
+ lastModifiedBy?: string;
+}
diff --git a/src/app/services/accounting/domain/permittable-group-ids.ts b/src/app/services/accounting/domain/permittable-group-ids.ts
new file mode 100644
index 0000000..30d27fc
--- /dev/null
+++ b/src/app/services/accounting/domain/permittable-group-ids.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export class AccountingPermittableGroupIds {
+ public static readonly ACCOUNT_MANAGEMENT = 'accounting__v1__account';
+ public static readonly JOURNAL_MANAGEMENT = 'accounting__v1__journal';
+ public static readonly LEDGER_MANAGEMENT = 'accounting__v1__ledger';
+ public static readonly TRANSACTION_TYPES = 'accounting__v1__tx_types';
+ public static readonly THOTH_INCOME_STMT = 'accounting__v1__income_stmt';
+ public static readonly THOTH_FIN_CONDITION = 'accounting__v1__fin_condition';
+}
diff --git a/src/app/services/accounting/domain/transaction-type-page.model.ts b/src/app/services/accounting/domain/transaction-type-page.model.ts
new file mode 100644
index 0000000..3c1ffbf
--- /dev/null
+++ b/src/app/services/accounting/domain/transaction-type-page.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {TransactionType} from './transaction-type.model';
+
+export interface TransactionTypePage {
+ transactionTypes: TransactionType[];
+ totalPages: number;
+ totalElements: number;
+}
diff --git a/src/app/services/accounting/domain/transaction-type.model.ts b/src/app/services/accounting/domain/transaction-type.model.ts
new file mode 100644
index 0000000..671242b
--- /dev/null
+++ b/src/app/services/accounting/domain/transaction-type.model.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface TransactionType {
+ code: string;
+ name: string;
+ description?: string;
+}
diff --git a/src/app/services/accounting/domain/trial-balance-entry-type.model.ts b/src/app/services/accounting/domain/trial-balance-entry-type.model.ts
new file mode 100644
index 0000000..d9d23cc
--- /dev/null
+++ b/src/app/services/accounting/domain/trial-balance-entry-type.model.ts
@@ -0,0 +1,19 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export type TrialBalanceEntryType = 'DEBIT' | 'CREDIT';
diff --git a/src/app/services/accounting/domain/trial-balance-entry.model.ts b/src/app/services/accounting/domain/trial-balance-entry.model.ts
new file mode 100644
index 0000000..9dcd6d2
--- /dev/null
+++ b/src/app/services/accounting/domain/trial-balance-entry.model.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Ledger} from './ledger.model';
+import {TrialBalanceEntryType} from './trial-balance-entry-type.model';
+
+export interface TrialBalanceEntry {
+ ledger: Ledger;
+ type: TrialBalanceEntryType;
+ amount: number;
+}
diff --git a/src/app/services/accounting/domain/trial-balance.model.ts b/src/app/services/accounting/domain/trial-balance.model.ts
new file mode 100644
index 0000000..aca7952
--- /dev/null
+++ b/src/app/services/accounting/domain/trial-balance.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {TrialBalanceEntry} from './trial-balance-entry.model';
+
+export interface TrialBalance {
+ trialBalanceEntries: TrialBalanceEntry[];
+ debitTotal: number;
+ creditTotal: number;
+}
diff --git a/src/app/services/anubis/permittable-endpoint.model.ts b/src/app/services/anubis/permittable-endpoint.model.ts
new file mode 100644
index 0000000..e28286c
--- /dev/null
+++ b/src/app/services/anubis/permittable-endpoint.model.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export interface PermittableEndpoint {
+ path: string;
+ method: Method;
+ groupId?: string;
+}
+
+export type Method = 'POST' | 'HEAD' | 'PUT' | 'DELETE';
diff --git a/src/app/services/anubis/permittable-group.model.ts b/src/app/services/anubis/permittable-group.model.ts
new file mode 100644
index 0000000..ef27616
--- /dev/null
+++ b/src/app/services/anubis/permittable-group.model.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {PermittableEndpoint} from './permittable-endpoint.model';
+
+export interface PermittableGroup {
+ identifier: string;
+ permittables: PermittableEndpoint[];
+}
diff --git a/src/app/services/catalog/catalog.service.ts b/src/app/services/catalog/catalog.service.ts
new file mode 100644
index 0000000..43eb77b
--- /dev/null
+++ b/src/app/services/catalog/catalog.service.ts
@@ -0,0 +1,58 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Inject, Injectable} from '@angular/core';
+import {HttpClient} from '../http/http.service';
+import {Observable} from 'rxjs/Observable';
+import {Catalog} from './domain/catalog.model';
+import {Field} from './domain/field.model';
+
+@Injectable()
+export class CatalogService {
+
+ constructor(@Inject('customerBaseUrl') private baseUrl: string, private http: HttpClient) {
+ }
+
+ fetchCatalogs(): Observable<Catalog[]> {
+ return this.http.get(`${this.baseUrl}/catalogs`);
+ }
+
+ createCatalog(catalog: Catalog): Observable<void> {
+ return this.http.post(`${this.baseUrl}/catalogs`, catalog);
+ }
+
+ updateCatalog(catalog: Catalog): Observable<void> {
+ return this.http.put(`${this.baseUrl}/catalogs/${catalog.identifier}`, catalog);
+ }
+
+ deleteCatalog(catalog: Catalog): Observable<void> {
+ return this.http.delete(`${this.baseUrl}/catalogs/${catalog.identifier}`, {});
+ }
+
+ findCatalog(identifier: string, silent: boolean = false): Observable<Catalog> {
+ return this.http.get(`${this.baseUrl}/catalogs/${identifier}`, {}, silent);
+ }
+
+ updateField(catalogIdentifier: string, field: Field): Observable<void> {
+ return this.http.put(`${this.baseUrl}/catalogs/${catalogIdentifier}/fields/${field.identifier}`, field);
+ }
+
+ deleteField(catalogIdentifier: string, field: Field): Observable<void> {
+ return this.http.delete(`${this.baseUrl}/catalogs/${catalogIdentifier}/fields/${field.identifier}`, );
+ }
+}
diff --git a/src/app/services/catalog/domain/catalog.model.ts b/src/app/services/catalog/domain/catalog.model.ts
new file mode 100644
index 0000000..280b6d4
--- /dev/null
+++ b/src/app/services/catalog/domain/catalog.model.ts
@@ -0,0 +1,30 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Field} from './field.model';
+
+export interface Catalog {
+ identifier: string;
+ name: string;
+ description?: string;
+ fields: Field[];
+ createdBy?: string;
+ createdOn?: string;
+ lastModifiedBy?: string;
+ lastModifiedOn?: string;
+}
diff --git a/src/app/services/catalog/domain/field.model.ts b/src/app/services/catalog/domain/field.model.ts
new file mode 100644
index 0000000..c60520c
--- /dev/null
+++ b/src/app/services/catalog/domain/field.model.ts
@@ -0,0 +1,37 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Option} from './option.model';
+
+export class Field {
+ identifier: string;
+ dataType: FieldDataType;
+ label: string;
+ hint?: string;
+ description?: string;
+ options: Option[];
+ mandatory?: boolean;
+ length?: number;
+ precision?: number;
+ minValue?: number;
+ maxValue?: number;
+ createdBy?: string;
+ createdOn?: string;
+}
+
+export type FieldDataType = 'TEXT' | 'NUMBER' | 'DATE' | 'SINGLE_SELECTION' | 'MULTI_SELECTION';
diff --git a/src/app/services/catalog/domain/option.model.ts b/src/app/services/catalog/domain/option.model.ts
new file mode 100644
index 0000000..48bcf35
--- /dev/null
+++ b/src/app/services/catalog/domain/option.model.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface Option {
+ label: string;
+ value: number;
+ createdBy?: string;
+ createdOn?: string;
+}
diff --git a/src/app/services/catalog/domain/value.model.ts b/src/app/services/catalog/domain/value.model.ts
new file mode 100644
index 0000000..1e02c19
--- /dev/null
+++ b/src/app/services/catalog/domain/value.model.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface Value {
+ catalogIdentifier: string;
+ fieldIdentifier: string;
+ value: string;
+}
diff --git a/src/app/services/cheque/cheque.service.ts b/src/app/services/cheque/cheque.service.ts
new file mode 100644
index 0000000..2d6f650
--- /dev/null
+++ b/src/app/services/cheque/cheque.service.ts
@@ -0,0 +1,72 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Inject, Injectable} from '@angular/core';
+import {HttpClient} from '../http/http.service';
+import {Observable} from 'rxjs/Observable';
+import {RequestOptionsArgs, URLSearchParams} from '@angular/http';
+import {Cheque} from './domain/cheque.model';
+import {IssuingCount} from './domain/issuing-count.model';
+import {ChequeProcessingCommand} from './domain/cheque-processing-command';
+import {ChequeTransaction} from './domain/cheque-transaction';
+import {MICRResolution} from './domain/micr-resolution.model';
+import {FimsCheque} from './domain/fims-cheque.model';
+import {mapToFimsCheque, mapToFimsCheques} from './domain/mapper/fims-cheque.mapper';
+
+@Injectable()
+export class ChequeService {
+
+ constructor(private http: HttpClient, @Inject('chequeBaseUrl') private baseUrl: string) {
+ }
+
+ public issue(issuingCount: IssuingCount): Observable<string> {
+ return this.http.post(`${this.baseUrl}/cheques/`, issuingCount);
+ }
+
+ public fetch(state?: string, accountIdentifier?: string): Observable<FimsCheque[]> {
+ const search = new URLSearchParams();
+
+ search.append('state', state);
+ search.append('accountIdentifier', accountIdentifier);
+
+ const requestOptions: RequestOptionsArgs = {
+ search
+ };
+
+ return this.http.get(`${this.baseUrl}/cheques/`, requestOptions)
+ .map((cheques: Cheque[]) => mapToFimsCheques(cheques));
+ }
+
+ public get(identifier: string): Observable<FimsCheque> {
+ return this.http.get(`${this.baseUrl}/cheques/${identifier}`)
+ .map((cheque: Cheque) => mapToFimsCheque(cheque));
+ }
+
+ public process(identifier: string, command: ChequeProcessingCommand): Observable<void> {
+ return this.http.post(`${this.baseUrl}/cheques/${identifier}/commands`, command);
+ }
+
+ public processTransaction(transaction: ChequeTransaction): Observable<void> {
+ return this.http.post(`${this.baseUrl}/transactions/`, transaction);
+ }
+
+ public expandMicr(identifier: string): Observable<MICRResolution> {
+ return this.http.get(`${this.baseUrl}/micr/${identifier}`, {}, true);
+ }
+
+}
diff --git a/src/app/services/cheque/domain/action.model.ts b/src/app/services/cheque/domain/action.model.ts
new file mode 100644
index 0000000..b6414c4
--- /dev/null
+++ b/src/app/services/cheque/domain/action.model.ts
@@ -0,0 +1,20 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export type Action = 'APPROVE' | 'CANCEL';
diff --git a/src/app/services/cheque/domain/cheque-processing-command.ts b/src/app/services/cheque/domain/cheque-processing-command.ts
new file mode 100644
index 0000000..27d5d76
--- /dev/null
+++ b/src/app/services/cheque/domain/cheque-processing-command.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Action} from './action.model';
+
+export interface ChequeProcessingCommand {
+ action: Action;
+}
diff --git a/src/app/services/cheque/domain/cheque-transaction.ts b/src/app/services/cheque/domain/cheque-transaction.ts
new file mode 100644
index 0000000..21dbc5a
--- /dev/null
+++ b/src/app/services/cheque/domain/cheque-transaction.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Cheque} from './cheque.model';
+
+export interface ChequeTransaction {
+ cheque: Cheque;
+ creditorAccountNumber: string;
+}
diff --git a/src/app/services/cheque/domain/cheque.model.ts b/src/app/services/cheque/domain/cheque.model.ts
new file mode 100644
index 0000000..7260595
--- /dev/null
+++ b/src/app/services/cheque/domain/cheque.model.ts
@@ -0,0 +1,32 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {MICR} from './micr.model';
+import {State} from './state.model';
+
+export interface Cheque {
+ micr: MICR;
+ drawee: string;
+ drawer: string;
+ payee: string;
+ amount: string;
+ dateIssued: string;
+ openCheque: boolean;
+ state: State;
+ journalEntryIdentifier: string;
+}
diff --git a/src/app/services/cheque/domain/fims-cheque.model.ts b/src/app/services/cheque/domain/fims-cheque.model.ts
new file mode 100644
index 0000000..249164f
--- /dev/null
+++ b/src/app/services/cheque/domain/fims-cheque.model.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {Cheque} from './cheque.model';
+
+export interface FimsCheque extends Cheque {
+ identifier: string;
+}
diff --git a/src/app/services/cheque/domain/issuing-count.model.ts b/src/app/services/cheque/domain/issuing-count.model.ts
new file mode 100644
index 0000000..98c53c4
--- /dev/null
+++ b/src/app/services/cheque/domain/issuing-count.model.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface IssuingCount {
+ accountIdentifier: string;
+ start?: number;
+ amount: number;
+}
diff --git a/src/app/services/cheque/domain/mapper/fims-cheque.mapper.ts b/src/app/services/cheque/domain/mapper/fims-cheque.mapper.ts
new file mode 100644
index 0000000..433d4cd
--- /dev/null
+++ b/src/app/services/cheque/domain/mapper/fims-cheque.mapper.ts
@@ -0,0 +1,39 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {FimsCheque} from '../fims-cheque.model';
+import {Cheque} from '../cheque.model';
+import {MICR} from '../micr.model';
+
+export function mapToFimsCheque(cheque: Cheque): FimsCheque {
+ return Object.assign({}, cheque, {
+ identifier: micrToIdentifier(cheque.micr)
+ });
+}
+
+export function mapToFimsCheques(cheques: Cheque[]): FimsCheque[] {
+ return cheques.map(cheque => mapToFimsCheque(cheque));
+}
+
+export function micrToIdentifier(micr: MICR): string {
+ return toMICRIdentifier(micr.chequeNumber, micr.branchSortCode, micr.accountNumber);
+}
+
+export function toMICRIdentifier(chequeNumber: string, branchSortCode: string, accountNumber: string): string {
+ return `${chequeNumber}~${branchSortCode}~${accountNumber}`;
+}
diff --git a/src/app/services/cheque/domain/micr-resolution.model.ts b/src/app/services/cheque/domain/micr-resolution.model.ts
new file mode 100644
index 0000000..8319a51
--- /dev/null
+++ b/src/app/services/cheque/domain/micr-resolution.model.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface MICRResolution {
+ office: string;
+ customer: string;
+}
diff --git a/src/app/services/cheque/domain/micr.model.ts b/src/app/services/cheque/domain/micr.model.ts
new file mode 100644
index 0000000..288d282
--- /dev/null
+++ b/src/app/services/cheque/domain/micr.model.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface MICR {
+ chequeNumber: string;
+ branchSortCode: string;
+ accountNumber: string;
+}
diff --git a/src/app/services/cheque/domain/permittable-group-ids.ts b/src/app/services/cheque/domain/permittable-group-ids.ts
new file mode 100644
index 0000000..81927b1
--- /dev/null
+++ b/src/app/services/cheque/domain/permittable-group-ids.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export class ChequePermittableGroupIds {
+ public static readonly CHEQUE_TRANSACTION = 'cheques__v1__transaction';
+ public static readonly CHEQUE_MANAGEMENT = 'cheques__v1__management';
+}
diff --git a/src/app/services/cheque/domain/state.model.ts b/src/app/services/cheque/domain/state.model.ts
new file mode 100644
index 0000000..adacc2b
--- /dev/null
+++ b/src/app/services/cheque/domain/state.model.ts
@@ -0,0 +1,20 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export type State = 'PENDING' | 'PROCESSED' | 'CANCELED';
diff --git a/src/app/services/country/country.service.spec.ts b/src/app/services/country/country.service.spec.ts
new file mode 100644
index 0000000..8cfde78
--- /dev/null
+++ b/src/app/services/country/country.service.spec.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {fakeAsync, tick} from '@angular/core/testing';
+import {CountryService} from './country.service';
+import {MockBackend} from '@angular/http/testing';
+import {BaseRequestOptions, ConnectionBackend, Http, RequestOptions, ResponseOptions} from '@angular/http';
+import {TranslateService} from '@ngx-translate/core';
+import {Observable} from 'rxjs/Observable';
+import {ReflectiveInjector} from '@angular/core';
+
+describe('Test country service', () => {
+
+ beforeEach(() => {
+ const translateService = {
+ onLangChange: Observable.empty()
+ };
+
+ this.injector = ReflectiveInjector.resolveAndCreate([
+ {provide: ConnectionBackend, useClass: MockBackend},
+ {provide: RequestOptions, useClass: BaseRequestOptions},
+ {provide: TranslateService, useValue: translateService},
+ Http,
+ CountryService
+ ]);
+ this.countryService = this.injector.get(CountryService);
+ this.backend = this.injector.get(ConnectionBackend) as MockBackend;
+ this.backend.connections.subscribe((connection: any) => this.lastConnection = connection);
+ });
+
+ xit('should return countries when term contains brackets', fakeAsync(() => {
+ // TODO find out why mock connection returns a rejected promise
+ this.countryService.init();
+
+ const mockResponse = [
+ {name: 'Country (A)', displayName: 'Country (A)', alpha2Code: '', translations: {}},
+ {name: 'Country (B)', displayName: 'Country (B)', alpha2Code: '', translations: {}}
+ ];
+
+ this.lastConnection.mockRespond(new Response(new ResponseOptions({
+ body: JSON.stringify(mockResponse)
+ })));
+
+ tick();
+
+ const result = this.countryService.fetchCountries('Country (A)');
+
+ expect(result.length).toBe(1);
+ expect(result[0]).toEqual(mockResponse[0]);
+ }));
+
+});
diff --git a/src/app/services/country/country.service.ts b/src/app/services/country/country.service.ts
new file mode 100644
index 0000000..80593ed
--- /dev/null
+++ b/src/app/services/country/country.service.ts
@@ -0,0 +1,77 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Http} from '@angular/http';
+import {Observable} from 'rxjs/Observable';
+import {Country} from './model/country.model';
+import {TranslateService} from '@ngx-translate/core';
+import {escapeRegexPattern} from '../../common/regex/escape';
+
+@Injectable()
+export class CountryService {
+
+ private countries: Country[] = [];
+
+ constructor(private http: Http, private translateService: TranslateService) {
+ }
+
+ init(): void {
+ this.getCountries()
+ .map(countries => this.translate(countries))
+ .subscribe(countries => this.countries = countries);
+
+ this.translateService.onLangChange
+ .map(() => this.translate(this.countries))
+ .subscribe(countries => this.countries = countries);
+ }
+
+ fetchCountries(term): Country[] {
+ const regTerm = new RegExp(`^${escapeRegexPattern(term)}`, 'gi');
+
+ let result: Country[];
+
+ if (term) {
+ result = this.countries.filter((country: Country) => regTerm.test(country.displayName));
+ } else {
+ result = this.countries.slice();
+ }
+ return result;
+ }
+
+ fetchByCountryCode(countryCode: string): Country {
+ return this.countries.find((country: Country) => country.alpha2Code === countryCode);
+ }
+
+ private translate(countries: Country[]): Country[] {
+ return countries.map(country => this.mapTranslation(country));
+ }
+
+ private mapTranslation(country: Country): Country {
+ const currentLang = this.translateService.currentLang;
+ return Object.assign({}, country, {
+ displayName: currentLang !== 'en' && country.translations[currentLang] ? country.translations[currentLang] : country.name
+ });
+ }
+
+ private getCountries(): Observable<Country[]> {
+ return this.http.get('https://restcountries.eu/rest/v2/all?fields=name;alpha2Code;translations')
+ .map(response => response.json());
+ }
+
+}
diff --git a/src/app/services/country/model/country.model.ts b/src/app/services/country/model/country.model.ts
new file mode 100644
index 0000000..1ee5eaf
--- /dev/null
+++ b/src/app/services/country/model/country.model.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface Country {
+ displayName: string;
+ name: string;
+ alpha2Code: string;
+ translations: { [ key: string ]: string };
+}
diff --git a/src/app/services/currency/currency.service.ts b/src/app/services/currency/currency.service.ts
new file mode 100644
index 0000000..9072e6b
--- /dev/null
+++ b/src/app/services/currency/currency.service.ts
@@ -0,0 +1,45 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {Currency} from './domain/currency.model';
+
+@Injectable()
+export class CurrencyService {
+
+ private currencies: Currency[] = [
+ {code: 'BZD', name: 'Belize Dollar', sign: '$', digits: 2},
+ {code: 'EUR', name: 'Euro', sign: '€', digits: 2},
+ {code: 'GMD', name: 'Gambian Dalasi', sign: 'D', digits: 2},
+ {code: 'JMD', name: 'Jamaican Dollar', sign: '$', digits: 2},
+ {code: 'MXN', name: 'Mexican Peso', sign: '$', digits: 2},
+ {code: 'USD', name: 'US Dollar', sign: '$', digits: 2},
+ {code: 'TTD', name: 'Trinidad and Tobago Dollar', sign: '$', digits: 2},
+ {code: 'XCD', name: 'East Caribbean Dollar', sign: '$', digits: 2}
+ ];
+
+ fetchCurrencies(): Observable<Currency[]> {
+ return Observable.of(this.currencies.slice(0));
+ }
+
+ getCurrency(code: string): Currency {
+ const foundCurrency = this.currencies.find(currency => currency.code === code);
+ return Object.assign({}, foundCurrency);
+ }
+}
diff --git a/src/app/services/currency/domain/currency.model.ts b/src/app/services/currency/domain/currency.model.ts
new file mode 100644
index 0000000..18de582
--- /dev/null
+++ b/src/app/services/currency/domain/currency.model.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface Currency {
+ code: string;
+ name: string;
+ sign: string;
+ digits: number;
+}
diff --git a/src/app/services/customer/customer.service.ts b/src/app/services/customer/customer.service.ts
new file mode 100644
index 0000000..ac4c69f
--- /dev/null
+++ b/src/app/services/customer/customer.service.ts
@@ -0,0 +1,209 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Inject, Injectable} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {Customer} from './domain/customer.model';
+import {HttpClient} from '../http/http.service';
+import {CustomerPage} from './domain/customer-page.model';
+import {FetchRequest} from '../domain/paging/fetch-request.model';
+import {buildSearchParams} from '../domain/paging/search-param.builder';
+import {RequestOptionsArgs, URLSearchParams} from '@angular/http';
+import {Command} from './domain/command.model';
+import {TaskDefinition} from './domain/task-definition.model';
+import {ImageService} from '../image/image.service';
+import {IdentificationCard} from './domain/identification-card.model';
+import {IdentificationCardScan} from './domain/identification-card-scan.model';
+import {ProcessStep} from './domain/process-step.model';
+import {CustomerDocument} from './domain/customer-document.model';
+
+@Injectable()
+export class CustomerService {
+
+ constructor(@Inject('customerBaseUrl') private baseUrl: string, private http: HttpClient, private imageService: ImageService) {
+ }
+
+ fetchCustomers(fetchRequest: FetchRequest): Observable<CustomerPage> {
+ const params: URLSearchParams = buildSearchParams(fetchRequest);
+
+ const requestOptions: RequestOptionsArgs = {
+ search: params
+ };
+
+ return this.http.get(`${this.baseUrl}/customers`, requestOptions).share();
+ }
+
+ getCustomer(id: string, silent?: boolean): Observable<Customer> {
+ return this.http.get(`${this.baseUrl}/customers/${id}`, {}, silent);
+ }
+
+ createCustomer(customer: Customer): Observable<Customer> {
+ return this.http.post(`${this.baseUrl}/customers`, customer);
+ }
+
+ updateCustomer(customer: Customer): Observable<Customer> {
+ return this.http.put(`${this.baseUrl}/customers/${customer.identifier}`, customer);
+ }
+
+ executeCustomerCommand(id: string, command: Command): Observable<void> {
+ return this.http.post(`${this.baseUrl}/customers/${id}/commands`, command);
+ }
+
+ listCustomerCommand(id: string): Observable<Command[]> {
+ return this.http.get(`${this.baseUrl}/customers/${id}/commands`);
+ }
+
+ addTaskToCustomer(customerId: string, taskId: string): Observable<void> {
+ return this.http.post(`${this.baseUrl}/customers/${customerId}/tasks/${taskId}`, {});
+ }
+
+ markTaskAsExecuted(customerId: string, taskId: string): Observable<void> {
+ return this.http.put(`${this.baseUrl}/customers/${customerId}/tasks/${taskId}`, {});
+ }
+
+ fetchCustomerTasks(customerId: string, includeExecuted?: boolean): Observable<TaskDefinition[]> {
+ return this.http.get(`${this.baseUrl}/customers/${customerId}/tasks`);
+ }
+
+ fetchTasks(): Observable<TaskDefinition[]> {
+ return this.http.get(`${this.baseUrl}/tasks`);
+ }
+
+ getTask(identifier: string): Observable<TaskDefinition> {
+ return this.http.get(`${this.baseUrl}/tasks/${identifier}`);
+ }
+
+ createTask(task: TaskDefinition): Observable<void> {
+ return this.http.post(`${this.baseUrl}/tasks`, task);
+ }
+
+ updateTask(task: TaskDefinition): Observable<void> {
+ return this.http.put(`${this.baseUrl}/tasks/${task.identifier}`, task);
+ }
+
+ fetchProcessSteps(customerId: string): Observable<ProcessStep[]> {
+ return this.http.get(`${this.baseUrl}/customers/${customerId}/actions`);
+ }
+
+ getPortrait(customerId: string): Observable<Blob> {
+ return this.imageService.getImage(`${this.baseUrl}/customers/${customerId}/portrait`);
+ }
+
+ uploadPortrait(customerId: string, file: File): Observable<void> {
+ const formData = new FormData();
+
+ formData.append('portrait', file, file.name);
+
+ return this.http.post(`${this.baseUrl}/customers/${customerId}/portrait`, formData);
+ }
+
+ deletePortrait(customerId: string): Observable<void> {
+ return this.http.delete(`${this.baseUrl}/customers/${customerId}/portrait`);
+ }
+
+ fetchIdentificationCards(customerId: string): Observable<IdentificationCard[]> {
+ return this.http.get(`${this.baseUrl}/customers/${customerId}/identifications`);
+ }
+
+ getIdentificationCard(customerId: string, number: string): Observable<IdentificationCard> {
+ return this.http.get(`${this.baseUrl}/customers/${customerId}/identifications/${number}`);
+ }
+
+ createIdentificationCard(customerId: string, identificationCard: IdentificationCard): Observable<void> {
+ return this.http.post(`${this.baseUrl}/customers/${customerId}/identifications`, identificationCard);
+ }
+
+ updateIdentificationCard(customerId: string, identificationCard: IdentificationCard): Observable<void> {
+ return this.http.put(`${this.baseUrl}/customers/${customerId}/identifications/${identificationCard.number}`, identificationCard);
+ }
+
+ deleteIdentificationCard(customerId: string, number: string): Observable<void> {
+ return this.http.delete(`${this.baseUrl}/customers/${customerId}/identifications/${number}`);
+ }
+
+ fetchIdentificationCardScans(customerId: string, number: string): Observable<IdentificationCardScan[]> {
+ return this.http.get(`${this.baseUrl}/customers/${customerId}/identifications/${number}/scans`);
+ }
+
+ getIdentificationCardScanImage(customerId: string, number: string, scanId: string): Observable<Blob> {
+ return this.imageService.getImage(`${this.baseUrl}/customers/${customerId}/identifications/${number}/scans/${scanId}/image`);
+ }
+
+ uploadIdentificationCardScan(customerId: string, number: string, scan: IdentificationCardScan, file: File): Observable<void> {
+ const formData = new FormData();
+ formData.append('image', file, file.name);
+
+ const params = new URLSearchParams();
+ params.append('scanIdentifier', scan.identifier);
+ params.append('description', scan.description);
+
+ const requestOptions: RequestOptionsArgs = {
+ search: params
+ };
+
+ return this.http.post(`${this.baseUrl}/customers/${customerId}/identifications/${number}/scans`, formData, requestOptions);
+ }
+
+ deleteIdentificationCardScan(customerId: string, number: string, scanId: string): Observable<void> {
+ return this.http.delete(`${this.baseUrl}/customers/${customerId}/identifications/${number}/scans/${scanId}`);
+ }
+
+ getDocuments(customerId: string): Observable<CustomerDocument[]> {
+ return this.http.get(`${this.baseUrl}/customers/${customerId}/documents`);
+ }
+
+ getDocument(customerId: string, documentId: string): Observable<CustomerDocument> {
+ return this.http.get(`${this.baseUrl}/customers/${customerId}/documents/${documentId}`);
+ }
+
+ createDocument(customerId: string, document: CustomerDocument): Observable<void> {
+ return this.http.post(`${this.baseUrl}/customers/${customerId}/documents/${document.identifier}`, document);
+ }
+
+ updateDocument(customerId: string, document: CustomerDocument): Observable<void> {
+ return this.http.put(`${this.baseUrl}/customers/${customerId}/documents/${document.identifier}`, document);
+ }
+
+ deleteDocument(customerId: string, document: CustomerDocument): Observable<void> {
+ return this.http.delete(`${this.baseUrl}/customers/${customerId}/documents/${document.identifier}`);
+ }
+
+ completeDocument(customerId: string, documentId: string, silent: boolean = false): Observable<void> {
+ return this.http.post(`${this.baseUrl}/customers/${customerId}/documents/${documentId}/completed`, true, {}, silent);
+ }
+
+ getDocumentPageNumbers(customerId: string, documentId: string): Observable<number[]> {
+ return this.http.get(`${this.baseUrl}/customers/${customerId}/documents/${documentId}/pages`);
+ }
+
+ getDocumentPage(customerId: string, documentId: string, pageNumber: number): Observable<Blob> {
+ return this.imageService.getImage(`${this.baseUrl}/customers/${customerId}/documents/${documentId}/pages/${pageNumber}`);
+ }
+
+ createDocumentPage(customerId: string, documentId: string, pageNumber: number, file: File): Observable<void> {
+ const formData = new FormData();
+ formData.append('page', file, file.name);
+
+ return this.http.post(`${this.baseUrl}/customers/${customerId}/documents/${documentId}/pages/${pageNumber}`, formData);
+ }
+
+ deleteDocumentPage(customerId: string, documentId: string, pageNumber: number): Observable<void> {
+ return this.http.delete(`${this.baseUrl}/customers/${customerId}/documents/${documentId}/pages/${pageNumber}`);
+ }
+
+}
diff --git a/src/app/services/customer/domain/command.model.ts b/src/app/services/customer/domain/command.model.ts
new file mode 100644
index 0000000..c2f87bf
--- /dev/null
+++ b/src/app/services/customer/domain/command.model.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export type CommandAction = 'ACTIVATE' | 'LOCK' | 'UNLOCK' | 'CLOSE' | 'REOPEN';
+
+export interface Command {
+ action: CommandAction;
+ comment?: string;
+ createdOn?: string;
+ createdBy?: string;
+}
diff --git a/src/app/services/customer/domain/customer-document.model.ts b/src/app/services/customer/domain/customer-document.model.ts
new file mode 100644
index 0000000..3ab7dd4
--- /dev/null
+++ b/src/app/services/customer/domain/customer-document.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface CustomerDocument {
+ identifier: string;
+ description?: string;
+ completed?: boolean;
+ createdBy?: string;
+ createdOn?: string;
+}
diff --git a/src/app/services/customer/domain/customer-page.model.ts b/src/app/services/customer/domain/customer-page.model.ts
new file mode 100644
index 0000000..eef7fab
--- /dev/null
+++ b/src/app/services/customer/domain/customer-page.model.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {Customer} from './customer.model';
+
+export interface CustomerPage {
+ customers: Customer[];
+ totalElements: number;
+ totalPages: number;
+}
diff --git a/src/app/services/customer/domain/customer-state.model.ts b/src/app/services/customer/domain/customer-state.model.ts
new file mode 100644
index 0000000..c444393
--- /dev/null
+++ b/src/app/services/customer/domain/customer-state.model.ts
@@ -0,0 +1,19 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export type CustomerState = 'PENDING' | 'ACTIVE' | 'LOCKED' | 'CLOSED';
diff --git a/src/app/services/customer/domain/customer-type.model.ts b/src/app/services/customer/domain/customer-type.model.ts
new file mode 100644
index 0000000..b9a1958
--- /dev/null
+++ b/src/app/services/customer/domain/customer-type.model.ts
@@ -0,0 +1,19 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export type CustomerType = 'PERSON' | 'BUSINESS';
diff --git a/src/app/services/customer/domain/customer.model.ts b/src/app/services/customer/domain/customer.model.ts
new file mode 100644
index 0000000..409bb9b
--- /dev/null
+++ b/src/app/services/customer/domain/customer.model.ts
@@ -0,0 +1,50 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {CustomerState} from './customer-state.model';
+import {CustomerType} from './customer-type.model';
+import {DateOfBirth} from './date-of-birth.model';
+import {IdentificationCard} from './identification-card.model';
+import {Address} from '../../domain/address/address.model';
+import {ContactDetail} from '../../domain/contact/contact-detail.model';
+import {Value} from '../../catalog/domain/value.model';
+
+export interface Customer {
+ identifier: string;
+ type: CustomerType;
+ givenName: string;
+ middleName?: string;
+ surname: string;
+ dateOfBirth: DateOfBirth;
+ identificationCard?: IdentificationCard;
+ accountBeneficiary?: string;
+ referenceCustomer?: string;
+ assignedOffice?: string;
+ assignedEmployee?: string;
+ address: Address;
+ contactDetails?: ContactDetail[];
+ currentState?: CustomerState;
+ applicationDate?: string;
+ customValues: Value[];
+ member: boolean;
+ createdBy?: string;
+ createdOn?: string;
+ lastModifiedBy?: string;
+ lastModifiedOn?: string;
+}
diff --git a/src/app/services/customer/domain/date-of-birth.model.ts b/src/app/services/customer/domain/date-of-birth.model.ts
new file mode 100644
index 0000000..6451d52
--- /dev/null
+++ b/src/app/services/customer/domain/date-of-birth.model.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface DateOfBirth {
+ year?: number;
+ month?: number;
+ day?: number;
+}
diff --git a/src/app/services/customer/domain/expiration-date.model.ts b/src/app/services/customer/domain/expiration-date.model.ts
new file mode 100644
index 0000000..a23b1c2
--- /dev/null
+++ b/src/app/services/customer/domain/expiration-date.model.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface ExpirationDate {
+ year: number;
+ month: number;
+ day: number;
+}
diff --git a/src/app/services/customer/domain/identification-card-scan.model.ts b/src/app/services/customer/domain/identification-card-scan.model.ts
new file mode 100644
index 0000000..0605777
--- /dev/null
+++ b/src/app/services/customer/domain/identification-card-scan.model.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface IdentificationCardScan {
+ identifier: string;
+ description: string;
+}
diff --git a/src/app/services/customer/domain/identification-card.model.ts b/src/app/services/customer/domain/identification-card.model.ts
new file mode 100644
index 0000000..27553b0
--- /dev/null
+++ b/src/app/services/customer/domain/identification-card.model.ts
@@ -0,0 +1,30 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ExpirationDate} from './expiration-date.model';
+
+export interface IdentificationCard {
+ type: string;
+ number: string;
+ expirationDate: ExpirationDate;
+ issuer?: string;
+ createdBy?: string;
+ createdOn?: string;
+ lastModifiedBy?: string;
+ lastModifiedOn?: string;
+}
diff --git a/src/app/services/customer/domain/permittable-group-ids.ts b/src/app/services/customer/domain/permittable-group-ids.ts
new file mode 100644
index 0000000..9926e4c
--- /dev/null
+++ b/src/app/services/customer/domain/permittable-group-ids.ts
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export class CustomerPermittableGroupIds {
+ public static readonly CUSTOMER_MANAGEMENT = 'customer__v1__customer';
+ public static readonly TASK_MANAGEMENT = 'customer__v1__task';
+ public static readonly CATALOG_MANAGEMENT = 'catalog__v1__catalog';
+ public static readonly IDENTITY_CARD_MANAGEMENT = 'customer__v1__identifications';
+ public static readonly PORTRAIT_MANAGEMENT = 'customer__v1__portrait';
+ public static readonly CUSTOMER_DOCUMENT = 'customer__v1__documents';
+}
+
+
diff --git a/src/app/services/customer/domain/process-step.model.ts b/src/app/services/customer/domain/process-step.model.ts
new file mode 100644
index 0000000..b112f22
--- /dev/null
+++ b/src/app/services/customer/domain/process-step.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Command} from './command.model';
+import {TaskDefinition} from './task-definition.model';
+
+export interface ProcessStep {
+ command: Command;
+ taskDefinitions: TaskDefinition[];
+}
diff --git a/src/app/services/customer/domain/task-definition.model.ts b/src/app/services/customer/domain/task-definition.model.ts
new file mode 100644
index 0000000..7cc6f1b
--- /dev/null
+++ b/src/app/services/customer/domain/task-definition.model.ts
@@ -0,0 +1,31 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export type TaskDefinitionType = 'ID_CARD' | 'FOUR_EYES' | 'CUSTOM';
+
+export type TaskDefinitionCommand = 'ACTIVATE' | 'LOCK' | 'UNLOCK' | 'CLOSE' | 'REOPEN';
+
+export interface TaskDefinition {
+ identifier: string;
+ type: TaskDefinitionType;
+ commands: TaskDefinitionCommand[];
+ name: string;
+ description: string;
+ mandatory: boolean;
+ predefined: boolean;
+}
diff --git a/src/app/services/depositAccount/deposit-account.service.ts b/src/app/services/depositAccount/deposit-account.service.ts
new file mode 100644
index 0000000..02ef3a0
--- /dev/null
+++ b/src/app/services/depositAccount/deposit-account.service.ts
@@ -0,0 +1,110 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Inject, Injectable} from '@angular/core';
+import {HttpClient} from '../http/http.service';
+import {Observable} from 'rxjs/Observable';
+import {ProductDefinition} from './domain/definition/product-definition.model';
+import {ProductDefinitionCommand} from './domain/definition/product-definition-command.model';
+import {ProductInstance} from './domain/instance/product-instance.model';
+import {RequestOptionsArgs, URLSearchParams} from '@angular/http';
+import {Action} from './domain/definition/action.model';
+import {DividendDistribution} from './domain/definition/dividend-distribution.model';
+import {AvailableTransactionType} from './domain/instance/available-transaction-type.model';
+
+@Injectable()
+export class DepositAccountService {
+
+ constructor(private http: HttpClient, @Inject('depositAccountBaseUrl') private baseUrl: string) {
+ }
+
+ createProductDefinition(productDefinition: ProductDefinition): Observable<void> {
+ return this.http.post(`${this.baseUrl}/definitions`, productDefinition);
+ }
+
+ updateProductDefinition(productDefinition: ProductDefinition): Observable<void> {
+ return this.http.put(`${this.baseUrl}/definitions/${productDefinition.identifier}`, productDefinition);
+ }
+
+ deleteProductDefinition(identifier: string): Observable<void> {
+ return this.http.delete(`${this.baseUrl}/definitions/${identifier}`);
+ }
+
+ fetchProductDefinitions(): Observable<ProductDefinition[]> {
+ return this.http.get(`${this.baseUrl}/definitions`);
+ }
+
+ findProductDefinition(identifier: string): Observable<ProductDefinition> {
+ return this.http.get(`${this.baseUrl}/definitions/${identifier}`);
+ }
+
+ processCommand(identifier: string, command: ProductDefinitionCommand): Observable<void> {
+ return this.http.post(`${this.baseUrl}/definitions/${identifier}/commands`, command);
+ }
+
+ fetchDividendDistributions(identifier: string): Observable<DividendDistribution[]> {
+ return this.http.get(`${this.baseUrl}/definitions/${identifier}/dividends`);
+ }
+
+ distributeDividend(identifier: string, dividendDistribution: DividendDistribution): Observable<void> {
+ return this.http.post(`${this.baseUrl}/definitions/${identifier}/dividends`, dividendDistribution);
+ }
+
+ createProductInstance(productInstance: ProductInstance): Observable<void> {
+ return this.http.post(`${this.baseUrl}/instances`, productInstance);
+ }
+
+ updateProductInstance(productInstance: ProductInstance): Observable<void> {
+ return this.http.put(`${this.baseUrl}/instances/${productInstance.accountIdentifier}`, productInstance);
+ }
+
+ findProductInstance(identifier: string): Observable<ProductInstance> {
+ return this.http.get(`${this.baseUrl}/instances/${identifier}`);
+ }
+
+ fetchProductInstances(customerIdentifier: string, productIdentifier?: string): Observable<ProductInstance[]> {
+ const search = new URLSearchParams();
+
+ search.append('customer', customerIdentifier);
+ search.append('product', productIdentifier);
+
+ const requestOptions: RequestOptionsArgs = {
+ search
+ };
+
+ return this.http.get(`${this.baseUrl}/instances`, requestOptions);
+ }
+
+ fetchActions(): Observable<Action[]> {
+ return this.http.get(`${this.baseUrl}/actions`);
+ }
+
+ fetchPossibleTransactionTypes(customerIdentifier: string): Observable<AvailableTransactionType[]> {
+ const search = new URLSearchParams();
+
+ search.append('customer', customerIdentifier);
+
+ const requestOptions: RequestOptionsArgs = {
+ search
+ };
+
+ return this.http.get(`${this.baseUrl}/instances/transactiontypes`, requestOptions);
+ }
+
+
+}
diff --git a/src/app/services/depositAccount/domain/definition/action.model.ts b/src/app/services/depositAccount/domain/definition/action.model.ts
new file mode 100644
index 0000000..7d4a801
--- /dev/null
+++ b/src/app/services/depositAccount/domain/definition/action.model.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface Action {
+ identifier: string;
+ name: string;
+ description?: string;
+ transactionType: string;
+}
diff --git a/src/app/services/depositAccount/domain/definition/charge.model.ts b/src/app/services/depositAccount/domain/definition/charge.model.ts
new file mode 100644
index 0000000..76ea29b
--- /dev/null
+++ b/src/app/services/depositAccount/domain/definition/charge.model.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface Charge {
+ actionIdentifier: string;
+ incomeAccountIdentifier: string;
+ name: string;
+ description?: string;
+ proportional?: boolean;
+ amount?: number;
+}
diff --git a/src/app/services/depositAccount/domain/definition/currency.model.ts b/src/app/services/depositAccount/domain/definition/currency.model.ts
new file mode 100644
index 0000000..1e10bf5
--- /dev/null
+++ b/src/app/services/depositAccount/domain/definition/currency.model.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface Currency {
+ code: string;
+ name: string;
+ sign: string;
+ scale: number;
+}
diff --git a/src/app/services/depositAccount/domain/definition/dividend-distribution.model.ts b/src/app/services/depositAccount/domain/definition/dividend-distribution.model.ts
new file mode 100644
index 0000000..170664c
--- /dev/null
+++ b/src/app/services/depositAccount/domain/definition/dividend-distribution.model.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface DividendDistribution {
+ dueDate: {
+ year?: number;
+ month?: number;
+ day?: number;
+ };
+ dividendRate: string;
+}
diff --git a/src/app/services/depositAccount/domain/definition/product-definition-command.model.ts b/src/app/services/depositAccount/domain/definition/product-definition-command.model.ts
new file mode 100644
index 0000000..6aef8c1
--- /dev/null
+++ b/src/app/services/depositAccount/domain/definition/product-definition-command.model.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export type Action = 'ACTIVATE' | 'DEACTIVATE';
+
+export interface ProductDefinitionCommand {
+ action: Action;
+ note?: string;
+ createdBy?: string;
+ createdOn?: string;
+}
diff --git a/src/app/services/depositAccount/domain/definition/product-definition.model.ts b/src/app/services/depositAccount/domain/definition/product-definition.model.ts
new file mode 100644
index 0000000..b87cfc6
--- /dev/null
+++ b/src/app/services/depositAccount/domain/definition/product-definition.model.ts
@@ -0,0 +1,40 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Type} from '../type.model';
+import {Currency} from './currency.model';
+import {Charge} from './charge.model';
+import {Term} from './term.model';
+
+export interface ProductDefinition {
+ type: Type;
+ identifier: string;
+ name: string;
+ description?: string;
+ currency: Currency;
+ minimumBalance: number;
+ equityLedgerIdentifier?: string;
+ expenseAccountIdentifier: string;
+ cashAccountIdentifier: string;
+ accrueAccountIdentifier?: string;
+ interest?: number;
+ term: Term;
+ charges: Charge[];
+ flexible: boolean;
+ active?: boolean;
+}
diff --git a/src/app/services/depositAccount/domain/definition/term.model.ts b/src/app/services/depositAccount/domain/definition/term.model.ts
new file mode 100644
index 0000000..d016b9a
--- /dev/null
+++ b/src/app/services/depositAccount/domain/definition/term.model.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {TimeUnit} from '../time-unit.model';
+import {InterestPayable} from '../interest-payable.model';
+
+export interface Term {
+ period?: number;
+ timeUnit?: TimeUnit;
+ interestPayable: InterestPayable;
+}
diff --git a/src/app/services/depositAccount/domain/instance/available-transaction-type.model.ts b/src/app/services/depositAccount/domain/instance/available-transaction-type.model.ts
new file mode 100644
index 0000000..e4a7a10
--- /dev/null
+++ b/src/app/services/depositAccount/domain/instance/available-transaction-type.model.ts
@@ -0,0 +1,21 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface AvailableTransactionType {
+ transactionType: string;
+}
diff --git a/src/app/services/depositAccount/domain/instance/product-instance.model.ts b/src/app/services/depositAccount/domain/instance/product-instance.model.ts
new file mode 100644
index 0000000..db82ca4
--- /dev/null
+++ b/src/app/services/depositAccount/domain/instance/product-instance.model.ts
@@ -0,0 +1,29 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface ProductInstance {
+ customerIdentifier: string;
+ productIdentifier: string;
+ accountIdentifier?: string;
+ alternativeAccountNumber?: string;
+ beneficiaries?: string[];
+ state?: string;
+ balance?: number;
+ openedOn?: string;
+ lastTransactionDate?: string;
+}
diff --git a/src/app/services/depositAccount/domain/interest-payable.model.ts b/src/app/services/depositAccount/domain/interest-payable.model.ts
new file mode 100644
index 0000000..716bb74
--- /dev/null
+++ b/src/app/services/depositAccount/domain/interest-payable.model.ts
@@ -0,0 +1,19 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export type InterestPayable = 'MATURITY' | 'ANNUALLY' | 'QUARTERLY' | 'MONTHLY';
diff --git a/src/app/services/depositAccount/domain/permittable-group-ids.ts b/src/app/services/depositAccount/domain/permittable-group-ids.ts
new file mode 100644
index 0000000..3f9bf3d
--- /dev/null
+++ b/src/app/services/depositAccount/domain/permittable-group-ids.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export class DepositAccountPermittableGroupIds {
+ public static readonly DEFINITION_MANAGEMENT = 'deposit__v1__definition';
+ public static readonly INSTANCE_MANAGEMENT = 'deposit__v1__instance';
+}
diff --git a/src/app/services/depositAccount/domain/time-unit.model.ts b/src/app/services/depositAccount/domain/time-unit.model.ts
new file mode 100644
index 0000000..9b7f6bf
--- /dev/null
+++ b/src/app/services/depositAccount/domain/time-unit.model.ts
@@ -0,0 +1,19 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export type TimeUnit = 'MONTH' | 'YEAR';
diff --git a/src/app/services/depositAccount/domain/type.model.ts b/src/app/services/depositAccount/domain/type.model.ts
new file mode 100644
index 0000000..20cea9e
--- /dev/null
+++ b/src/app/services/depositAccount/domain/type.model.ts
@@ -0,0 +1,19 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export type Type = 'CHECKING' | 'SAVINGS' | 'SHARE';
diff --git a/src/app/services/domain/address/address.model.ts b/src/app/services/domain/address/address.model.ts
new file mode 100644
index 0000000..e378dc7
--- /dev/null
+++ b/src/app/services/domain/address/address.model.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface Address {
+ street: string;
+ city: string;
+ region?: string;
+ postalCode?: string;
+ countryCode: string;
+ country: string;
+}
diff --git a/src/app/services/domain/contact/contact-detail.model.ts b/src/app/services/domain/contact/contact-detail.model.ts
new file mode 100644
index 0000000..ed1170d
--- /dev/null
+++ b/src/app/services/domain/contact/contact-detail.model.ts
@@ -0,0 +1,36 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export interface ContactDetail {
+ type: ContactDetailType;
+ group: ContactDetailGroup;
+ value: string;
+ preferenceLevel: number;
+}
+
+export const BUSINESS = 'BUSINESS';
+export const PRIVATE = 'PRIVATE';
+
+export type ContactDetailGroup = 'BUSINESS' | 'PRIVATE';
+
+export const EMAIL = 'EMAIL';
+export const PHONE = 'PHONE';
+export const MOBILE = 'MOBILE';
+
+export type ContactDetailType = 'EMAIL' | 'PHONE' | 'MOBILE';
diff --git a/src/app/services/domain/date.converter.ts b/src/app/services/domain/date.converter.ts
new file mode 100644
index 0000000..365382f
--- /dev/null
+++ b/src/app/services/domain/date.converter.ts
@@ -0,0 +1,100 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export interface FimsDate {
+ year: number;
+ month: number;
+ day: number;
+}
+
+export function dateAsISOString(date: Date): string {
+ return date.toISOString().slice(0, 10);
+}
+
+export function todayAsISOString(): string {
+ return new Date().toISOString().slice(0, 10);
+}
+
+/**
+ * Converts '2017-01-20' to full ISO string e.g. '2017-01-20T00:00:00.000Z'
+ * @param dateString
+ */
+export function toLongISOString(dateString: string): string {
+ const date: Date = parseDate(dateString);
+ return date.toISOString();
+}
+
+export function toShortISOString(dateString: string): string {
+ return `${toLongISOString(dateString).slice(0, 10)}Z`;
+}
+
+export function toEndOfDay(dateString: string): string {
+ const date: Date = parseDate(dateString);
+
+ date.setDate(date.getDate() + 1);
+ date.setTime(date.getTime() - 1);
+
+ return date.toISOString();
+}
+
+export function addCurrentTime(date: Date): Date {
+ const now: Date = new Date();
+
+ date.setUTCHours(now.getUTCHours());
+ date.setUTCMinutes(now.getUTCMinutes());
+ date.setUTCSeconds(now.getUTCSeconds());
+ date.setUTCMilliseconds(now.getUTCMilliseconds());
+
+ return date;
+}
+
+export function parseDate(dateString: string): Date {
+ const millis = Date.parse(dateString);
+ const date: Date = new Date(millis);
+
+ return date;
+}
+
+/**
+ * Converts '2017-01-20' to FimsDate
+ * @param dateString
+ */
+export function toFimsDate(dateString: string): FimsDate {
+ const chunks: string[] = dateString ? dateString.split('-') : [];
+
+ return {
+ year: chunks.length ? Number(chunks[0]) : undefined,
+ month: chunks.length ? Number(chunks[1]) : undefined,
+ day: chunks.length ? Number(chunks[2]) : undefined,
+ };
+}
+
+export function toISOString(fimsDate: FimsDate): string {
+ return formatDate(fimsDate.year, fimsDate.month, fimsDate.day);
+}
+
+function formatDate(year: number, month: number, day: number): string {
+ return `${year}-${addZero(month)}-${addZero(day)}`;
+}
+
+function addZero(value: number): string {
+ return ('0' + value).slice(-2);
+}
+
+
diff --git a/src/app/services/domain/error.model.ts b/src/app/services/domain/error.model.ts
new file mode 100644
index 0000000..1073ef5
--- /dev/null
+++ b/src/app/services/domain/error.model.ts
@@ -0,0 +1,48 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Observable} from 'rxjs/Observable';
+import {ErrorObservable} from 'rxjs/observable/ErrorObservable';
+
+export class Error {
+ status: number;
+ statusText: string;
+ message: string;
+
+ static handleError(errorResponse: any): ErrorObservable {
+ const error: Error = new Error(errorResponse.status, errorResponse.statusText, errorResponse.message);
+
+ console.error(error.getErrorMessage());
+
+ return Observable.throw(error);
+ }
+
+ constructor(status: number, statusText: string, message: string) {
+ this.status = status;
+ this.message = message;
+ this.statusText = statusText;
+ }
+
+ getErrorMessage(): string {
+ const errMsg: string = (this.message)
+ ? this.message
+ : this.status ? `${this.status} - ${this.statusText}` : 'Server error';
+ return errMsg;
+ }
+
+}
diff --git a/src/app/services/domain/paging/fetch-request.model.ts b/src/app/services/domain/paging/fetch-request.model.ts
new file mode 100644
index 0000000..989a365
--- /dev/null
+++ b/src/app/services/domain/paging/fetch-request.model.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Sort} from './sort.model';
+import {Page} from './page.model';
+
+export interface FetchRequest {
+ searchTerm?: string;
+ page?: Page;
+ sort?: Sort;
+}
diff --git a/src/app/services/domain/paging/page.model.ts b/src/app/services/domain/paging/page.model.ts
new file mode 100644
index 0000000..b8172cf
--- /dev/null
+++ b/src/app/services/domain/paging/page.model.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface Page {
+ pageIndex: number;
+ size: number;
+}
diff --git a/src/app/services/domain/paging/search-param.builder.ts b/src/app/services/domain/paging/search-param.builder.ts
new file mode 100644
index 0000000..258cebd
--- /dev/null
+++ b/src/app/services/domain/paging/search-param.builder.ts
@@ -0,0 +1,45 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {URLSearchParams} from '@angular/http';
+import {FetchRequest} from './fetch-request.model';
+import {Page} from './page.model';
+import {Sort} from './sort.model';
+
+export function buildSearchParams(fetchRequest?: FetchRequest): URLSearchParams {
+ const params = new URLSearchParams();
+
+ fetchRequest = fetchRequest || {};
+
+ const page: Page = fetchRequest.page || {pageIndex: 0, size: 10};
+ const sort: Sort = fetchRequest.sort || {sortColumn: '', sortDirection: ''};
+
+ params.append('term', fetchRequest.searchTerm ? fetchRequest.searchTerm : undefined);
+
+ params.append('pageIndex', page.pageIndex !== undefined ? page.pageIndex.toString() : undefined);
+ params.append('size', page.size ? page.size.toString() : undefined);
+
+ params.append('sortColumn', sort.sortColumn ? sort.sortColumn : undefined);
+ params.append('sortDirection', sort.sortDirection ? sort.sortDirection : undefined);
+
+ return params;
+}
+
+export function buildDateRangeParam(startDate: string, endDate: string): string {
+ return `${startDate}..${endDate}`;
+}
diff --git a/src/app/services/domain/paging/sort.model.ts b/src/app/services/domain/paging/sort.model.ts
new file mode 100644
index 0000000..982864b
--- /dev/null
+++ b/src/app/services/domain/paging/sort.model.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export interface Sort {
+ sortColumn: string;
+ sortDirection: string;
+}
diff --git a/src/app/services/http/default-request-options.service.ts b/src/app/services/http/default-request-options.service.ts
new file mode 100644
index 0000000..56549c1
--- /dev/null
+++ b/src/app/services/http/default-request-options.service.ts
@@ -0,0 +1,34 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {BaseRequestOptions, RequestOptions} from '@angular/http';
+import {Injectable} from '@angular/core';
+
+@Injectable()
+export class DefaultRequestOptions extends BaseRequestOptions {
+
+ constructor() {
+ super();
+
+ // this.headers.set('Accept', 'application/json');
+ // this.headers.set('Content-Type', 'application/json');
+ }
+}
+
+export const requestOptionsProvider = {provide: RequestOptions, useClass: DefaultRequestOptions};
diff --git a/src/app/services/http/header.service.ts b/src/app/services/http/header.service.ts
new file mode 100644
index 0000000..2abcfba
--- /dev/null
+++ b/src/app/services/http/header.service.ts
@@ -0,0 +1,21 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export class HeaderService {
+
+}
diff --git a/src/app/services/http/http.service.spec.ts b/src/app/services/http/http.service.spec.ts
new file mode 100644
index 0000000..0500f29
--- /dev/null
+++ b/src/app/services/http/http.service.spec.ts
@@ -0,0 +1,137 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {MockBackend, MockConnection} from '@angular/http/testing';
+import {AUTHORIZATION_HEADER, HttpClient, TENANT_HEADER, USER_HEADER} from './http.service';
+import {
+ BaseRequestOptions,
+ ConnectionBackend,
+ Headers,
+ Http,
+ RequestOptions,
+ RequestOptionsArgs,
+ Response,
+ ResponseOptions
+} from '@angular/http';
+import {Store} from '@ngrx/store';
+import {Observable} from 'rxjs/Observable';
+import {ReflectiveInjector} from '@angular/core';
+
+describe('Test http client', () => {
+
+ const tenant = 'Reynholm Industries';
+
+ const authenticationState = {
+ username: 'test',
+ tenant: tenant,
+ authentication: {
+ tokenType: 'iDontCare',
+ accessToken: 'accessToken',
+ accessTokenExpiration: new Date().toISOString(),
+ refreshTokenExpiration: new Date().toISOString(),
+ passwordExpiration: new Date().toISOString(),
+ passwordChangedBy: 'moss'
+ }
+ };
+
+ const doPostRequest = function (httpClient: HttpClient, options?: RequestOptionsArgs): void {
+ httpClient.post('/test', {}, options).subscribe(() => {
+ });
+ };
+
+ describe('Test http header', () => {
+
+ beforeEach(() => {
+ this.injector = ReflectiveInjector.resolveAndCreate([
+ {provide: ConnectionBackend, useClass: MockBackend},
+ {provide: RequestOptions, useClass: BaseRequestOptions},
+ {
+ provide: Store, useClass: class {
+ select = jasmine.createSpy('select').and.returnValue(Observable.of(authenticationState));
+ }
+ },
+ Http,
+ HttpClient,
+ ]);
+ this.httpClient = this.injector.get(HttpClient);
+ this.backend = this.injector.get(ConnectionBackend) as MockBackend;
+ });
+
+ it('should send tenant header', (done: DoneFn) => {
+ this.backend.connections.subscribe((connection: MockConnection) => {
+ expect(connection.request.headers.get(TENANT_HEADER)).toBe(tenant);
+ done();
+ });
+ doPostRequest(this.httpClient);
+ });
+
+ it('should send authorization header when logged in', (done: DoneFn) => {
+ this.backend.connections.subscribe((connection: MockConnection) => {
+ expect(connection.request.headers.get(USER_HEADER)).toBe(authenticationState.username);
+ expect(connection.request.headers.get(AUTHORIZATION_HEADER)).toBe(authenticationState.authentication.accessToken);
+ done();
+ });
+ doPostRequest(this.httpClient);
+ });
+
+ it('should send custom headers', (done: DoneFn) => {
+ this.backend.connections.subscribe((connection: MockConnection) => {
+ expect(connection.request.headers.get('Content-Type')).toBe('multipart/form-data');
+ done();
+ });
+ doPostRequest(this.httpClient, {
+ headers: new Headers({
+ 'Content-Type': 'multipart/form-data'
+ })
+ });
+ });
+
+ it('should return json if json', (done: DoneFn) => {
+ const expectedResponse: any = {
+ text: 'text'
+ };
+ this.backend.connections.subscribe((connection: MockConnection) => {
+ const response = new Response(new ResponseOptions({
+ body: JSON.stringify(expectedResponse)
+ }));
+ connection.mockRespond(response);
+ });
+
+ this.httpClient.post('/test', {}).subscribe(response => {
+ expect(response).toEqual(expectedResponse);
+ done();
+ });
+ });
+
+ it('should return text if no json', (done: DoneFn) => {
+ this.backend.connections.subscribe((connection: MockConnection) => {
+ const response = new Response(new ResponseOptions({
+ body: 'text'
+ }));
+ connection.mockRespond(response);
+ });
+
+ this.httpClient.post('/test', {}).subscribe(text => {
+ expect(text).toEqual('text');
+ done();
+ });
+ });
+
+ });
+
+});
diff --git a/src/app/services/http/http.service.ts b/src/app/services/http/http.service.ts
new file mode 100644
index 0000000..7a33b35
--- /dev/null
+++ b/src/app/services/http/http.service.ts
@@ -0,0 +1,124 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Headers, Http, Request, RequestMethod, RequestOptions, RequestOptionsArgs, Response} from '@angular/http';
+import {Observable} from 'rxjs/Observable';
+import {Subject} from 'rxjs/Subject';
+import {Store} from '@ngrx/store';
+import * as fromRoot from '../../store';
+import {LOGOUT} from '../../store/security/security.actions';
+
+export enum Action { QueryStart, QueryStop }
+
+export const TENANT_HEADER = 'X-Tenant-Identifier';
+export const USER_HEADER = 'User';
+export const AUTHORIZATION_HEADER = 'Authorization';
+
+@Injectable()
+export class HttpClient {
+
+ process: Subject<Action> = new Subject<Action>();
+
+ error: Subject<any> = new Subject<any>();
+
+ constructor(private http: Http, private store: Store<fromRoot.State>) {
+ }
+
+ public get(url: string, options?: RequestOptionsArgs, silent?: boolean): Observable<any> {
+ return this.createRequest(RequestMethod.Get, url, undefined, options, silent);
+ }
+
+ public post(url: string, body: any, options?: RequestOptionsArgs, silent?: boolean): Observable<any> {
+ return this.createRequest(RequestMethod.Post, url, body, options, silent);
+ }
+
+ public put(url: string, body: any, options?: RequestOptionsArgs): Observable<any> {
+ return this.createRequest(RequestMethod.Put, url, body, options);
+ }
+
+ public delete(url: string, options?: RequestOptionsArgs): Observable<any> {
+ return this.createRequest(RequestMethod.Delete, url, undefined, options);
+ }
+
+ private _buildRequestOptions(method: RequestMethod, url: string, body: any, tenant: string, username: string,
+ accessToken: string, options?: RequestOptionsArgs): RequestOptions {
+ options = options || {};
+
+ const headers = new Headers();
+
+ if (!(body instanceof FormData)) {
+ headers.set('Accept', 'application/json');
+ headers.set('Content-Type', 'application/json');
+ }
+
+ headers.set(TENANT_HEADER, tenant);
+ headers.set(USER_HEADER, username);
+ headers.set(AUTHORIZATION_HEADER, accessToken);
+
+ const requestOptions: RequestOptions = new RequestOptions({
+ method: method,
+ url: url,
+ body: body,
+ headers: headers
+ });
+
+ return requestOptions.merge(options);
+ }
+
+ private createRequest(method: RequestMethod, url: string, body?: any, options?: RequestOptionsArgs, silent?: boolean): Observable<any> {
+ return this.store.select(fromRoot.getAuthenticationState)
+ .take(1)
+ .map(state => this._buildRequestOptions(method, url, body, state.tenant, state.username, state.authentication.accessToken, options))
+ .flatMap(requestOptions => {
+ this.process.next(Action.QueryStart);
+
+ const request: Observable<any> = this.http.request(new Request(requestOptions))
+ .catch((err: any) => {
+ const error = err.json();
+ if (silent) {
+ return Observable.throw(error);
+ }
+
+ switch (error.status) {
+ case 409:
+ return Observable.throw(error);
+ case 401:
+ case 403:
+ this.store.dispatch({type: LOGOUT});
+ return Observable.throw('User is not authenticated');
+ default:
+ console.error('Error', error);
+ this.error.next(error);
+ return Observable.throw(error);
+ }
+ }).finally(() => this.process.next(Action.QueryStop));
+
+ return request.map((res: Response) => {
+ if (res.text()) {
+ try {
+ return res.json();
+ } catch (err) {
+ return res.text();
+ }
+ }
+ });
+ });
+ }
+
+}
diff --git a/src/app/services/identity/domain/authentication.model.ts b/src/app/services/identity/domain/authentication.model.ts
new file mode 100644
index 0000000..25ed770
--- /dev/null
+++ b/src/app/services/identity/domain/authentication.model.ts
@@ -0,0 +1,36 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export class Authentication {
+ tokenType: string;
+ accessToken: string;
+ accessTokenExpiration: string;
+ refreshTokenExpiration: string;
+ passwordExpiration: string;
+
+ constructor(tokenType: string,
+ accessToken: string, accessTokenExpiration: string,
+ refreshTokenExpiration: string,
+ passwordExpiration: string) {
+ this.tokenType = tokenType;
+ this.accessToken = accessToken;
+ this.accessTokenExpiration = accessTokenExpiration;
+ this.refreshTokenExpiration = refreshTokenExpiration;
+ this.passwordExpiration = passwordExpiration;
+ }
+}
diff --git a/src/app/services/identity/domain/password.model.ts b/src/app/services/identity/domain/password.model.ts
new file mode 100644
index 0000000..2b6a48b
--- /dev/null
+++ b/src/app/services/identity/domain/password.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export class Password {
+ password: string;
+
+ constructor(password: string) {
+ this.password = password;
+ }
+}
diff --git a/src/app/services/identity/domain/permission.model.ts b/src/app/services/identity/domain/permission.model.ts
new file mode 100644
index 0000000..436f14d
--- /dev/null
+++ b/src/app/services/identity/domain/permission.model.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface Permission {
+ permittableEndpointGroupIdentifier: string;
+ allowedOperations: AllowedOperation[];
+}
+
+export type AllowedOperation = 'READ' | 'CHANGE' | 'DELETE';
diff --git a/src/app/services/identity/domain/permittable-group-ids.model.ts b/src/app/services/identity/domain/permittable-group-ids.model.ts
new file mode 100644
index 0000000..271ae45
--- /dev/null
+++ b/src/app/services/identity/domain/permittable-group-ids.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export class IdentityPermittableGroupIds {
+ public static readonly IDENTITY_MANAGEMENT = 'identity__v1__users';
+ public static readonly ROLE_MANAGEMENT = 'identity__v1__roles';
+ public static readonly SELF_MANAGEMENT = 'identity__v1__self';
+}
+
+
diff --git a/src/app/services/identity/domain/role-identifier.model.ts b/src/app/services/identity/domain/role-identifier.model.ts
new file mode 100644
index 0000000..50424b5
--- /dev/null
+++ b/src/app/services/identity/domain/role-identifier.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export class RoleIdentifier {
+ identifier: string;
+
+ constructor(identifier: string) {
+ this.identifier = identifier;
+ }
+}
diff --git a/src/app/services/identity/domain/role.model.ts b/src/app/services/identity/domain/role.model.ts
new file mode 100644
index 0000000..123b657
--- /dev/null
+++ b/src/app/services/identity/domain/role.model.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Permission} from './permission.model';
+
+export interface Role {
+ identifier: string;
+ permissions: Permission[];
+}
diff --git a/src/app/services/identity/domain/user-with-password.model.ts b/src/app/services/identity/domain/user-with-password.model.ts
new file mode 100644
index 0000000..4369788
--- /dev/null
+++ b/src/app/services/identity/domain/user-with-password.model.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export class UserWithPassword {
+ identifier: string;
+ role: string;
+ password: string;
+}
diff --git a/src/app/services/identity/domain/user.model.ts b/src/app/services/identity/domain/user.model.ts
new file mode 100644
index 0000000..ca761c7
--- /dev/null
+++ b/src/app/services/identity/domain/user.model.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export class User {
+ identifier: string;
+ role: string;
+}
diff --git a/src/app/services/identity/identity.service.ts b/src/app/services/identity/identity.service.ts
new file mode 100644
index 0000000..63ba3a0
--- /dev/null
+++ b/src/app/services/identity/identity.service.ts
@@ -0,0 +1,101 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Inject, Injectable} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {Error} from '../domain/error.model';
+import {HttpClient} from '../http/http.service';
+import {Password} from './domain/password.model';
+import {UserWithPassword} from './domain/user-with-password.model';
+import {Role} from './domain/role.model';
+import {RoleIdentifier} from './domain/role-identifier.model';
+import {User} from './domain/user.model';
+import {PermittableGroup} from '../anubis/permittable-group.model';
+
+@Injectable()
+export class IdentityService {
+
+ private static encodePassword(password: string): string {
+ return btoa(password);
+ }
+
+ constructor(private http: HttpClient, @Inject('identityBaseUrl') private baseUrl: string) {
+ }
+
+ changePassword(id: string, password: Password): Observable<any> {
+ password.password = IdentityService.encodePassword(password.password);
+ return this.http.put(this.baseUrl + '/users/' + id + '/password', password)
+ .catch(Error.handleError);
+ }
+
+ createUser(user: UserWithPassword): Observable<any> {
+ user.password = IdentityService.encodePassword(user.password);
+ return this.http.post(this.baseUrl + '/users', user)
+ .catch(Error.handleError);
+ }
+
+ getUser(id: string): Observable<User> {
+ return this.http.get(this.baseUrl + '/users/' + id)
+ .catch(Error.handleError);
+ }
+
+ changeUserRole(user: string, roleIdentifier: RoleIdentifier): Observable<any> {
+ return this.http.put(this.baseUrl + '/users/' + user + '/roleIdentifier', roleIdentifier)
+ .catch(Error.handleError);
+ }
+
+ listRoles(): Observable<Role[]> {
+ return this.http.get(this.baseUrl + '/roles')
+ .catch(Error.handleError);
+ }
+
+ getRole(id: string): Observable<Role> {
+ return this.http.get(this.baseUrl + '/roles/' + id)
+ .catch(Error.handleError);
+ }
+
+ createRole(role: Role): Observable<any> {
+ return this.http.post(this.baseUrl + '/roles', role)
+ .catch(Error.handleError);
+ }
+
+ changeRole(role: Role): Observable<any> {
+ return this.http.put(this.baseUrl + '/roles/' + role.identifier, role)
+ .catch(Error.handleError);
+ }
+
+ deleteRole(id: String): Observable<any> {
+ return this.http.delete(this.baseUrl + '/roles/' + id, {})
+ .catch(Error.handleError);
+ }
+
+ createPermittableGroup(permittableGroup: PermittableGroup): Observable<PermittableGroup> {
+ return this.http.post(this.baseUrl + '/permittablegroups', permittableGroup)
+ .catch(Error.handleError);
+ }
+
+ getPermittableGroup(id: string): Observable<PermittableGroup> {
+ return this.http.get(this.baseUrl + '/permittablegroups/' + id)
+ .catch(Error.handleError);
+ }
+
+ getPermittableGroups(): Observable<PermittableGroup[]> {
+ return this.http.get(this.baseUrl + '/permittablegroups')
+ .catch(Error.handleError);
+ }
+}
diff --git a/src/app/services/image/image.service.ts b/src/app/services/image/image.service.ts
new file mode 100644
index 0000000..ac08f2b
--- /dev/null
+++ b/src/app/services/image/image.service.ts
@@ -0,0 +1,59 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Headers, Http, Response, ResponseContentType} from '@angular/http';
+import {Observable} from 'rxjs/Observable';
+import {Store} from '@ngrx/store';
+import * as fromRoot from '../../store';
+import {AUTHORIZATION_HEADER, TENANT_HEADER, USER_HEADER} from '../http/http.service';
+import {State} from '../../store/security/authentication.reducer';
+
+@Injectable()
+export class ImageService {
+
+ constructor(private http: Http, private store: Store<fromRoot.State>) {
+ }
+
+ public getImage(url: string): Observable<Blob> {
+ return this.store.select(fromRoot.getAuthenticationState)
+ .take(1)
+ .map(this.mapHeader)
+ .switchMap((headers: Headers) =>
+ this.http.get(url, {
+ responseType: ResponseContentType.Blob,
+ headers: headers
+ })
+ .map((response: Response) => response.blob())
+ .catch(() => Observable.empty()));
+ }
+
+
+ private mapHeader(authenticationState: State): Headers {
+ const headers = new Headers();
+
+ headers.set('Accept', 'application/json');
+
+ headers.set(TENANT_HEADER, authenticationState.tenant);
+ headers.set(USER_HEADER, authenticationState.username);
+ headers.set(AUTHORIZATION_HEADER, authenticationState.authentication.accessToken);
+
+ return headers;
+ }
+
+}
diff --git a/src/app/services/notification/notification.service.ts b/src/app/services/notification/notification.service.ts
new file mode 100644
index 0000000..96ba681
--- /dev/null
+++ b/src/app/services/notification/notification.service.ts
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {BehaviorSubject} from 'rxjs/BehaviorSubject';
+
+export enum NotificationType {
+ MESSAGE, ALERT
+}
+
+export interface NotificationEvent {
+ type: NotificationType;
+ title?: string;
+ message: string;
+}
+
+@Injectable()
+export class NotificationService {
+
+ private notificationSource = new BehaviorSubject<NotificationEvent>(null);
+
+ notifications$ = this.notificationSource.asObservable();
+
+ send(notification: NotificationEvent) {
+ this.notificationSource.next(notification);
+ }
+}
diff --git a/src/app/services/office/domain/employee-page.model.ts b/src/app/services/office/domain/employee-page.model.ts
new file mode 100644
index 0000000..5e55739
--- /dev/null
+++ b/src/app/services/office/domain/employee-page.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Employee} from './employee.model';
+
+export interface EmployeePage {
+ employees: Employee[];
+ totalPages: number;
+ totalElements: number;
+}
diff --git a/src/app/services/office/domain/employee.model.ts b/src/app/services/office/domain/employee.model.ts
new file mode 100644
index 0000000..485e515
--- /dev/null
+++ b/src/app/services/office/domain/employee.model.ts
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ContactDetail} from '../../domain/contact/contact-detail.model';
+
+export interface Employee {
+ identifier: string;
+ givenName: string;
+ middleName?: string;
+ surname: string;
+ assignedOffice?: string;
+ contactDetails: ContactDetail[];
+}
diff --git a/src/app/services/office/domain/office-page.model.ts b/src/app/services/office/domain/office-page.model.ts
new file mode 100644
index 0000000..07d0400
--- /dev/null
+++ b/src/app/services/office/domain/office-page.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Office} from './office.model';
+
+export interface OfficePage {
+ offices: Office[];
+ totalPages: number;
+ totalElements: number;
+}
diff --git a/src/app/services/office/domain/office.model.ts b/src/app/services/office/domain/office.model.ts
new file mode 100644
index 0000000..4fc8019
--- /dev/null
+++ b/src/app/services/office/domain/office.model.ts
@@ -0,0 +1,30 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Address} from '../../domain/address/address.model';
+
+export interface Office {
+ identifier: string;
+ parentIdentifier?: string;
+ name: string;
+ description?: string;
+ address?: Address;
+ branches?: Office[];
+ tellerIds?: string[];
+ externalReferences?: boolean;
+}
diff --git a/src/app/services/office/domain/permittable-group-ids.model.ts b/src/app/services/office/domain/permittable-group-ids.model.ts
new file mode 100644
index 0000000..cec0a5a
--- /dev/null
+++ b/src/app/services/office/domain/permittable-group-ids.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export class OfficePermittableGroupIds {
+ public static readonly OFFICE_MANAGEMENT = 'office__v1__offices';
+ public static readonly EMPLOYEE_MANAGEMENT = 'office__v1__employees';
+ public static readonly SELF_MANAGEMENT = 'office__v1__self';
+}
+
+
diff --git a/src/app/services/office/office.service.ts b/src/app/services/office/office.service.ts
new file mode 100644
index 0000000..6496062
--- /dev/null
+++ b/src/app/services/office/office.service.ts
@@ -0,0 +1,118 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Inject, Injectable} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {HttpClient} from '../http/http.service';
+import {Error} from '../domain/error.model';
+import {RequestOptionsArgs, URLSearchParams} from '@angular/http';
+import {Office} from './domain/office.model';
+import {FetchRequest} from '../domain/paging/fetch-request.model';
+import {OfficePage} from './domain/office-page.model';
+import {EmployeePage} from './domain/employee-page.model';
+import {Employee} from './domain/employee.model';
+import {buildSearchParams} from '../domain/paging/search-param.builder';
+import {ContactDetail} from '../domain/contact/contact-detail.model';
+
+@Injectable()
+export class OfficeService {
+
+ constructor(private http: HttpClient, @Inject('officeBaseUrl') private baseUrl: string) {
+ }
+
+ createOffice(office: Office): Observable<Office> {
+ return this.http.post(this.baseUrl + '/offices', office)
+ .catch(Error.handleError);
+ }
+
+ addBranch(id: string, office: Office): Observable<Office> {
+ return this.http.post(this.baseUrl + '/offices/' + id, office)
+ .catch(Error.handleError);
+ }
+
+ updateOffice(office: Office): Observable<Office> {
+ return this.http.put(this.baseUrl + '/offices/' + office.identifier, office)
+ .catch(Error.handleError);
+ }
+
+ deleteOffice(id: String): Observable<Office> {
+ return this.http.delete(this.baseUrl + '/offices/' + id, {})
+ .catch(Error.handleError);
+ }
+
+ listOffices(fetchRequest?: FetchRequest): Observable<OfficePage> {
+ const params: URLSearchParams = buildSearchParams(fetchRequest);
+
+ const requestOptions: RequestOptionsArgs = {
+ search: params
+ };
+ return this.http.get(this.baseUrl + '/offices', requestOptions)
+ .catch(Error.handleError);
+ }
+
+ listBranches(parentIdentifier: string, fetchRequest?: FetchRequest): Observable<OfficePage> {
+ const params: URLSearchParams = buildSearchParams(fetchRequest);
+
+ const requestOptions: RequestOptionsArgs = {
+ search: params
+ };
+ return this.http.get(this.baseUrl + '/offices/' + parentIdentifier + '/branches', requestOptions)
+ .catch(Error.handleError);
+ }
+
+ getOffice(id: string): Observable<Office> {
+ return this.http.get(this.baseUrl + '/offices/' + id)
+ .catch(Error.handleError);
+ }
+
+ listEmployees(fetchRequest?: FetchRequest): Observable<EmployeePage> {
+ const params: URLSearchParams = buildSearchParams(fetchRequest);
+
+ const requestOptions: RequestOptionsArgs = {
+ search: params
+ };
+
+ return this.http.get(this.baseUrl + '/employees', requestOptions)
+ .catch(Error.handleError);
+ }
+
+ getEmployee(id: string, silent?: true): Observable<Employee> {
+ return this.http.get(this.baseUrl + '/employees/' + id, {}, silent)
+ .catch(Error.handleError);
+ }
+
+ createEmployee(employee: Employee): Observable<Employee> {
+ return this.http.post(this.baseUrl + '/employees', employee)
+ .catch(Error.handleError);
+ }
+
+ updateEmployee(employee: Employee): Observable<Employee> {
+ return this.http.put(this.baseUrl + '/employees/' + employee.identifier, employee)
+ .catch(Error.handleError);
+ }
+
+ deleteEmployee(id: string): Observable<Employee> {
+ return this.http.delete(this.baseUrl + '/employees/' + id, {})
+ .catch(Error.handleError);
+ }
+
+ setContactDetails(id: string, contactDetails: ContactDetail[]): Observable<void> {
+ return this.http.put(this.baseUrl + '/employees/' + id + '/contacts', contactDetails);
+ }
+
+}
diff --git a/src/app/services/payroll/domain/payroll-allocation.model.ts b/src/app/services/payroll/domain/payroll-allocation.model.ts
new file mode 100644
index 0000000..c9de4e5
--- /dev/null
+++ b/src/app/services/payroll/domain/payroll-allocation.model.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface PayrollAllocation {
+ accountNumber: string;
+ amount: string;
+ proportional: boolean;
+}
diff --git a/src/app/services/payroll/domain/payroll-collection-history.model.ts b/src/app/services/payroll/domain/payroll-collection-history.model.ts
new file mode 100644
index 0000000..87c0a30
--- /dev/null
+++ b/src/app/services/payroll/domain/payroll-collection-history.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export interface PayrollCollectionHistory {
+ identifier?: string;
+ sourceAccountNumber: string;
+ createdBy: string;
+ createdOn: string;
+}
diff --git a/src/app/services/payroll/domain/payroll-collection-sheet.model.ts b/src/app/services/payroll/domain/payroll-collection-sheet.model.ts
new file mode 100644
index 0000000..de2b492
--- /dev/null
+++ b/src/app/services/payroll/domain/payroll-collection-sheet.model.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {PayrollPayment} from './payroll-payment.model';
+
+export interface PayrollCollectionSheet {
+ sourceAccountNumber: string;
+ payrollPayments: PayrollPayment[];
+}
diff --git a/src/app/services/payroll/domain/payroll-configuration.model.ts b/src/app/services/payroll/domain/payroll-configuration.model.ts
new file mode 100644
index 0000000..21fb5ea
--- /dev/null
+++ b/src/app/services/payroll/domain/payroll-configuration.model.ts
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {PayrollAllocation} from './payroll-allocation.model';
+
+export interface PayrollConfiguration {
+ mainAccountNumber: string;
+ payrollAllocations: PayrollAllocation[];
+ createdBy?: string;
+ createdOn?: string;
+ lastModifiedBy?: string;
+ lastModifiedOn?: string;
+}
diff --git a/src/app/services/payroll/domain/payroll-payment-page.model.ts b/src/app/services/payroll/domain/payroll-payment-page.model.ts
new file mode 100644
index 0000000..e0ca049
--- /dev/null
+++ b/src/app/services/payroll/domain/payroll-payment-page.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {PayrollPayment} from './payroll-payment.model';
+
+export interface PayrollPaymentPage {
+ payrollPayments: PayrollPayment[];
+ totalPages: number;
+ totalElements: number;
+}
diff --git a/src/app/services/payroll/domain/payroll-payment.model.ts b/src/app/services/payroll/domain/payroll-payment.model.ts
new file mode 100644
index 0000000..252cfba
--- /dev/null
+++ b/src/app/services/payroll/domain/payroll-payment.model.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface PayrollPayment {
+ customerIdentifier: string;
+ employer: string;
+ salary: string;
+}
diff --git a/src/app/services/payroll/domain/permittable-group-ids.ts b/src/app/services/payroll/domain/permittable-group-ids.ts
new file mode 100644
index 0000000..9bb2e99
--- /dev/null
+++ b/src/app/services/payroll/domain/permittable-group-ids.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export class PayrollPermittableGroupIds {
+ public static readonly CONFIGURATION = 'payroll__v1__configuration';
+ public static readonly DISTRIBUTION = 'payroll__v1__distribution';
+}
diff --git a/src/app/services/payroll/payroll.service.ts b/src/app/services/payroll/payroll.service.ts
new file mode 100644
index 0000000..c9236b8
--- /dev/null
+++ b/src/app/services/payroll/payroll.service.ts
@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Inject, Injectable} from '@angular/core';
+import {RequestOptionsArgs} from '@angular/http';
+import {buildSearchParams} from '../domain/paging/search-param.builder';
+import {PayrollPaymentPage} from './domain/payroll-payment-page.model';
+import {Observable} from 'rxjs/Observable';
+import {FetchRequest} from '../domain/paging/fetch-request.model';
+import {PayrollCollectionHistory} from './domain/payroll-collection-history.model';
+import {PayrollCollectionSheet} from './domain/payroll-collection-sheet.model';
+import {HttpClient} from '../http/http.service';
+import {PayrollConfiguration} from './domain/payroll-configuration.model';
+
+@Injectable()
+export class PayrollService {
+
+ constructor(private http: HttpClient, @Inject('payrollBaseUrl') private baseUrl: string) {
+ }
+
+ public distribute(sheet: PayrollCollectionSheet): Observable<void> {
+ return this.http.post(`${this.baseUrl}/distribution`, sheet);
+ }
+
+ public fetchDistributionHistory(): Observable<PayrollCollectionHistory[]> {
+ return this.http.get(`${this.baseUrl}/distribution`);
+ }
+
+ public fetchPayments(identifier: string, fetchRequest?: FetchRequest): Observable<PayrollPaymentPage> {
+ const params: URLSearchParams = buildSearchParams(fetchRequest);
+
+ const requestOptions: RequestOptionsArgs = {
+ params
+ };
+ return this.http.get(`${this.baseUrl}/distribution/${identifier}/payments`, requestOptions);
+ }
+
+ setPayrollConfiguration(customerId: string, configuration: PayrollConfiguration): Observable<void> {
+ return this.http.put(`${this.baseUrl}/customers/${customerId}/payroll`, configuration);
+ }
+
+ findPayrollConfiguration(customerId: string, silent: boolean = false): Observable<PayrollConfiguration> {
+ return this.http.get(`${this.baseUrl}/customers/${customerId}/payroll`, {}, silent);
+ }
+}
diff --git a/src/app/services/portfolio/domain/account-assignment.model.ts b/src/app/services/portfolio/domain/account-assignment.model.ts
new file mode 100644
index 0000000..7cb183e
--- /dev/null
+++ b/src/app/services/portfolio/domain/account-assignment.model.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface AccountAssignment {
+ designator: string;
+ accountIdentifier?: string;
+ ledgerIdentifier?: string;
+}
diff --git a/src/app/services/portfolio/domain/balance-range.model.ts b/src/app/services/portfolio/domain/balance-range.model.ts
new file mode 100644
index 0000000..0c4b5aa
--- /dev/null
+++ b/src/app/services/portfolio/domain/balance-range.model.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface BalanceRange {
+ minimum: number;
+ maximum: number;
+}
diff --git a/src/app/services/portfolio/domain/balance-segment-set.model.ts b/src/app/services/portfolio/domain/balance-segment-set.model.ts
new file mode 100644
index 0000000..d39cb5c
--- /dev/null
+++ b/src/app/services/portfolio/domain/balance-segment-set.model.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface BalanceSegmentSet {
+ identifier: string;
+ segments: number[];
+ segmentIdentifiers: string[];
+}
diff --git a/src/app/services/portfolio/domain/case-command.model.ts b/src/app/services/portfolio/domain/case-command.model.ts
new file mode 100644
index 0000000..6318513
--- /dev/null
+++ b/src/app/services/portfolio/domain/case-command.model.ts
@@ -0,0 +1,27 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {AccountAssignment} from './account-assignment.model';
+
+export interface CaseCommand {
+ oneTimeAccountAssignments?: AccountAssignment[];
+ paymentSize?: number;
+ note?: string;
+ createdOn: string;
+ createdBy?: string;
+}
diff --git a/src/app/services/portfolio/domain/case-customer-documents.model.ts b/src/app/services/portfolio/domain/case-customer-documents.model.ts
new file mode 100644
index 0000000..6aec456
--- /dev/null
+++ b/src/app/services/portfolio/domain/case-customer-documents.model.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface Document {
+ customerId: string;
+ documentId: string;
+}
+
+export interface CaseCustomerDocuments {
+ documents: Document[]
+}
diff --git a/src/app/services/portfolio/domain/case-page.model.ts b/src/app/services/portfolio/domain/case-page.model.ts
new file mode 100644
index 0000000..90fbf31
--- /dev/null
+++ b/src/app/services/portfolio/domain/case-page.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Case} from './case.model';
+
+export interface CasePage {
+ elements: Case[];
+ totalPages: number;
+ totalElements: number;
+}
diff --git a/src/app/services/portfolio/domain/case-state.model.ts b/src/app/services/portfolio/domain/case-state.model.ts
new file mode 100644
index 0000000..91defff
--- /dev/null
+++ b/src/app/services/portfolio/domain/case-state.model.ts
@@ -0,0 +1,19 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export type CaseState = 'CREATED' | 'PENDING' | 'APPROVED' | 'ACTIVE' | 'CLOSED';
diff --git a/src/app/services/portfolio/domain/case.model.ts b/src/app/services/portfolio/domain/case.model.ts
new file mode 100644
index 0000000..6856b44
--- /dev/null
+++ b/src/app/services/portfolio/domain/case.model.ts
@@ -0,0 +1,33 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {AccountAssignment} from './account-assignment.model';
+import {CaseState} from './case-state.model';
+
+export interface Case {
+ identifier: string;
+ productIdentifier: string;
+ interest: number;
+ parameters: string;
+ accountAssignments: AccountAssignment[];
+ currentState?: CaseState;
+ createdOn?: string;
+ createdBy?: string;
+ lastModifiedOn?: string;
+ lastModifiedBy?: string;
+}
diff --git a/src/app/services/portfolio/domain/charge-definition.model.ts b/src/app/services/portfolio/domain/charge-definition.model.ts
new file mode 100644
index 0000000..599593e
--- /dev/null
+++ b/src/app/services/portfolio/domain/charge-definition.model.ts
@@ -0,0 +1,41 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ChargeMethod} from './charge-method.model';
+import {ChronoUnit} from './chrono-unit.model';
+import {WorkflowAction} from './individuallending/workflow-action.model';
+
+export interface ChargeDefinition {
+ identifier: string;
+ name: string;
+ description: string;
+ chargeAction: WorkflowAction;
+ chargeMethod: ChargeMethod;
+ amount: number;
+ fromAccountDesignator: string;
+ toAccountDesignator: string;
+ forCycleSizeUnit: ChronoUnit;
+ accrualAccountDesignator?: string;
+ accrueAction?: WorkflowAction;
+ readOnly?: boolean;
+ proportionalTo: string;
+ forSegmentSet?: string;
+ fromSegment?: string;
+ toSegment?: string;
+ chargeOnTop?: boolean;
+}
diff --git a/src/app/services/portfolio/domain/charge-method.model.ts b/src/app/services/portfolio/domain/charge-method.model.ts
new file mode 100644
index 0000000..1bc00c9
--- /dev/null
+++ b/src/app/services/portfolio/domain/charge-method.model.ts
@@ -0,0 +1,19 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export type ChargeMethod = 'FIXED' | 'PROPORTIONAL' | 'INTEREST';
diff --git a/src/app/services/portfolio/domain/chrono-unit.model.ts b/src/app/services/portfolio/domain/chrono-unit.model.ts
new file mode 100644
index 0000000..7fb845e
--- /dev/null
+++ b/src/app/services/portfolio/domain/chrono-unit.model.ts
@@ -0,0 +1,20 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export type ChronoUnit = 'WEEKS' | 'MONTHS' | 'YEARS';
diff --git a/src/app/services/portfolio/domain/cost-component.model.ts b/src/app/services/portfolio/domain/cost-component.model.ts
new file mode 100644
index 0000000..8c955f1
--- /dev/null
+++ b/src/app/services/portfolio/domain/cost-component.model.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export class CostComponent {
+ chargeIdentifier: string;
+ amount: number;
+}
diff --git a/src/app/services/portfolio/domain/fims-case-page.model.ts b/src/app/services/portfolio/domain/fims-case-page.model.ts
new file mode 100644
index 0000000..37f4a75
--- /dev/null
+++ b/src/app/services/portfolio/domain/fims-case-page.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {FimsCase} from './fims-case.model';
+
+export interface FimsCasePage {
+ elements: FimsCase[];
+ totalPages: number;
+ totalElements: number;
+}
diff --git a/src/app/services/portfolio/domain/fims-case.model.ts b/src/app/services/portfolio/domain/fims-case.model.ts
new file mode 100644
index 0000000..36985e8
--- /dev/null
+++ b/src/app/services/portfolio/domain/fims-case.model.ts
@@ -0,0 +1,34 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {CaseParameters} from './individuallending/case-parameters.model';
+import {CaseState} from './case-state.model';
+
+export interface FimsCase {
+ identifier: string;
+ productIdentifier: string;
+ interest: number;
+ parameters: CaseParameters;
+ depositAccountIdentifier: string;
+ customerLoanAccountIdentifier?: string;
+ currentState?: CaseState;
+ createdOn?: string;
+ createdBy?: string;
+ lastModifiedOn?: string;
+ lastModifiedBy?: string;
+}
diff --git a/src/app/services/portfolio/domain/individuallending/account-designators.model.ts b/src/app/services/portfolio/domain/individuallending/account-designators.model.ts
new file mode 100644
index 0000000..aefeb4a
--- /dev/null
+++ b/src/app/services/portfolio/domain/individuallending/account-designators.model.ts
@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+/**
+ * A debit is an accounting entry that either increases an asset or expense account, or decreases a liability or equity account.
+ * It is positioned to the left in an accounting entry.
+ *
+ * A credit is an accounting entry that either increases a liability or equity account, or decreases an asset or expense account.
+ */
+
+export class AccountDesignators {
+
+ public static readonly CUSTOMER_LOAN_GROUP = 'cll';
+
+ public static readonly CUSTOMER_LOAN_PRINCIPAL = 'clp';
+
+ public static readonly CUSTOMER_LOAN_INTEREST = 'cli';
+
+ public static readonly CUSTOMER_LOAN_FEES = 'clf';
+
+ public static readonly LOAN_FUNDS_SOURCE = 'ls';
+
+ public static readonly PROCESSING_FEE_INCOME = 'pfi';
+
+ public static readonly ORIGINATION_FEE_INCOME = 'ofi';
+
+ public static readonly DISBURSEMENT_FEE_INCOME = 'dfi';
+
+ public static readonly INTEREST_INCOME = 'ii';
+
+ public static readonly INTEREST_ACCRUAL = 'ia';
+
+ public static readonly LATE_FEE_INCOME = 'lfi';
+
+ public static readonly LATE_FEE_ACCRUAL = 'lfa';
+
+ public static readonly PRODUCT_LOSS_ALLOWANCE = 'pa';
+
+ public static readonly GENERAL_LOSS_ALLOWANCE = 'aa';
+
+ public static readonly GENERAL_EXPENSE = 'ge';
+
+ public static readonly ENTRY = 'ey';
+
+}
diff --git a/src/app/services/portfolio/domain/individuallending/case-parameters.model.ts b/src/app/services/portfolio/domain/individuallending/case-parameters.model.ts
new file mode 100644
index 0000000..40bbc43
--- /dev/null
+++ b/src/app/services/portfolio/domain/individuallending/case-parameters.model.ts
@@ -0,0 +1,29 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {TermRange} from '../term-range.model';
+import {PaymentCycle} from '../payment-cycle.model';
+import {CreditWorthinessSnapshot} from './credit-worthiness-snapshot.model';
+
+export interface CaseParameters {
+ customerIdentifier: string;
+ termRange: TermRange;
+ maximumBalance: number;
+ paymentCycle: PaymentCycle;
+ creditWorthinessSnapshots: CreditWorthinessSnapshot[];
+}
diff --git a/src/app/services/portfolio/domain/individuallending/charge-name.model.ts b/src/app/services/portfolio/domain/individuallending/charge-name.model.ts
new file mode 100644
index 0000000..3d41229
--- /dev/null
+++ b/src/app/services/portfolio/domain/individuallending/charge-name.model.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface ChargeName {
+ identifier: string;
+ name: string;
+}
diff --git a/src/app/services/portfolio/domain/individuallending/charge-proportional-designators.model.ts b/src/app/services/portfolio/domain/individuallending/charge-proportional-designators.model.ts
new file mode 100644
index 0000000..3c5f513
--- /dev/null
+++ b/src/app/services/portfolio/domain/individuallending/charge-proportional-designators.model.ts
@@ -0,0 +1,37 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export class ChargeProportionalDesignators {
+ public static readonly NOT_PROPORTIONAL = '{notproportional}';
+
+ public static readonly MAXIMUM_BALANCE_DESIGNATOR = '{maximumbalance}';
+
+ public static readonly RUNNING_BALANCE_DESIGNATOR = '{runningbalance}';
+
+ public static readonly PRINCIPAL_DESIGNATOR = '{principal}';
+
+ public static readonly REQUESTED_DISBURSEMENT_DESIGNATOR = '{requesteddisbursement}';
+
+ public static readonly TO_ACCOUNT_DESIGNATOR = '{toAccount}';
+
+ public static readonly FROM_ACCOUNT_DESIGNATOR = '{fromAccount}';
+
+ public static readonly REQUESTED_REPAYMENT_DESIGNATOR = '{requestedrepayment}';
+
+ public static readonly CONTRACTUAL_REPAYMENT_DESIGNATOR = '{contractualrepayment}';
+}
diff --git a/src/app/services/portfolio/domain/individuallending/credit-worthiness-factor.model.ts b/src/app/services/portfolio/domain/individuallending/credit-worthiness-factor.model.ts
new file mode 100644
index 0000000..d128f4b
--- /dev/null
+++ b/src/app/services/portfolio/domain/individuallending/credit-worthiness-factor.model.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface CreditWorthinessFactor {
+ description?: string;
+ amount: string;
+}
diff --git a/src/app/services/portfolio/domain/individuallending/credit-worthiness-snapshot.model.ts b/src/app/services/portfolio/domain/individuallending/credit-worthiness-snapshot.model.ts
new file mode 100644
index 0000000..fca8446
--- /dev/null
+++ b/src/app/services/portfolio/domain/individuallending/credit-worthiness-snapshot.model.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {CreditWorthinessFactor} from './credit-worthiness-factor.model';
+
+export interface CreditWorthinessSnapshot {
+ forCustomer: string;
+ incomeSources: CreditWorthinessFactor[];
+ assets: CreditWorthinessFactor[];
+ debts: CreditWorthinessFactor[];
+}
diff --git a/src/app/services/portfolio/domain/individuallending/document.model.ts b/src/app/services/portfolio/domain/individuallending/document.model.ts
new file mode 100644
index 0000000..d77a8eb
--- /dev/null
+++ b/src/app/services/portfolio/domain/individuallending/document.model.ts
@@ -0,0 +1,21 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface Document {
+ description: string;
+}
diff --git a/src/app/services/portfolio/domain/individuallending/moratorium.model.ts b/src/app/services/portfolio/domain/individuallending/moratorium.model.ts
new file mode 100644
index 0000000..50f910a
--- /dev/null
+++ b/src/app/services/portfolio/domain/individuallending/moratorium.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ChronoUnit} from '../chrono-unit.model';
+
+export class Moratorium {
+ chargeTask: string;
+ temporalUnit: ChronoUnit;
+ period: number;
+}
diff --git a/src/app/services/portfolio/domain/individuallending/planned-payment-page.model.ts b/src/app/services/portfolio/domain/individuallending/planned-payment-page.model.ts
new file mode 100644
index 0000000..9c7a0c8
--- /dev/null
+++ b/src/app/services/portfolio/domain/individuallending/planned-payment-page.model.ts
@@ -0,0 +1,27 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {PlannedPayment} from './planned-payment.model';
+import {ChargeName} from './charge-name.model';
+
+export class PlannedPaymentPage {
+ elements: PlannedPayment[];
+ chargeNames: ChargeName[];
+ totalPages: number;
+ totalElements: number;
+}
diff --git a/src/app/services/portfolio/domain/individuallending/planned-payment.model.ts b/src/app/services/portfolio/domain/individuallending/planned-payment.model.ts
new file mode 100644
index 0000000..6b68557
--- /dev/null
+++ b/src/app/services/portfolio/domain/individuallending/planned-payment.model.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Payment} from '../payment.model';
+
+export interface PlannedPayment {
+ payment: Payment;
+ balances: { [id: string]: number };
+}
diff --git a/src/app/services/portfolio/domain/individuallending/product-parameters.model.ts b/src/app/services/portfolio/domain/individuallending/product-parameters.model.ts
new file mode 100644
index 0000000..5e8a507
--- /dev/null
+++ b/src/app/services/portfolio/domain/individuallending/product-parameters.model.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Moratorium} from './moratorium.model';
+
+export class ProductParameters {
+ moratoriums: Moratorium[];
+ maximumDispersalCount: number;
+ maximumDispersalAmount: number;
+ minimumDispersalAmount: number;
+}
diff --git a/src/app/services/portfolio/domain/individuallending/workflow-action.model.ts b/src/app/services/portfolio/domain/individuallending/workflow-action.model.ts
new file mode 100644
index 0000000..69a7127
--- /dev/null
+++ b/src/app/services/portfolio/domain/individuallending/workflow-action.model.ts
@@ -0,0 +1,29 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export type WorkflowAction =
+ 'OPEN'
+ | 'DENY'
+ | 'APPROVE'
+ | 'ACCEPT_PAYMENT'
+ | 'DISBURSE'
+ | 'MARK_LATE'
+ | 'APPLY_INTEREST'
+ | 'WRITE_OFF'
+ | 'CLOSE'
+ | 'RECOVER';
diff --git a/src/app/services/portfolio/domain/interest-basis.model.ts b/src/app/services/portfolio/domain/interest-basis.model.ts
new file mode 100644
index 0000000..fbad706
--- /dev/null
+++ b/src/app/services/portfolio/domain/interest-basis.model.ts
@@ -0,0 +1,19 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export type InterestBasis = 'CURRENT_BALANCE' | 'BEGINNING_BALANCE';
diff --git a/src/app/services/portfolio/domain/interest-range.model.ts b/src/app/services/portfolio/domain/interest-range.model.ts
new file mode 100644
index 0000000..fb97846
--- /dev/null
+++ b/src/app/services/portfolio/domain/interest-range.model.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export interface InterestRange {
+ minimum: number;
+ maximum: number;
+}
diff --git a/src/app/services/portfolio/domain/loss-provision-configuration.model.ts b/src/app/services/portfolio/domain/loss-provision-configuration.model.ts
new file mode 100644
index 0000000..08c8a77
--- /dev/null
+++ b/src/app/services/portfolio/domain/loss-provision-configuration.model.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {LossProvisionStep} from './loss-provision-step.model';
+
+export interface LossProvisionConfiguration {
+ lossProvisionSteps: LossProvisionStep[];
+}
diff --git a/src/app/services/portfolio/domain/loss-provision-step.model.ts b/src/app/services/portfolio/domain/loss-provision-step.model.ts
new file mode 100644
index 0000000..b11e708
--- /dev/null
+++ b/src/app/services/portfolio/domain/loss-provision-step.model.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface LossProvisionStep {
+ daysLate: number;
+ percentProvision: number;
+}
diff --git a/src/app/services/portfolio/domain/mapper/fims-case-page.mapper.ts b/src/app/services/portfolio/domain/mapper/fims-case-page.mapper.ts
new file mode 100644
index 0000000..1fbd52a
--- /dev/null
+++ b/src/app/services/portfolio/domain/mapper/fims-case-page.mapper.ts
@@ -0,0 +1,35 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {mapToFimsCase} from './fims-case.mapper';
+import {FimsCasePage} from '../fims-case-page.model';
+import {CasePage} from '../case-page.model';
+
+export function mapToFimsCasePage(casePage: CasePage): FimsCasePage {
+ const elements = [];
+
+ for (const caseInstance of casePage.elements) {
+ elements.push(mapToFimsCase(caseInstance));
+ }
+
+ return {
+ elements: elements,
+ totalPages: casePage.totalPages,
+ totalElements: casePage.totalElements
+ };
+}
diff --git a/src/app/services/portfolio/domain/mapper/fims-case.mapper.ts b/src/app/services/portfolio/domain/mapper/fims-case.mapper.ts
new file mode 100644
index 0000000..9c6ad56
--- /dev/null
+++ b/src/app/services/portfolio/domain/mapper/fims-case.mapper.ts
@@ -0,0 +1,46 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Case} from '../case.model';
+import {AccountDesignators} from '../individuallending/account-designators.model';
+import {accountIdentifier, findAccountDesignator} from '../../../../common/util/account-assignments';
+import {FimsCase} from '../fims-case.model';
+
+export function mapToCase(caseInstance: FimsCase): Case {
+ return Object.assign({}, caseInstance, {
+ parameters: JSON.stringify(caseInstance.parameters),
+ accountAssignments: [
+ {accountIdentifier: caseInstance.depositAccountIdentifier, designator: AccountDesignators.ENTRY}
+ ]
+ });
+}
+
+export function mapToFimsCase(caseInstance: Case): FimsCase {
+ const entryDesignator = findAccountDesignator(caseInstance.accountAssignments, AccountDesignators.ENTRY);
+ const customerLoanDesignator = findAccountDesignator(caseInstance.accountAssignments, AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
+
+ return Object.assign({}, caseInstance, {
+ parameters: JSON.parse(caseInstance.parameters),
+ depositAccountIdentifier: accountIdentifier(entryDesignator),
+ customerLoanAccountIdentifier: accountIdentifier(customerLoanDesignator),
+ });
+}
+
+export function mapToFimsCases(caseInstances: Case[]): FimsCase[] {
+ return caseInstances.map(instance => mapToFimsCase(instance));
+}
diff --git a/src/app/services/portfolio/domain/mapper/fims-range.mapper.ts b/src/app/services/portfolio/domain/mapper/fims-range.mapper.ts
new file mode 100644
index 0000000..1e0a8ca
--- /dev/null
+++ b/src/app/services/portfolio/domain/mapper/fims-range.mapper.ts
@@ -0,0 +1,49 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {BalanceSegmentSet} from '../balance-segment-set.model';
+import {FimsRange} from '../range-model';
+
+export function mapToBalanceSegmentSet(range: FimsRange): BalanceSegmentSet {
+ const balanceSegmentSet: BalanceSegmentSet = {
+ identifier: range.identifier,
+ segments: range.segments.map(segment => segment.start),
+ segmentIdentifiers: range.segments.map(segment => segment.identifier)
+ };
+
+ return balanceSegmentSet;
+}
+
+export function mapToFimsRanges(balanceSegmentSets: BalanceSegmentSet[]): FimsRange[] {
+ return balanceSegmentSets.map(set => mapToFimsRange(set));
+}
+
+export function mapToFimsRange(balanceSegmentSet: BalanceSegmentSet): FimsRange {
+ return {
+ identifier: balanceSegmentSet.identifier,
+ segments: balanceSegmentSet.segments.map((segment, index, array) => ({
+ identifier: balanceSegmentSet.segmentIdentifiers[index],
+ start: segment,
+ end: hasNextIndex(array, index) ? array[index + 1] : undefined
+ }))
+ };
+}
+
+function hasNextIndex(array: number[], index: number): boolean {
+ return array.length - 1 > index;
+}
diff --git a/src/app/services/portfolio/domain/note.model.ts b/src/app/services/portfolio/domain/note.model.ts
new file mode 100644
index 0000000..7be73e2
--- /dev/null
+++ b/src/app/services/portfolio/domain/note.model.ts
@@ -0,0 +1,21 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface Note {
+ content: string;
+}
diff --git a/src/app/services/portfolio/domain/pattern.model.ts b/src/app/services/portfolio/domain/pattern.model.ts
new file mode 100644
index 0000000..d049d20
--- /dev/null
+++ b/src/app/services/portfolio/domain/pattern.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {RequiredAccountAssignment} from './required-account-assignment.model';
+
+export interface Pattern {
+ parameterPackage: string;
+ accountAssignmentGroups: string[];
+ accountAssignmentsRequired: RequiredAccountAssignment[];
+}
diff --git a/src/app/services/portfolio/domain/payment-cycle.model.ts b/src/app/services/portfolio/domain/payment-cycle.model.ts
new file mode 100644
index 0000000..0ff3285
--- /dev/null
+++ b/src/app/services/portfolio/domain/payment-cycle.model.ts
@@ -0,0 +1,27 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ChronoUnit} from './chrono-unit.model';
+
+export interface PaymentCycle {
+ temporalUnit: ChronoUnit;
+ period: number;
+ alignmentDay: number;
+ alignmentWeek: number;
+ alignmentMonth: number;
+}
diff --git a/src/app/services/portfolio/domain/payment.model.ts b/src/app/services/portfolio/domain/payment.model.ts
new file mode 100644
index 0000000..637703e
--- /dev/null
+++ b/src/app/services/portfolio/domain/payment.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {CostComponent} from './cost-component.model';
+
+export interface Payment {
+ costComponents?: CostComponent[];
+ balanceAdjustments: { [key: string]: number };
+ date?: string;
+}
diff --git a/src/app/services/portfolio/domain/permittable-group-ids.ts b/src/app/services/portfolio/domain/permittable-group-ids.ts
new file mode 100644
index 0000000..0e335b6
--- /dev/null
+++ b/src/app/services/portfolio/domain/permittable-group-ids.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export class PortfolioPermittableGroupIds {
+ public static readonly PRODUCT_OPERATIONS_MANAGEMENT = 'portfolio__v1__products__enable';
+ public static readonly PRODUCT_LOSS_PROVISIONING_MANAGEMENT = 'portfolio__v1__products__lossprv';
+ public static readonly PRODUCT_MANAGEMENT = 'portfolio__v1__products';
+ public static readonly CASE_MANAGEMENT = 'portfolio__v1__case';
+ public static readonly CASE_DOCUMENT_MANAGEMENT = 'portfolio__v1__case_documents';
+}
diff --git a/src/app/services/portfolio/domain/product-page.model.ts b/src/app/services/portfolio/domain/product-page.model.ts
new file mode 100644
index 0000000..e6ca2bd
--- /dev/null
+++ b/src/app/services/portfolio/domain/product-page.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Product} from './product.model';
+
+export interface ProductPage {
+ elements: Product[];
+ totalPages: number;
+ totalElements: number;
+}
diff --git a/src/app/services/portfolio/domain/product.model.ts b/src/app/services/portfolio/domain/product.model.ts
new file mode 100644
index 0000000..bf2f47e
--- /dev/null
+++ b/src/app/services/portfolio/domain/product.model.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {BalanceRange} from './balance-range.model';
+import {InterestRange} from './interest-range.model';
+import {TermRange} from './term-range.model';
+import {InterestBasis} from './interest-basis.model';
+import {AccountAssignment} from './account-assignment.model';
+
+export interface Product {
+ identifier: string;
+ name: string;
+ termRange: TermRange;
+ balanceRange: BalanceRange;
+ interestRange: InterestRange;
+ interestBasis: InterestBasis;
+ patternPackage: string;
+ description: string;
+ accountAssignments: AccountAssignment[];
+ parameters: string;
+ currencyCode: string;
+ minorCurrencyUnitDigits: number;
+ enabled?: boolean;
+ createdOn?: string;
+ createdBy?: string;
+ lastModifiedOn?: string;
+ lastModifiedBy?: string;
+}
diff --git a/src/app/services/portfolio/domain/range-model.ts b/src/app/services/portfolio/domain/range-model.ts
new file mode 100644
index 0000000..e43cb23
--- /dev/null
+++ b/src/app/services/portfolio/domain/range-model.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {RangeSegment} from './range-segment.model';
+
+export interface FimsRange {
+ identifier: string;
+ segments: RangeSegment[];
+}
diff --git a/src/app/services/portfolio/domain/range-segment.model.ts b/src/app/services/portfolio/domain/range-segment.model.ts
new file mode 100644
index 0000000..1e8c474
--- /dev/null
+++ b/src/app/services/portfolio/domain/range-segment.model.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface RangeSegment {
+ identifier: string;
+ start: number;
+ end?: number;
+}
diff --git a/src/app/services/portfolio/domain/required-account-assignment.model.ts b/src/app/services/portfolio/domain/required-account-assignment.model.ts
new file mode 100644
index 0000000..371ea33
--- /dev/null
+++ b/src/app/services/portfolio/domain/required-account-assignment.model.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface RequiredAccountAssignment {
+ accountDesignator: string;
+ accountType: string;
+ group?: string;
+}
diff --git a/src/app/services/portfolio/domain/task-definition.model.ts b/src/app/services/portfolio/domain/task-definition.model.ts
new file mode 100644
index 0000000..26f8ae4
--- /dev/null
+++ b/src/app/services/portfolio/domain/task-definition.model.ts
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {WorkflowAction} from './individuallending/workflow-action.model';
+
+export interface TaskDefinition {
+ identifier: string;
+ name: string;
+ description: string;
+ actions: WorkflowAction[];
+ fourEyes: boolean;
+ mandatory: boolean;
+}
diff --git a/src/app/services/portfolio/domain/task-instance.model.ts b/src/app/services/portfolio/domain/task-instance.model.ts
new file mode 100644
index 0000000..44079e7
--- /dev/null
+++ b/src/app/services/portfolio/domain/task-instance.model.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export interface TaskInstance {
+ // task definition identifier
+ taskIdentifier: string;
+ comment: string;
+ executedOn: string;
+ executedBy: string;
+}
diff --git a/src/app/services/portfolio/domain/term-range.model.ts b/src/app/services/portfolio/domain/term-range.model.ts
new file mode 100644
index 0000000..86f2172
--- /dev/null
+++ b/src/app/services/portfolio/domain/term-range.model.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ChronoUnit} from './chrono-unit.model';
+
+export interface TermRange {
+ temporalUnit: ChronoUnit;
+ maximum: number;
+}
diff --git a/src/app/services/portfolio/portfolio.service.ts b/src/app/services/portfolio/portfolio.service.ts
new file mode 100644
index 0000000..63bad61
--- /dev/null
+++ b/src/app/services/portfolio/portfolio.service.ts
@@ -0,0 +1,285 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Inject, Injectable} from '@angular/core';
+import {HttpClient} from '../http/http.service';
+import {Observable} from 'rxjs/Observable';
+import {Product} from './domain/product.model';
+import {RequestOptionsArgs, URLSearchParams} from '@angular/http';
+import {TaskDefinition} from './domain/task-definition.model';
+import {ChargeDefinition} from './domain/charge-definition.model';
+import {FetchRequest} from '../domain/paging/fetch-request.model';
+import {buildSearchParams} from '../domain/paging/search-param.builder';
+import {CaseCommand} from './domain/case-command.model';
+import {TaskInstance} from './domain/task-instance.model';
+import {PlannedPaymentPage} from './domain/individuallending/planned-payment-page.model';
+import {CasePage} from './domain/case-page.model';
+import {AccountAssignment} from './domain/account-assignment.model';
+import {WorkflowAction} from './domain/individuallending/workflow-action.model';
+import {ProductPage} from './domain/product-page.model';
+import {FimsCase} from './domain/fims-case.model';
+import {FimsCasePage} from './domain/fims-case-page.model';
+import {Case} from './domain/case.model';
+import {mapToCase, mapToFimsCase, mapToFimsCases} from './domain/mapper/fims-case.mapper';
+import {mapToFimsCasePage} from './domain/mapper/fims-case-page.mapper';
+import {BalanceSegmentSet} from './domain/balance-segment-set.model';
+import {mapToBalanceSegmentSet, mapToFimsRange, mapToFimsRanges} from './domain/mapper/fims-range.mapper';
+import {FimsRange} from './domain/range-model';
+import {Payment} from './domain/payment.model';
+import {LossProvisionConfiguration} from './domain/loss-provision-configuration.model';
+import {CaseCustomerDocuments} from './domain/case-customer-documents.model';
+
+@Injectable()
+export class PortfolioService {
+
+ constructor(private http: HttpClient, @Inject('portfolioBaseUrl') private baseUrl: string) {
+ }
+
+ findAllPatterns(): Observable<void> {
+ return this.http.get(`${this.baseUrl}/patterns/`);
+ }
+
+ findAllProducts(includeDisabled?: boolean, fetchRequest?: FetchRequest): Observable<ProductPage> {
+ const params: URLSearchParams = buildSearchParams(fetchRequest);
+ params.append('includeDisabled', includeDisabled ? 'true' : 'false');
+
+ const requestOptions: RequestOptionsArgs = {
+ search: params
+ };
+ return this.http.get(`${this.baseUrl}/products/`, requestOptions);
+ }
+
+ createProduct(product: Product): Observable<void> {
+ return this.http.post(`${this.baseUrl}/products`, product);
+ }
+
+ getProduct(identifier: string): Observable<Product> {
+ return this.http.get(`${this.baseUrl}/products/${identifier}`);
+ }
+
+ changeProduct(product: Product): Observable<void> {
+ return this.http.put(`${this.baseUrl}/products/${product.identifier}`, product);
+ }
+
+ deleteProduct(identifier: string): Observable<void> {
+ return this.http.delete(`${this.baseUrl}/products/${identifier}`);
+ }
+
+ enableProduct(identifier: string, enabled: boolean): Observable<void> {
+ return this.http.put(`${this.baseUrl}/products/${identifier}/enabled`, enabled);
+ }
+
+ getProductEnabled(identifier: string): Observable<boolean> {
+ return this.http.get(`${this.baseUrl}/products/${identifier}/enabled`);
+ }
+
+ incompleteaccountassignments(identifier: string): Observable<AccountAssignment[]> {
+ return this.http.get(`${this.baseUrl}/products/${identifier}/incompleteaccountassignments`);
+ }
+
+ findAllTaskDefinitionsForProduct(identifier: string): Observable<TaskDefinition[]> {
+ return this.http.get(`${this.baseUrl}/products/${identifier}/tasks/`);
+ }
+
+ createTaskDefinition(productIdentifier: string, taskDefinition: TaskDefinition): Observable<void> {
+ return this.http.post(`${this.baseUrl}/products/${productIdentifier}/tasks/`, taskDefinition);
+ }
+
+ getTaskDefinition(productIdentifier: string, taskDefinitionIdentifier: string): Observable<TaskDefinition> {
+ return this.http.get(`${this.baseUrl}/products/${productIdentifier}/tasks/${taskDefinitionIdentifier}`);
+ }
+
+ changeTaskDefinition(productIdentifier: string, taskDefinition: TaskDefinition): Observable<void> {
+ return this.http.put(`${this.baseUrl}/products/${productIdentifier}/tasks/${taskDefinition.identifier}`, taskDefinition);
+ }
+
+ deleteTaskDefinition(productIdentifier: string, taskDefinitionIdentifier: string): Observable<void> {
+ return this.http.delete(`${this.baseUrl}/products/${productIdentifier}/tasks/${taskDefinitionIdentifier}`);
+ }
+
+ findAllChargeDefinitionsForProduct(identifier: string): Observable<ChargeDefinition[]> {
+ return this.http.get(`${this.baseUrl}/products/${identifier}/charges/`);
+ }
+
+ createChargeDefinition(productIdentifier: string, chargeDefinition: ChargeDefinition): Observable<void> {
+ return this.http.post(`${this.baseUrl}/products/${productIdentifier}/charges/`, chargeDefinition);
+ }
+
+ getChargeDefinition(productIdentifier: string, chargeDefinitionIdentifier: string): Observable<ChargeDefinition> {
+ return this.http.get(`${this.baseUrl}/products/${productIdentifier}/charges/${chargeDefinitionIdentifier}`);
+ }
+
+ changeChargeDefinition(productIdentifier: string, chargeDefinition: ChargeDefinition): Observable<void> {
+ return this.http.put(`${this.baseUrl}/products/${productIdentifier}/charges/${chargeDefinition.identifier}`, chargeDefinition);
+ }
+
+ deleteChargeDefinition(productIdentifier: string, chargeDefinitionIdentifier: string): Observable<void> {
+ return this.http.delete(`${this.baseUrl}/products/${productIdentifier}/charges/${chargeDefinitionIdentifier}`);
+ }
+
+ getAllCasesForProduct(productIdentifier: string, fetchRequest?: FetchRequest, includeClosed?: boolean): Observable<FimsCasePage> {
+ const params: URLSearchParams = buildSearchParams(fetchRequest);
+
+ params.append('includeClosed', includeClosed ? 'true' : 'false');
+ params.append('pageIndex', '1');
+ params.append('size', '10');
+
+ const requestOptions: RequestOptionsArgs = {
+ search: params
+ };
+
+ return this.http.get(`${this.baseUrl}/products/${productIdentifier}/cases/`, requestOptions)
+ .map((casePage: CasePage) => mapToFimsCasePage(casePage));
+ }
+
+ createCase(productIdentifier: string, fimsCase: FimsCase): Observable<void> {
+ const caseInstance: Case = mapToCase(fimsCase);
+
+ return this.http.post(`${this.baseUrl}/products/${productIdentifier}/cases/`, caseInstance);
+ }
+
+ getCase(productIdentifier: string, caseIdentifier: string): Observable<FimsCase> {
+ return this.http.get(`${this.baseUrl}/products/${productIdentifier}/cases/${caseIdentifier}`)
+ .map((caseInstance: Case) => mapToFimsCase(caseInstance));
+ }
+
+ changeCase(productIdentifier: string, fimsCase: FimsCase): Observable<void> {
+ const caseInstance: Case = mapToCase(fimsCase);
+ return this.http.put(`${this.baseUrl}/products/${productIdentifier}/cases/${caseInstance.identifier}`, caseInstance);
+ }
+
+ getAllActionsForCase(productIdentifier: string, caseIdentifier: string): Observable<WorkflowAction[]> {
+ return this.http.get(`${this.baseUrl}/products/${productIdentifier}/cases/${caseIdentifier}/actions/`);
+ }
+
+ getCostComponentsForAction(productIdentifier: string, caseIdentifier: string, action: string,
+ touchingAccounts: string[] = [], forPaymentSize?: string, forDateTime?: string): Observable<Payment> {
+
+ const params: URLSearchParams = new URLSearchParams();
+
+ params.append('touchingaccounts', touchingAccounts.join(','));
+ params.append('forpaymentsize', forPaymentSize);
+ params.append('fordatetime', forDateTime);
+
+ return this.http.get(`${this.baseUrl}/products/${productIdentifier}/cases/${caseIdentifier}/actions/${action}/costcomponents`);
+ }
+
+ executeCaseCommand(productIdentifier: string, caseIdentifier: string, action: string, command: CaseCommand): Observable<void> {
+ return this.http.post(`${this.baseUrl}/products/${productIdentifier}/cases/${caseIdentifier}/commands/${action}`, command);
+ }
+
+ findAllTasksForCase(productIdentifier: string, caseIdentifier: string, includeExcluded?: boolean): Observable<TaskInstance[]> {
+ const params: URLSearchParams = new URLSearchParams();
+
+ params.append('includeExecuted', String(includeExcluded));
+
+ const requestOptions: RequestOptionsArgs = {
+ search: params
+ };
+
+ return this.http.get(`${this.baseUrl}/products/${productIdentifier}/cases/${caseIdentifier}/tasks/`, requestOptions);
+ }
+
+ getTaskForCase(productIdentifier: string, caseIdentifier: string, taskIdentifier: string): Observable<TaskInstance> {
+ return this.http.get(`${this.baseUrl}/products/${productIdentifier}/cases/${caseIdentifier}/tasks/${taskIdentifier}`);
+ }
+
+ taskForCaseExecuted(productIdentifier: string, caseIdentifier: string, taskIdentifier: string, executed: boolean): Observable<void> {
+ return this.http.put(`${this.baseUrl}/products/${productIdentifier}/cases/${caseIdentifier}/tasks/${taskIdentifier}/executed`,
+ executed);
+ }
+
+ findAllCases(fetchRequest?: FetchRequest): Observable<FimsCase[]> {
+ const search: URLSearchParams = buildSearchParams(fetchRequest);
+
+ const requestOptions: RequestOptionsArgs = {
+ search
+ };
+
+ return this.http.get(`${this.baseUrl}/cases/`, requestOptions)
+ .map((caseInstances: Case[]) => mapToFimsCases(caseInstances));
+ }
+
+ getPaymentScheduleForCase(productIdentifier: string, caseIdentifier: string,
+ initialDisbursalDate?: string): Observable<PlannedPaymentPage> {
+ const params: URLSearchParams = new URLSearchParams();
+ params.append('initialDisbursalDate', initialDisbursalDate ? new Date(initialDisbursalDate).toISOString() : undefined);
+
+ const requestOptions: RequestOptionsArgs = {
+ search: params
+ };
+
+ return this.http.get(`${this.baseUrl}/individuallending/products/${productIdentifier}/cases/${caseIdentifier}/plannedpayments`,
+ requestOptions);
+ }
+
+ getAllCasesForCustomer(customerIdentifier: string, fetchRequest?: FetchRequest): Observable<FimsCasePage> {
+ const search: URLSearchParams = buildSearchParams(fetchRequest);
+
+ const requestOptions: RequestOptionsArgs = {
+ search
+ };
+
+ return this.http.get(`${this.baseUrl}/individuallending/customers/${customerIdentifier}/cases`, requestOptions)
+ .map((casePage: CasePage) => mapToFimsCasePage(casePage));
+ }
+
+ findAllRanges(productIdentifier: string): Observable<FimsRange[]> {
+ return this.http.get(`${this.baseUrl}/products/${productIdentifier}/balancesegmentsets/`)
+ .map((segments: BalanceSegmentSet[]) => mapToFimsRanges(segments));
+ }
+
+ createRange(productIdentifier: string, range: FimsRange): Observable<void> {
+ const balanceSegmentSet = mapToBalanceSegmentSet(range);
+ return this.http.post(`${this.baseUrl}/products/${productIdentifier}/balancesegmentsets/`, balanceSegmentSet);
+ }
+
+ getRange(productIdentifier: string, rangeIdentifier: string): Observable<FimsRange> {
+ return this.http.get(`${this.baseUrl}/products/${productIdentifier}/balancesegmentsets/${rangeIdentifier}`)
+ .map(segments => mapToFimsRange(segments));
+ }
+
+ changeRange(productIdentifier: string, range: FimsRange): Observable<void> {
+ const balanceSegmentSet = mapToBalanceSegmentSet(range);
+ return this.http.put(`${this.baseUrl}/products/${productIdentifier}/balancesegmentsets/${balanceSegmentSet.identifier}`,
+ balanceSegmentSet);
+ }
+
+ deleteRange(productIdentifier: string, rangeIdentifier: string): Observable<void> {
+ return this.http.delete(`${this.baseUrl}/products/${productIdentifier}/balancesegmentsets/${rangeIdentifier}`);
+ }
+
+ changeLossProvisionConfiguration(productIdentifier: string, lossProvisionConfiguration: LossProvisionConfiguration): Observable<void> {
+ return this.http.put(
+ `${this.baseUrl}/individuallending/products/${productIdentifier}/lossprovisionconfiguration`, lossProvisionConfiguration
+ );
+ }
+
+ getLossProvisionConfiguration(productIdentifier: string): Observable<LossProvisionConfiguration> {
+ return this.http.get(`${this.baseUrl}/individuallending/products/${productIdentifier}/lossprovisionconfiguration`);
+ }
+
+ getCaseDocuments(productIdentifier: string, caseIdentifier: string): Observable<CaseCustomerDocuments> {
+ return this.http.get(`${this.baseUrl}/individuallending/products/${productIdentifier}/cases/${caseIdentifier}/documents`);
+ }
+
+ changeCaseDocuments(productIdentifier: string, caseIdentifier: string, documents: CaseCustomerDocuments): Observable<void> {
+ return this.http.put(`${this.baseUrl}/individuallending/products/${productIdentifier}/cases/${caseIdentifier}/documents`, documents);
+ }
+
+}
diff --git a/src/app/services/reporting/domain/auto-complete-resource.model.ts b/src/app/services/reporting/domain/auto-complete-resource.model.ts
new file mode 100644
index 0000000..220cd10
--- /dev/null
+++ b/src/app/services/reporting/domain/auto-complete-resource.model.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface AutoCompleteResource {
+ path: string;
+ terms: string[];
+}
diff --git a/src/app/services/reporting/domain/displayable-field.model.ts b/src/app/services/reporting/domain/displayable-field.model.ts
new file mode 100644
index 0000000..d00d334
--- /dev/null
+++ b/src/app/services/reporting/domain/displayable-field.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Type} from './type.model';
+
+export interface DisplayableField {
+ name: string;
+ type: Type;
+ mandatory: boolean;
+}
diff --git a/src/app/services/reporting/domain/footer.model.ts b/src/app/services/reporting/domain/footer.model.ts
new file mode 100644
index 0000000..0806327
--- /dev/null
+++ b/src/app/services/reporting/domain/footer.model.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Value} from './value.model';
+
+export interface Footer {
+ values: Value[];
+}
diff --git a/src/app/services/reporting/domain/header.model.ts b/src/app/services/reporting/domain/header.model.ts
new file mode 100644
index 0000000..b75fff4
--- /dev/null
+++ b/src/app/services/reporting/domain/header.model.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export interface Header {
+ columnNames: string[];
+}
diff --git a/src/app/services/reporting/domain/permittable-group-ids.ts b/src/app/services/reporting/domain/permittable-group-ids.ts
new file mode 100644
index 0000000..69ad249
--- /dev/null
+++ b/src/app/services/reporting/domain/permittable-group-ids.ts
@@ -0,0 +1,21 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export class ReportingPermittableGroupIds {
+ public static readonly REPORT_MANAGEMENT = 'reporting__v1__general';
+}
diff --git a/src/app/services/reporting/domain/query-parameter.model.ts b/src/app/services/reporting/domain/query-parameter.model.ts
new file mode 100644
index 0000000..9e48481
--- /dev/null
+++ b/src/app/services/reporting/domain/query-parameter.model.ts
@@ -0,0 +1,31 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Type} from './type.model';
+import {AutoCompleteResource} from './auto-complete-resource.model';
+
+export type Operator = 'EQUALS' | 'IN' | 'LIKE' | 'BETWEEN' | 'GREATER' | 'LESSER';
+
+export interface QueryParameter {
+ name: string;
+ type: Type;
+ operator: Operator;
+ value?: string;
+ mandatory: boolean;
+ autoCompleteResource?: AutoCompleteResource;
+}
diff --git a/src/app/services/reporting/domain/report-definition.model.ts b/src/app/services/reporting/domain/report-definition.model.ts
new file mode 100644
index 0000000..1a48729
--- /dev/null
+++ b/src/app/services/reporting/domain/report-definition.model.ts
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {QueryParameter} from './query-parameter.model';
+import {DisplayableField} from './displayable-field.model';
+
+export interface ReportDefinition {
+ identifier: string;
+ name: string;
+ description: string;
+ queryParameters: QueryParameter[];
+ displayableFields: DisplayableField[];
+}
diff --git a/src/app/services/reporting/domain/report-page.model.ts b/src/app/services/reporting/domain/report-page.model.ts
new file mode 100644
index 0000000..d05b698
--- /dev/null
+++ b/src/app/services/reporting/domain/report-page.model.ts
@@ -0,0 +1,32 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Header} from './header.model';
+import {Row} from './row.model';
+import {Footer} from './footer.model';
+
+export interface ReportPage {
+ name: string;
+ description: string;
+ generatedOn: string;
+ generatedBy: string;
+ header: Header;
+ rows: Row[];
+ footer: Footer;
+ hasMore: boolean;
+}
diff --git a/src/app/services/reporting/domain/report-request.model.ts b/src/app/services/reporting/domain/report-request.model.ts
new file mode 100644
index 0000000..04d7aad
--- /dev/null
+++ b/src/app/services/reporting/domain/report-request.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {QueryParameter} from './query-parameter.model';
+import {DisplayableField} from './displayable-field.model';
+
+export interface ReportRequest {
+ queryParameters: QueryParameter[];
+ displayableFields: DisplayableField[];
+}
diff --git a/src/app/services/reporting/domain/row.model.ts b/src/app/services/reporting/domain/row.model.ts
new file mode 100644
index 0000000..6ccbea0
--- /dev/null
+++ b/src/app/services/reporting/domain/row.model.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Value} from './value.model';
+
+export interface Row {
+ values: Value[];
+}
diff --git a/src/app/services/reporting/domain/type.model.ts b/src/app/services/reporting/domain/type.model.ts
new file mode 100644
index 0000000..7a86ea0
--- /dev/null
+++ b/src/app/services/reporting/domain/type.model.ts
@@ -0,0 +1,19 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export type Type = 'TEXT' | 'NUMBER' | 'DATE';
diff --git a/src/app/services/reporting/domain/value.model.ts b/src/app/services/reporting/domain/value.model.ts
new file mode 100644
index 0000000..ec2a625
--- /dev/null
+++ b/src/app/services/reporting/domain/value.model.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Type} from './type.model';
+
+export interface Value {
+ values: string[];
+ type: Type;
+}
diff --git a/src/app/services/reporting/reporting.service.ts b/src/app/services/reporting/reporting.service.ts
new file mode 100644
index 0000000..40a12d8
--- /dev/null
+++ b/src/app/services/reporting/reporting.service.ts
@@ -0,0 +1,57 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Inject, Injectable} from '@angular/core';
+import {HttpClient} from '../http/http.service';
+import {Observable} from 'rxjs/Observable';
+import {ReportDefinition} from './domain/report-definition.model';
+import {FetchRequest} from '../domain/paging/fetch-request.model';
+import {ReportPage} from './domain/report-page.model';
+import {ReportRequest} from './domain/report-request.model';
+import {RequestOptionsArgs, URLSearchParams} from '@angular/http';
+import {buildSearchParams} from '../domain/paging/search-param.builder';
+
+@Injectable()
+export class ReportingService {
+
+ constructor(private http: HttpClient, @Inject('reportingBaseUrl') private baseUrl: string) {
+ }
+
+ fetchCategories(): Observable<string[]> {
+ return this.http.get(`${this.baseUrl}/categories`);
+ }
+
+ fetchReportDefinitions(category: string): Observable<ReportDefinition[]> {
+ return this.http.get(`${this.baseUrl}/categories/${category}`);
+ }
+
+ findReportDefinition(category: string, identifier: string): Observable<ReportDefinition> {
+ return this.http.get(`${this.baseUrl}/categories/${category}/definitions/${identifier}`);
+ }
+
+ generateReport(category: string, identifier: string, reportRequest: ReportRequest, fetchRequest?: FetchRequest): Observable<ReportPage> {
+ const params: URLSearchParams = buildSearchParams(fetchRequest);
+
+ const requestOptions: RequestOptionsArgs = {
+ search: params
+ };
+
+ return this.http.post(`${this.baseUrl}/categories/${category}/reports/${identifier}`, reportRequest, requestOptions);
+ }
+
+}
diff --git a/src/app/services/security/authn/auth-guard.service.spec.ts b/src/app/services/security/authn/auth-guard.service.spec.ts
new file mode 100644
index 0000000..5d70011
--- /dev/null
+++ b/src/app/services/security/authn/auth-guard.service.spec.ts
@@ -0,0 +1,113 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {AuthGuard} from './auth-guard.service';
+import {ActivatedRouteSnapshot, Router, RouterStateSnapshot} from '@angular/router';
+import {inject, TestBed} from '@angular/core/testing';
+import {Observable} from 'rxjs/Observable';
+import {Store} from '@ngrx/store';
+import * as fromRoot from '../../../store';
+
+describe('Test Auth Guard Service', () => {
+
+ const route: ActivatedRouteSnapshot = undefined;
+
+ const state: RouterStateSnapshot = undefined;
+
+ const mockRouter = {
+ navigate() {
+ }
+ };
+
+ describe('when logged in', () => {
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ AuthGuard,
+ {provide: Router, useValue: mockRouter},
+ {
+ provide: Store, useClass: class {
+ select = jasmine.createSpy('select').and.callFake(selector => {
+ if (selector === fromRoot.getAuthenticationLoading) {
+ return Observable.of(false);
+ }
+ if (selector === fromRoot.getAuthentication) {
+ return Observable.of({});
+ }
+ });
+ }
+ }
+ ]
+ });
+ });
+
+ it('should test if route is active', (done: DoneFn) => {
+ inject([AuthGuard], (authGuard: AuthGuard) => {
+ authGuard.canActivate(route, state).subscribe(canActivate => {
+ expect(canActivate).toBeTruthy();
+ done();
+ });
+ })();
+ });
+ });
+
+ describe('when not logged in', () => {
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ AuthGuard,
+ {provide: Router, useValue: mockRouter},
+ {
+ provide: Store, useClass: class {
+ select = jasmine.createSpy('select').and.callFake(selector => {
+ if (selector === fromRoot.getAuthenticationLoading) {
+ return Observable.of(false);
+ }
+ if (selector === fromRoot.getAuthentication) {
+ return Observable.of(null);
+ }
+ });
+ }
+ }
+ ]
+ });
+ });
+
+ it('should test if route gets deactivated when user is not logged in', (done: DoneFn) => {
+ inject([AuthGuard], (authGuard: AuthGuard) => {
+ authGuard.canActivate(route, state).subscribe(canActivate => {
+ expect(canActivate).toBeFalsy();
+ done();
+ });
+ })();
+ });
+
+ it('should test if guard redirects to login page when user is not logged in', (done: DoneFn) => {
+ inject([AuthGuard, Router], (authGuard: AuthGuard, router: Router) => {
+ spyOn(router, 'navigate');
+ authGuard.canActivate(route, state).subscribe(canActivate => {
+ expect(router.navigate).toHaveBeenCalledWith(['/login']);
+ done();
+ });
+ })();
+ });
+ });
+
+});
diff --git a/src/app/services/security/authn/auth-guard.service.ts b/src/app/services/security/authn/auth-guard.service.ts
new file mode 100644
index 0000000..458cef4
--- /dev/null
+++ b/src/app/services/security/authn/auth-guard.service.ts
@@ -0,0 +1,50 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router';
+import {Observable} from 'rxjs/Observable';
+import * as fromRoot from '../../../store';
+import {Store} from '@ngrx/store';
+
+@Injectable()
+export class AuthGuard implements CanActivate {
+
+ constructor(private store: Store<fromRoot.State>, private router: Router) {
+ }
+
+ waitForAuthentication(): Observable<boolean> {
+ return this.store.select(fromRoot.getAuthenticationLoading)
+ .filter(loading => !loading)
+ .take(1);
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.waitForAuthentication()
+ .switchMap(() => this.store.select(fromRoot.getAuthentication)
+ .map(authentication => {
+ if (!authentication) {
+ this.router.navigate(['/login']);
+ return false;
+ }
+ return true;
+ })
+ );
+
+ }
+}
diff --git a/src/app/services/security/authn/authentication.service.spec.ts b/src/app/services/security/authn/authentication.service.spec.ts
new file mode 100644
index 0000000..1d4e7fe
--- /dev/null
+++ b/src/app/services/security/authn/authentication.service.spec.ts
@@ -0,0 +1,62 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {AuthenticationService} from './authentication.service';
+import {BaseRequestOptions, Http, Response, ResponseOptions} from '@angular/http';
+import {MockBackend, MockConnection} from '@angular/http/testing';
+import {Authentication} from '../../identity/domain/authentication.model';
+
+describe('Test Authentication Service', () => {
+
+ let authService: AuthenticationService;
+
+ const tenant = 'Reynholm Industries';
+
+ const mockAuthentication: Authentication = {
+ tokenType: 'iDontCare',
+ accessToken: 'accessToken',
+ accessTokenExpiration: new Date().toISOString(),
+ refreshTokenExpiration: new Date().toISOString(),
+ passwordExpiration: new Date().toISOString()
+ };
+
+ beforeEach(() => {
+ const mockBackend: MockBackend = new MockBackend();
+
+ mockBackend.connections.subscribe((connection: MockConnection) =>
+ connection.mockRespond(new Response(new ResponseOptions({body: mockAuthentication})))
+ );
+ const requestOptions: BaseRequestOptions = new BaseRequestOptions();
+ const http: Http = new Http(mockBackend, requestOptions);
+
+ authService = new AuthenticationService('/identity', http);
+ });
+
+ it('should login and return authentication', (done: DoneFn) => {
+ authService.login(tenant, 'moss', 'test').subscribe((authentication: Authentication) => {
+ expect(authentication.tokenType).toBe(mockAuthentication.tokenType);
+ expect(authentication.accessToken).toBe(mockAuthentication.accessToken);
+ expect(authentication.accessTokenExpiration).toBe(mockAuthentication.accessTokenExpiration);
+ expect(authentication.refreshTokenExpiration).toBe(mockAuthentication.refreshTokenExpiration);
+ expect(authentication.passwordExpiration).toBe(mockAuthentication.passwordExpiration);
+
+ done();
+ });
+ });
+
+});
diff --git a/src/app/services/security/authn/authentication.service.ts b/src/app/services/security/authn/authentication.service.ts
new file mode 100644
index 0000000..3295174
--- /dev/null
+++ b/src/app/services/security/authn/authentication.service.ts
@@ -0,0 +1,89 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Inject, Injectable} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {Headers, Http, RequestOptionsArgs, Response} from '@angular/http';
+import {Error} from '../../domain/error.model';
+import {Authentication} from '../../identity/domain/authentication.model';
+import {Permission} from '../../identity/domain/permission.model';
+
+@Injectable()
+export class AuthenticationService {
+
+ private static encodePassword(password: string): string {
+ return btoa(password);
+ }
+
+ constructor(@Inject('identityBaseUrl') private identityBaseUrl: string, private http: Http) {
+ }
+
+ login(tenantId: string, userId: string, password: string): Observable<Authentication> {
+ const encodedPassword: string = AuthenticationService.encodePassword(password);
+ const loginUrl = '/token?grant_type=password&username=';
+ return this.http.post(this.identityBaseUrl + loginUrl + userId + '&password=' + encodedPassword, {}, this.tenantHeader(tenantId))
+ .map((response: Response) => this.mapResponse(response))
+ .catch(Error.handleError);
+ }
+
+ logout(tenantId: string, userId: string, accessToken: string): Observable<Response> {
+ return this.http.delete(this.identityBaseUrl + '/token/_current', this.authorizationHeader(tenantId, userId, accessToken))
+ .map((response: Response) => this.mapResponse(response))
+ .catch(Error.handleError);
+ }
+
+ getUserPermissions(tenantId: string, userId: string, accessToken: string): Observable<Permission[]> {
+ return this.http.get(this.identityBaseUrl + '/users/' + userId + '/permissions',
+ this.authorizationHeader(tenantId, userId, accessToken)
+ )
+ .map((response: Response) => this.mapResponse(response))
+ .catch(Error.handleError);
+ }
+
+ refreshAccessToken(tenantId: string): Observable<Authentication> {
+ const refreshTokenUrl = '/token?grant_type=refresh_token';
+ return this.http.post(this.identityBaseUrl + refreshTokenUrl, {}, this.tenantHeader(tenantId))
+ .map((response: Response): Authentication => this.mapResponse(response))
+ .catch(Error.handleError);
+ }
+
+ private mapResponse(response: Response): any {
+ if (response.text()) {
+ return response.json();
+ }
+ }
+
+ private authorizationHeader(tenantId: string, userId: string, accessToken: string): RequestOptionsArgs {
+ const requestOptions: RequestOptionsArgs = this.tenantHeader(tenantId);
+
+ requestOptions.headers.set('User', userId);
+ requestOptions.headers.set('Authorization', accessToken);
+
+ return requestOptions;
+ }
+
+ private tenantHeader(tenantId: string): RequestOptionsArgs {
+ const headers: Headers = new Headers();
+ headers.set('X-Tenant-Identifier', tenantId);
+
+ return {
+ headers: headers
+ };
+ }
+
+}
diff --git a/src/app/services/security/authz/fims-permission-descriptor.ts b/src/app/services/security/authz/fims-permission-descriptor.ts
new file mode 100644
index 0000000..d897416
--- /dev/null
+++ b/src/app/services/security/authz/fims-permission-descriptor.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {PermissionId} from './permission-id.type';
+
+export interface FimsPermissionDescriptor {
+ id: PermissionId;
+ label: string;
+ description?: string;
+ readOnly?: boolean;
+}
diff --git a/src/app/services/security/authz/fims-permission.model.ts b/src/app/services/security/authz/fims-permission.model.ts
new file mode 100644
index 0000000..da650a9
--- /dev/null
+++ b/src/app/services/security/authz/fims-permission.model.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {PermissionId} from './permission-id.type';
+
+export interface FimsPermission {
+ id: PermissionId;
+ accessLevel: AccessLevel;
+}
+
+export type AccessLevel = 'READ' | 'CHANGE' | 'DELETE';
diff --git a/src/app/services/security/authz/permission-id.type.ts b/src/app/services/security/authz/permission-id.type.ts
new file mode 100644
index 0000000..efbe082
--- /dev/null
+++ b/src/app/services/security/authz/permission-id.type.ts
@@ -0,0 +1,32 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+/**
+ * List of supported permission ids for fims
+ */
+export type PermissionId = 'identity_self' | 'identity_identities' | 'identity_roles' |
+ 'office_self' | 'office_offices' | 'office_employees' |
+ 'customer_customers' | 'customer_tasks' | 'catalog_catalogs' | 'customer_identifications' | 'customer_portrait' | 'customer_documents' |
+ 'accounting_accounts' | 'accounting_ledgers' | 'accounting_journals' | 'accounting_tx_types' | 'accounting_income_statement' |
+ 'accounting_fin_condition' |
+ 'portfolio_product_operations' | 'portfolio_loss_provision' | 'portfolio_products' | 'portfolio_cases' | 'portfolio_documents' |
+ 'deposit_definitions' | 'deposit_instances' |
+ 'teller_management' | 'teller_operations' |
+ 'reporting_management' |
+ 'cheque_management' | 'cheque_transaction' |
+ 'payroll_configuration' | 'payroll_distribution';
diff --git a/src/app/services/security/authz/permission.directive.spec.ts b/src/app/services/security/authz/permission.directive.spec.ts
new file mode 100644
index 0000000..ca528ad
--- /dev/null
+++ b/src/app/services/security/authz/permission.directive.spec.ts
@@ -0,0 +1,77 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component} from '@angular/core';
+import {TestBed} from '@angular/core/testing';
+import {By} from '@angular/platform-browser';
+import {PermissionDirective} from './permission.directive';
+import {Store} from '@ngrx/store';
+import {Observable} from 'rxjs/Observable';
+import {FimsPermission} from './fims-permission.model';
+
+describe('Test permission directive', () => {
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ {
+ provide: Store, useClass: class {
+ select = function () {
+ };
+ }
+ }
+ ],
+ declarations: [PermissionDirective, TestComponent]
+ });
+ });
+
+ describe('Test permission directive with object parameter', () => {
+ it('should add item to dom', () => {
+ const store = TestBed.get(Store);
+
+ spyOn(store, 'select').and.returnValue(Observable.of<FimsPermission[]>([
+ {id: 'office_offices', accessLevel: 'READ'}
+ ]));
+
+ const fixture = TestBed.createComponent(TestComponent);
+ fixture.detectChanges();
+
+ const element = fixture.debugElement.query(By.css('button'));
+ expect(element).not.toBeNull('Button should be existent within the dom');
+ });
+
+ it('should remove item from dom', () => {
+ const store = TestBed.get(Store);
+ spyOn(store, 'select').and.returnValue(Observable.of([]));
+
+ const fixture = TestBed.createComponent(TestComponent);
+ fixture.detectChanges();
+ const element = fixture.debugElement.query(By.css('button'));
+ expect(element).toBeNull('Button should be not existent within the dom');
+ });
+ });
+
+});
+
+@Component({
+ template: `
+ <button *hasPermission="{ id: 'office_offices', accessLevel: 'READ' }">randomTestValue</button>
+ `
+})
+class TestComponent {
+}
diff --git a/src/app/services/security/authz/permission.directive.ts b/src/app/services/security/authz/permission.directive.ts
new file mode 100644
index 0000000..72ad743
--- /dev/null
+++ b/src/app/services/security/authz/permission.directive.ts
@@ -0,0 +1,64 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Directive, Input, OnDestroy, OnInit, TemplateRef, ViewContainerRef} from '@angular/core';
+import {FimsPermission} from './fims-permission.model';
+import {Store} from '@ngrx/store';
+import * as fromRoot from '../../../store';
+import {Subscription} from 'rxjs/Subscription';
+
+@Directive({
+ // tslint:disable-next-line:directive-selector
+ selector: '[hasPermission]'
+})
+export class PermissionDirective implements OnInit, OnDestroy {
+
+ private permissionSubscription: Subscription;
+
+ @Input('hasPermission') hasPermission: FimsPermission;
+
+ constructor(private store: Store<fromRoot.State>, private viewContainer: ViewContainerRef, private template: TemplateRef<Object>) {
+ }
+
+ ngOnInit(): void {
+ this.viewContainer.clear();
+
+ if (!this.hasPermission) {
+ this.viewContainer.createEmbeddedView(this.template);
+ return;
+ }
+
+ this.permissionSubscription = this.store.select(fromRoot.getPermissions)
+ .map(permissions => permissions.filter(permission => permission.id === this.hasPermission.id
+ && permission.accessLevel === this.hasPermission.accessLevel
+ ))
+ .map(matches => matches.length > 0)
+ .subscribe(hasPermission => {
+ this.viewContainer.clear();
+ if (hasPermission) {
+ this.viewContainer.createEmbeddedView(this.template);
+ }
+ });
+ }
+
+ ngOnDestroy(): void {
+ if (this.permissionSubscription) {
+ this.permissionSubscription.unsubscribe();
+ }
+ }
+}
diff --git a/src/app/services/security/authz/permission.guard.ts b/src/app/services/security/authz/permission.guard.ts
new file mode 100644
index 0000000..1b9565c
--- /dev/null
+++ b/src/app/services/security/authz/permission.guard.ts
@@ -0,0 +1,67 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {ActivatedRouteSnapshot, CanActivateChild, Router, RouterStateSnapshot} from '@angular/router';
+import {Observable} from 'rxjs/Observable';
+import {FimsPermission} from './fims-permission.model';
+import {Store} from '@ngrx/store';
+import * as fromRoot from '../../../store';
+
+@Injectable()
+export class PermissionGuard implements CanActivateChild {
+
+ constructor(private store: Store<fromRoot.State>, private router: Router) {
+ }
+
+ waitForPermissions(): Observable<boolean> {
+ return this.store.select(fromRoot.getPermissionsLoading)
+ .filter(loading => !loading)
+ .take(1);
+ }
+
+ canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ const routeData: any = route.data;
+
+ const routePermission: FimsPermission = routeData.hasPermission;
+
+ // No permission set on route at all
+ if (!routePermission) {
+ return Observable.of(true);
+ }
+
+ return this.waitForPermissions()
+ .switchMap(() => this.hasPermission(routePermission)
+ .map(hasPermission => {
+ if (hasPermission) {
+ return true;
+ }
+ this.router.navigate(['/denied']);
+ return false;
+ }));
+
+ }
+
+ private hasPermission(routePermission: FimsPermission): Observable<boolean> {
+ return this.store.select(fromRoot.getPermissions)
+ .map(permissions => permissions.filter(permission => permission.id === routePermission.id
+ && permission.accessLevel === routePermission.accessLevel))
+ .map(matches => matches.length > 0)
+ .take(1);
+ }
+}
diff --git a/src/app/services/security/authz/permittable-group-id-mapper.ts b/src/app/services/security/authz/permittable-group-id-mapper.ts
new file mode 100644
index 0000000..c81b30e
--- /dev/null
+++ b/src/app/services/security/authz/permittable-group-id-mapper.ts
@@ -0,0 +1,139 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {FimsPermissionDescriptor} from './fims-permission-descriptor';
+import {IdentityPermittableGroupIds} from '../../identity/domain/permittable-group-ids.model';
+import {OfficePermittableGroupIds} from '../../office/domain/permittable-group-ids.model';
+import {CustomerPermittableGroupIds} from '../../customer/domain/permittable-group-ids';
+import {AccountingPermittableGroupIds} from '../../accounting/domain/permittable-group-ids';
+import {PortfolioPermittableGroupIds} from '../../portfolio/domain/permittable-group-ids';
+import {PermissionId} from './permission-id.type';
+import {Injectable} from '@angular/core';
+import {DepositAccountPermittableGroupIds} from '../../depositAccount/domain/permittable-group-ids';
+import {TellerPermittableGroupIds} from '../../teller/domain/permittable-group-ids';
+import {ReportingPermittableGroupIds} from '../../reporting/domain/permittable-group-ids';
+import {ChequePermittableGroupIds} from '../../cheque/domain/permittable-group-ids';
+import {PayrollPermittableGroupIds} from '../../payroll/domain/permittable-group-ids';
+
+interface PermittableGroupMap {
+ [s: string]: FimsPermissionDescriptor;
+}
+
+/**
+ * Maps permittable group ids to internal keys
+ */
+@Injectable()
+export class PermittableGroupIdMapper {
+
+ private _permittableGroupMap: PermittableGroupMap = {};
+
+ constructor() {
+ this._permittableGroupMap[OfficePermittableGroupIds.EMPLOYEE_MANAGEMENT] = {id: 'office_employees', label: 'Employees'};
+ this._permittableGroupMap[OfficePermittableGroupIds.OFFICE_MANAGEMENT] = {id: 'office_offices', label: 'Offices'};
+ this._permittableGroupMap[OfficePermittableGroupIds.SELF_MANAGEMENT] = {
+ id: 'office_self',
+ label: 'User created resources(Offices & Employees)'
+ };
+
+ this._permittableGroupMap[IdentityPermittableGroupIds.IDENTITY_MANAGEMENT] = {id: 'identity_identities', label: 'Identities'};
+ this._permittableGroupMap[IdentityPermittableGroupIds.ROLE_MANAGEMENT] = {id: 'identity_roles', label: 'Roles'};
+ this._permittableGroupMap[IdentityPermittableGroupIds.SELF_MANAGEMENT] = {
+ id: 'identity_self',
+ label: 'User created resources(Identity & Roles)'
+ };
+
+ this._permittableGroupMap[CustomerPermittableGroupIds.CUSTOMER_MANAGEMENT] = {id: 'customer_customers', label: 'Members'};
+ this._permittableGroupMap[CustomerPermittableGroupIds.TASK_MANAGEMENT] = {id: 'customer_tasks', label: 'Tasks'};
+ this._permittableGroupMap[CustomerPermittableGroupIds.CATALOG_MANAGEMENT] = {id: 'catalog_catalogs', label: 'Custom fields'};
+ this._permittableGroupMap[CustomerPermittableGroupIds.IDENTITY_CARD_MANAGEMENT] = {
+ id: 'customer_identifications',
+ label: 'Member identification cards'
+ };
+ this._permittableGroupMap[CustomerPermittableGroupIds.PORTRAIT_MANAGEMENT] = {id: 'customer_portrait', label: 'Member portrait'};
+ this._permittableGroupMap[CustomerPermittableGroupIds.CUSTOMER_DOCUMENT] = {id: 'customer_documents', label: 'Member documents'};
+
+ this._permittableGroupMap[AccountingPermittableGroupIds.ACCOUNT_MANAGEMENT] = {id: 'accounting_accounts', label: 'Accounts'};
+ this._permittableGroupMap[AccountingPermittableGroupIds.JOURNAL_MANAGEMENT] = {id: 'accounting_journals', label: 'Journal'};
+ this._permittableGroupMap[AccountingPermittableGroupIds.LEDGER_MANAGEMENT] = {id: 'accounting_ledgers', label: 'Ledger'};
+ this._permittableGroupMap[AccountingPermittableGroupIds.TRANSACTION_TYPES] = {id: 'accounting_tx_types', label: 'Transaction types'};
+ this._permittableGroupMap[AccountingPermittableGroupIds.THOTH_INCOME_STMT] = {
+ id: 'accounting_income_statement',
+ label: 'Income statement'
+ };
+ this._permittableGroupMap[AccountingPermittableGroupIds.THOTH_FIN_CONDITION] = {
+ id: 'accounting_fin_condition',
+ label: 'Financial condition'
+ };
+
+ this._permittableGroupMap[PortfolioPermittableGroupIds.PRODUCT_OPERATIONS_MANAGEMENT] = {
+ id: 'portfolio_product_operations',
+ label: 'Loan product operations'
+ };
+ this._permittableGroupMap[PortfolioPermittableGroupIds.PRODUCT_LOSS_PROVISIONING_MANAGEMENT] = {
+ id: 'portfolio_loss_provision', label: 'Loan loss provision'
+ };
+ this._permittableGroupMap[PortfolioPermittableGroupIds.PRODUCT_MANAGEMENT] = {id: 'portfolio_products', label: 'Loan products'};
+ this._permittableGroupMap[PortfolioPermittableGroupIds.CASE_MANAGEMENT] = {id: 'portfolio_cases', label: 'Member loans'};
+ this._permittableGroupMap[PortfolioPermittableGroupIds.CASE_DOCUMENT_MANAGEMENT] = {
+ id: 'portfolio_documents',
+ label: 'Member loan documents'
+ };
+
+ this._permittableGroupMap[DepositAccountPermittableGroupIds.DEFINITION_MANAGEMENT] = {
+ id: 'deposit_definitions',
+ label: 'Deposit account management'
+ };
+ this._permittableGroupMap[DepositAccountPermittableGroupIds.INSTANCE_MANAGEMENT] = {
+ id: 'deposit_instances',
+ label: 'Deposit account for members'
+ };
+
+ this._permittableGroupMap[TellerPermittableGroupIds.TELLER_MANAGEMENT] = {id: 'teller_management', label: 'Teller management'};
+ this._permittableGroupMap[TellerPermittableGroupIds.TELLER_OPERATION] = {id: 'teller_operations', label: 'Teller operations'};
+
+ this._permittableGroupMap[ReportingPermittableGroupIds.REPORT_MANAGEMENT] = {id: 'reporting_management', label: 'Report management'};
+
+ this._permittableGroupMap[ChequePermittableGroupIds.CHEQUE_TRANSACTION] = {id: 'cheque_transaction', label: 'Cheque transaction'};
+ this._permittableGroupMap[ChequePermittableGroupIds.CHEQUE_MANAGEMENT] = {id: 'cheque_management', label: 'Cheque management'};
+
+ this._permittableGroupMap[PayrollPermittableGroupIds.CONFIGURATION] = {id: 'payroll_configuration', label: 'Payroll configuration'};
+ this._permittableGroupMap[PayrollPermittableGroupIds.DISTRIBUTION] = {id: 'payroll_distribution', label: 'Payroll distribution'};
+ }
+
+ public map(permittableGroupId: string): FimsPermissionDescriptor {
+ const descriptor: FimsPermissionDescriptor = this._permittableGroupMap[permittableGroupId];
+ if (!descriptor) {
+ console.warn(`Could not find permission descriptor for permittable group id '${permittableGroupId}'`);
+ }
+ return descriptor;
+ }
+
+ public isValid(id: PermissionId): boolean {
+ for (const key in this._permittableGroupMap) {
+ if (this._permittableGroupMap.hasOwnProperty(key)) {
+ const descriptor: FimsPermissionDescriptor = this._permittableGroupMap[key];
+ if (descriptor.id === id) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/src/app/services/security/change.password.service.spec.ts b/src/app/services/security/change.password.service.spec.ts
new file mode 100644
index 0000000..897bec3
--- /dev/null
+++ b/src/app/services/security/change.password.service.spec.ts
@@ -0,0 +1,94 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ActivatedRouteSnapshot, Router, RouterStateSnapshot} from '@angular/router';
+import {TestBed} from '@angular/core/testing';
+import {Observable} from 'rxjs/Observable';
+import {Store} from '@ngrx/store';
+import {ChangePasswordGuard} from './change.password.service';
+import {Authentication} from '../identity/domain/authentication.model';
+
+describe('Test Password Change Service', () => {
+
+ const route: ActivatedRouteSnapshot = undefined;
+
+ const state: RouterStateSnapshot = undefined;
+
+ const router = {
+ navigate() {
+ }
+ };
+
+ describe('when logged in', () => {
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ ChangePasswordGuard,
+ {provide: Router, useValue: router},
+ {
+ provide: Store, useClass: class {
+ select = jasmine.createSpy('select').and.callFake(selector => Observable.of({}));
+ }
+ }
+ ]
+ });
+ });
+
+ function setup(authentication: Authentication): ChangePasswordGuard {
+ const store = TestBed.get(Store);
+
+ store.select.and.returnValue(Observable.of({
+ authentication
+ }));
+
+ return TestBed.get(ChangePasswordGuard);
+ }
+
+ it('should test if route is not active when password expiration is in the past', (done: DoneFn) => {
+ const changePasswordGuard = setup({
+ passwordExpiration: '2016-01-01',
+ accessToken: '',
+ accessTokenExpiration: '',
+ tokenType: '',
+ refreshTokenExpiration: ''
+ });
+
+ changePasswordGuard.canActivateChild(route, state).subscribe(canActivateChild => {
+ expect(canActivateChild).toBeFalsy();
+ done();
+ });
+ });
+
+ it('should test if route is active when no password expiration is set', (done: DoneFn) => {
+ const changePasswordGuard = setup({
+ passwordExpiration: null,
+ accessToken: '',
+ accessTokenExpiration: '',
+ tokenType: '',
+ refreshTokenExpiration: ''
+ });
+
+ changePasswordGuard.canActivateChild(route, state).subscribe(canActivateChild => {
+ expect(canActivateChild).toBeTruthy();
+ done();
+ });
+ });
+ });
+
+});
diff --git a/src/app/services/security/change.password.service.ts b/src/app/services/security/change.password.service.ts
new file mode 100644
index 0000000..1a844ff
--- /dev/null
+++ b/src/app/services/security/change.password.service.ts
@@ -0,0 +1,49 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {ActivatedRouteSnapshot, CanActivateChild, Router, RouterStateSnapshot} from '@angular/router';
+import {Observable} from 'rxjs/Observable';
+import * as fromRoot from '../../store';
+import {Store} from '@ngrx/store';
+
+@Injectable()
+export class ChangePasswordGuard implements CanActivateChild {
+
+ constructor(private store: Store<fromRoot.State>, private router: Router) {
+ }
+
+ canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.isPasswordChangeNeeded()
+ .switchMap(passwordChangeNeeded => {
+ if (passwordChangeNeeded) {
+ this.router.navigate(['/changePassword'], {queryParams: {forced: true}});
+ return Observable.of(false);
+ }
+
+ return Observable.of(true);
+ });
+
+ }
+
+ private isPasswordChangeNeeded(): Observable<boolean> {
+ return this.store.select(fromRoot.getAuthenticationState)
+ .map(state => state.authentication.passwordExpiration ? new Date(state.authentication.passwordExpiration) : undefined)
+ .map(expiryDate => expiryDate ? expiryDate.getTime() < new Date().getTime() : false);
+ }
+}
diff --git a/src/app/services/teller/domain/charge.model.ts b/src/app/services/teller/domain/charge.model.ts
new file mode 100644
index 0000000..f578570
--- /dev/null
+++ b/src/app/services/teller/domain/charge.model.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export interface Charge {
+ code: string;
+ name: string;
+ amount: number;
+}
diff --git a/src/app/services/teller/domain/cheque.model.ts b/src/app/services/teller/domain/cheque.model.ts
new file mode 100644
index 0000000..e65e19c
--- /dev/null
+++ b/src/app/services/teller/domain/cheque.model.ts
@@ -0,0 +1,29 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {MICR} from './micr.model';
+
+export interface Cheque {
+ micr: MICR;
+ drawee: string;
+ drawer: string;
+ payee: string;
+ amount: number;
+ dateIssued: string;
+ openCheque?: boolean;
+}
diff --git a/src/app/services/teller/domain/micr.model.ts b/src/app/services/teller/domain/micr.model.ts
new file mode 100644
index 0000000..288d282
--- /dev/null
+++ b/src/app/services/teller/domain/micr.model.ts
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface MICR {
+ chequeNumber: string;
+ branchSortCode: string;
+ accountNumber: string;
+}
diff --git a/src/app/services/teller/domain/permittable-group-ids.ts b/src/app/services/teller/domain/permittable-group-ids.ts
new file mode 100644
index 0000000..c91e63e
--- /dev/null
+++ b/src/app/services/teller/domain/permittable-group-ids.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export class TellerPermittableGroupIds {
+ public static readonly TELLER_MANAGEMENT = 'teller__v1__management';
+ public static readonly TELLER_OPERATION = 'teller__v1__operation';
+}
diff --git a/src/app/services/teller/domain/teller-authentication.model.ts b/src/app/services/teller/domain/teller-authentication.model.ts
new file mode 100644
index 0000000..589774b
--- /dev/null
+++ b/src/app/services/teller/domain/teller-authentication.model.ts
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface TellerAuthentication {
+ employeeIdentifier: string;
+ password: string;
+}
diff --git a/src/app/services/teller/domain/teller-balance-sheet.model.ts b/src/app/services/teller/domain/teller-balance-sheet.model.ts
new file mode 100644
index 0000000..989b18d
--- /dev/null
+++ b/src/app/services/teller/domain/teller-balance-sheet.model.ts
@@ -0,0 +1,29 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {TellerEntry} from './teller-entry.model';
+
+export interface TellerBalanceSheet {
+ day?: string;
+ cashOnHand: string;
+ cashReceivedTotal: string;
+ cashDisbursedTotal: string;
+ chequesReceivedTotal: string;
+ cashEntries: TellerEntry[];
+ chequeEntries: TellerEntry[];
+}
diff --git a/src/app/services/teller/domain/teller-denomination.model.ts b/src/app/services/teller/domain/teller-denomination.model.ts
new file mode 100644
index 0000000..b28dc8e
--- /dev/null
+++ b/src/app/services/teller/domain/teller-denomination.model.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export interface TellerDenomination {
+ countedTotal: string;
+ note: string;
+ adjustingJournalEntry?: string;
+ createdOn?: string;
+ createdBy?: string;
+}
diff --git a/src/app/services/teller/domain/teller-entry.model.ts b/src/app/services/teller/domain/teller-entry.model.ts
new file mode 100644
index 0000000..6839269
--- /dev/null
+++ b/src/app/services/teller/domain/teller-entry.model.ts
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export type Type = 'DEBIT' | 'CREDIT' | 'CHEQUE';
+
+export interface TellerEntry {
+ type?: Type;
+ transactionDate: string;
+ message: string;
+ amount: number;
+ balance: number;
+}
diff --git a/src/app/services/teller/domain/teller-management-command.model.ts b/src/app/services/teller/domain/teller-management-command.model.ts
new file mode 100644
index 0000000..97f395f
--- /dev/null
+++ b/src/app/services/teller/domain/teller-management-command.model.ts
@@ -0,0 +1,29 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export type Action = 'OPEN' | 'CLOSE';
+
+export type Adjustment = 'NONE' | 'DEBIT' | 'CREDIT';
+
+export interface TellerManagementCommand {
+ action: Action;
+ adjustment?: Adjustment;
+ amount?: number;
+ assignedEmployeeIdentifier?: string;
+}
diff --git a/src/app/services/teller/domain/teller-transaction-costs.model.ts b/src/app/services/teller/domain/teller-transaction-costs.model.ts
new file mode 100644
index 0000000..307071d
--- /dev/null
+++ b/src/app/services/teller/domain/teller-transaction-costs.model.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {Charge} from './charge.model';
+
+export interface TellerTransactionCosts {
+ tellerTransactionIdentifier?: string;
+ totalAmount?: string;
+ charges?: Charge[];
+}
diff --git a/src/app/services/teller/domain/teller-transaction.model.ts b/src/app/services/teller/domain/teller-transaction.model.ts
new file mode 100644
index 0000000..11de9cf
--- /dev/null
+++ b/src/app/services/teller/domain/teller-transaction.model.ts
@@ -0,0 +1,39 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {Cheque} from './cheque.model';
+
+export type State = 'PENDING' | 'CANCELED' | 'CONFIRMED';
+
+export type TransactionType = 'ACCO' | 'ACCC' | 'ACCT' | 'CDPT' | 'CWDL' | 'PPAY' | 'CCHQ';
+
+export interface TellerTransaction {
+ identifier?: string;
+ transactionType: TransactionType;
+ transactionDate: string;
+ customerIdentifier: string;
+ productIdentifier: string;
+ productCaseIdentifier?: string;
+ customerAccountIdentifier: string;
+ targetAccountIdentifier?: string;
+ clerk: string;
+ amount: number;
+ state?: State;
+ cheque?: Cheque;
+}
diff --git a/src/app/services/teller/domain/teller.model.ts b/src/app/services/teller/domain/teller.model.ts
new file mode 100644
index 0000000..6e5ef2c
--- /dev/null
+++ b/src/app/services/teller/domain/teller.model.ts
@@ -0,0 +1,39 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export type Status = 'ACTIVE' | 'CLOSED' | 'OPEN' | 'PAUSED';
+
+export interface Teller {
+ code: string;
+ password: string;
+ cashdrawLimit: number;
+ tellerAccountIdentifier: string;
+ vaultAccountIdentifier: string;
+ chequesReceivableAccount: string;
+ cashOverShortAccount: string;
+ denominationRequired: boolean;
+ assignedEmployee?: string;
+ state?: Status;
+ createdBy?: string;
+ createdOn?: string;
+ lastModifiedBy?: string;
+ lastModifiedOn?: string;
+ lastOpenedBy?: string;
+ lastOpenedOn?: string;
+}
diff --git a/src/app/services/teller/teller-service.ts b/src/app/services/teller/teller-service.ts
new file mode 100644
index 0000000..1655496
--- /dev/null
+++ b/src/app/services/teller/teller-service.ts
@@ -0,0 +1,106 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {Inject, Injectable} from '@angular/core';
+import {HttpClient} from '../http/http.service';
+import {Teller} from './domain/teller.model';
+import {Observable} from 'rxjs/Observable';
+import {TellerManagementCommand} from './domain/teller-management-command.model';
+import {TellerBalanceSheet} from './domain/teller-balance-sheet.model';
+import {TellerAuthentication} from './domain/teller-authentication.model';
+import {RequestOptionsArgs, URLSearchParams} from '@angular/http';
+import {TellerTransactionCosts} from './domain/teller-transaction-costs.model';
+import {TellerTransaction} from './domain/teller-transaction.model';
+import {TellerDenomination} from './domain/teller-denomination.model';
+
+@Injectable()
+export class TellerService {
+
+ constructor(private http: HttpClient, @Inject('tellerBaseUrl') private baseUrl: string) {
+ }
+
+ create(officeIdentifier: string, teller: Teller): Observable<void> {
+ return this.http.post(`${this.baseUrl}/offices/${officeIdentifier}/teller`, teller);
+ }
+
+ find(officeIdentifier: string, tellerCode: string): Observable<Teller> {
+ return this.http.get(`${this.baseUrl}/offices/${officeIdentifier}/teller/${tellerCode}`);
+ }
+
+ fetch(officeIdentifier: string): Observable<Teller[]> {
+ return this.http.get(`${this.baseUrl}/offices/${officeIdentifier}/teller`);
+ }
+
+ change(officeIdentifier: string, teller: Teller): Observable<void> {
+ return this.http.put(`${this.baseUrl}/offices/${officeIdentifier}/teller/${teller.code}`, teller);
+ }
+
+ createCommand(officeIdentifier: string, tellerCode: string, tellerManagementCommand: TellerManagementCommand): Observable<void> {
+ return this.http.post(`${this.baseUrl}/offices/${officeIdentifier}/teller/${tellerCode}/commands`, tellerManagementCommand);
+ }
+
+ getBalance(officeIdentifier: string, tellerCode: string): Observable<TellerBalanceSheet> {
+ return this.http.get(`${this.baseUrl}/offices/${officeIdentifier}/teller/${tellerCode}/balance`);
+ }
+
+ unlockDrawer(tellerCode: string, tellerAuthentication: TellerAuthentication): Observable<Teller> {
+ return this.http.post(`${this.baseUrl}/teller/${tellerCode}/drawer`, tellerAuthentication, undefined, true);
+ }
+
+ executeCommand(tellerCode: string, command: string): Observable<void> {
+ const params = new URLSearchParams();
+ params.append('command', command);
+
+ const requestOptions: RequestOptionsArgs = {
+ params
+ };
+
+ return this.http.post(`${this.baseUrl}/teller/${tellerCode}`, {}, requestOptions);
+ }
+
+ createTransaction(tellerCode: string, tellerTransaction: TellerTransaction): Observable<TellerTransactionCosts> {
+ return this.http.post(`${this.baseUrl}/teller/${tellerCode}/transactions`, tellerTransaction);
+ }
+
+ confirmTransaction(tellerCode: string, tellerTransactionIdentifier: string, command: string,
+ chargesIncluded?: boolean): Observable<void> {
+ const params = new URLSearchParams();
+ params.append('command', command);
+ params.append('charges', chargesIncluded ? 'included' : 'excluded');
+
+ const requestOptions: RequestOptionsArgs = {
+ params
+ };
+
+ return this.http.post(`${this.baseUrl}/teller/${tellerCode}/transactions/${tellerTransactionIdentifier}`, {}, requestOptions);
+ }
+
+ getTransactions(tellerCode: string): Observable<TellerTransaction[]> {
+ return this.http.get(`${this.baseUrl}/teller/${tellerCode}/transactions`);
+ }
+
+ saveTellerDenomination(officeIdentifier: string, tellerCode: string, tellerDenomination: TellerDenomination): Observable<void> {
+ return this.http.post(`${this.baseUrl}/offices/${officeIdentifier}/teller/${tellerCode}/denominations`, tellerDenomination);
+ }
+
+ fetchTellerDenominations(officeIdentifier: string, tellerCode: string): Observable<TellerDenomination[]> {
+ return this.http.get(`${this.baseUrl}/offices/${officeIdentifier}/teller/${tellerCode}/denominations`);
+ }
+
+}
diff --git a/src/app/store/account/account.actions.ts b/src/app/store/account/account.actions.ts
index 2b1c406..9e3f735 100644
--- a/src/app/store/account/account.actions.ts
+++ b/src/app/store/account/account.actions.ts
@@ -18,7 +18,7 @@
*/
import {Action} from '@ngrx/store';
import {type} from '../util';
-import {FetchRequest} from '../../sevices/domain/paging/fetch-request.model';
+import {FetchRequest} from '../../services/domain/paging/fetch-request.model';
import {SearchResult} from '../../common/store/search.reducer';
export const SEARCH = type('[Account] Search');
diff --git a/src/app/store/account/effects/service.effects.spec.ts b/src/app/store/account/effects/service.effects.spec.ts
index 1b0db8a..7129a89 100644
--- a/src/app/store/account/effects/service.effects.spec.ts
+++ b/src/app/store/account/effects/service.effects.spec.ts
@@ -19,8 +19,8 @@
import {fakeAsync, TestBed, tick} from '@angular/core/testing';
import {EffectsRunner, EffectsTestingModule} from '@ngrx/effects/testing';
import {AccountSearchApiEffects} from './service.effects';
-import {AccountingService} from '../../../sevices/accounting/accounting.service';
-import {AccountPage} from '../../../sevices/accounting/domain/account-page.model';
+import {AccountingService} from '../../../services/accounting/accounting.service';
+import {AccountPage} from '../../../services/accounting/domain/account-page.model';
import {SearchAction, SearchByLedgerAction, SearchCompleteAction} from '../account.actions';
import {Observable} from 'rxjs/Observable';
import {emptySearchResult} from '../../../common/store/search.reducer';
diff --git a/src/app/store/account/effects/service.effects.ts b/src/app/store/account/effects/service.effects.ts
index 7d111c2..c53f52d 100644
--- a/src/app/store/account/effects/service.effects.ts
+++ b/src/app/store/account/effects/service.effects.ts
@@ -22,9 +22,9 @@
import {Action} from '@ngrx/store';
import {of} from 'rxjs/observable/of';
import * as accountActions from '../account.actions';
-import {AccountingService} from '../../../sevices/accounting/accounting.service';
+import {AccountingService} from '../../../services/accounting/accounting.service';
import {emptySearchResult, SearchResult} from '../../../common/store/search.reducer';
-import {AccountPage} from '../../../sevices/accounting/domain/account-page.model';
+import {AccountPage} from '../../../services/accounting/domain/account-page.model';
@Injectable()
export class AccountSearchApiEffects {
diff --git a/src/app/store/customer/customer.actions.ts b/src/app/store/customer/customer.actions.ts
index c034c34..d04a639 100644
--- a/src/app/store/customer/customer.actions.ts
+++ b/src/app/store/customer/customer.actions.ts
@@ -19,7 +19,7 @@
import {Action} from '@ngrx/store';
import {type} from '../util';
-import {FetchRequest} from '../../sevices/domain/paging/fetch-request.model';
+import {FetchRequest} from '../../services/domain/paging/fetch-request.model';
import {SearchResult} from '../../common/store/search.reducer';
export const SEARCH = type('[Customer] Search');
diff --git a/src/app/store/customer/effects/service.effects.spec.ts b/src/app/store/customer/effects/service.effects.spec.ts
index d96b158..43f031e 100644
--- a/src/app/store/customer/effects/service.effects.spec.ts
+++ b/src/app/store/customer/effects/service.effects.spec.ts
@@ -21,9 +21,9 @@
import {EffectsRunner, EffectsTestingModule} from '@ngrx/effects/testing';
import {CustomerSearchApiEffects} from './service.effects';
import {Observable} from 'rxjs/Observable';
-import {CustomerService} from '../../../sevices/customer/customer.service';
+import {CustomerService} from '../../../services/customer/customer.service';
import {SearchAction, SearchCompleteAction} from '../customer.actions';
-import {CustomerPage} from '../../../sevices/customer/domain/customer-page.model';
+import {CustomerPage} from '../../../services/customer/domain/customer-page.model';
import {emptySearchResult} from '../../../common/store/search.reducer';
describe('Customer Search Api Effects', () => {
diff --git a/src/app/store/customer/effects/service.effects.ts b/src/app/store/customer/effects/service.effects.ts
index ab9432a..3d95d9e 100644
--- a/src/app/store/customer/effects/service.effects.ts
+++ b/src/app/store/customer/effects/service.effects.ts
@@ -22,7 +22,7 @@
import {Action} from '@ngrx/store';
import {of} from 'rxjs/observable/of';
import * as customerActions from '../customer.actions';
-import {CustomerService} from '../../../sevices/customer/customer.service';
+import {CustomerService} from '../../../services/customer/customer.service';
import {emptySearchResult} from '../../../common/store/search.reducer';
@Injectable()
diff --git a/src/app/store/employee/effects/service.effects.spec.ts b/src/app/store/employee/effects/service.effects.spec.ts
index f0b5094..7fe718c 100644
--- a/src/app/store/employee/effects/service.effects.spec.ts
+++ b/src/app/store/employee/effects/service.effects.spec.ts
@@ -20,9 +20,9 @@
import {EffectsRunner, EffectsTestingModule} from '@ngrx/effects/testing';
import {EmployeeSearchApiEffects} from './service.effects';
import {Observable} from 'rxjs/Observable';
-import {OfficeService} from '../../../sevices/office/office.service';
+import {OfficeService} from '../../../services/office/office.service';
import {SearchAction, SearchCompleteAction} from '../employee.actions';
-import {EmployeePage} from '../../../sevices/office/domain/employee-page.model';
+import {EmployeePage} from '../../../services/office/domain/employee-page.model';
import {emptySearchResult} from '../../../common/store/search.reducer';
describe('Employee Search Api Effects', () => {
diff --git a/src/app/store/employee/effects/service.effects.ts b/src/app/store/employee/effects/service.effects.ts
index ecf7940..4324794 100644
--- a/src/app/store/employee/effects/service.effects.ts
+++ b/src/app/store/employee/effects/service.effects.ts
@@ -17,7 +17,7 @@
* under the License.
*/
import {Injectable} from '@angular/core';
-import {OfficeService} from '../../../sevices/office/office.service';
+import {OfficeService} from '../../../services/office/office.service';
import {Actions, Effect} from '@ngrx/effects';
import {Observable} from 'rxjs/Observable';
import {Action} from '@ngrx/store';
diff --git a/src/app/store/employee/employee.actions.ts b/src/app/store/employee/employee.actions.ts
index 2716256..6526245 100644
--- a/src/app/store/employee/employee.actions.ts
+++ b/src/app/store/employee/employee.actions.ts
@@ -18,7 +18,7 @@
*/
import {Action} from '@ngrx/store';
import {type} from '../util';
-import {FetchRequest} from '../../sevices/domain/paging/fetch-request.model';
+import {FetchRequest} from '../../services/domain/paging/fetch-request.model';
import {SearchResult} from '../../common/store/search.reducer';
export const SEARCH = type('[Employee] Search');
diff --git a/src/app/store/index.ts b/src/app/store/index.ts
index 70005b4..f31b4ab 100644
--- a/src/app/store/index.ts
+++ b/src/app/store/index.ts
@@ -23,7 +23,7 @@
import * as fromAuthorization from './security/authorization.reducer';
import * as fromAccounts from './account/accounts.reducer';
import * as authenticationActions from './security/security.actions';
-import {compose} from '@ngrx/store';
+import {compose} from '@ngrx/core/compose';
import {localStorageSync} from 'ngrx-store-localstorage';
import {
createSearchReducer,
diff --git a/src/app/store/ledger/effects/service.effects.ts b/src/app/store/ledger/effects/service.effects.ts
index 422c1fd..28a87ad 100644
--- a/src/app/store/ledger/effects/service.effects.ts
+++ b/src/app/store/ledger/effects/service.effects.ts
@@ -23,7 +23,7 @@
import {Action} from '@ngrx/store';
import {of} from 'rxjs/observable/of';
import * as ledgerActions from '../ledger.actions';
-import {AccountingService} from '../../../sevices/accounting/accounting.service';
+import {AccountingService} from '../../../services/accounting/accounting.service';
import {emptySearchResult} from '../../../common/store/search.reducer';
@Injectable()
diff --git a/src/app/store/ledger/ledger.actions.ts b/src/app/store/ledger/ledger.actions.ts
index ff522e8..6368bb2 100644
--- a/src/app/store/ledger/ledger.actions.ts
+++ b/src/app/store/ledger/ledger.actions.ts
@@ -19,7 +19,7 @@
import {type} from '../util';
import {Action} from '@ngrx/store';
-import {FetchRequest} from '../../sevices/domain/paging/fetch-request.model';
+import {FetchRequest} from '../../services/domain/paging/fetch-request.model';
import {SearchResult} from '../../common/store/search.reducer';
export const SEARCH = type('[Ledger] Search');
diff --git a/src/app/store/office/effects/service.effects.spec.ts b/src/app/store/office/effects/service.effects.spec.ts
index 15ba058..13bf8b4 100644
--- a/src/app/store/office/effects/service.effects.spec.ts
+++ b/src/app/store/office/effects/service.effects.spec.ts
@@ -20,9 +20,9 @@
import {EffectsRunner, EffectsTestingModule} from '@ngrx/effects/testing';
import {OfficeSearchApiEffects} from './service.effects';
import {Observable} from 'rxjs/Observable';
-import {OfficeService} from '../../../sevices/office/office.service';
+import {OfficeService} from '../../../services/office/office.service';
import {SearchAction, SearchCompleteAction} from '../office.actions';
-import {OfficePage} from '../../../sevices/office/domain/office-page.model';
+import {OfficePage} from '../../../services/office/domain/office-page.model';
import {emptySearchResult} from '../../../common/store/search.reducer';
describe('Office Search Api Effects', () => {
diff --git a/src/app/store/office/effects/service.effects.ts b/src/app/store/office/effects/service.effects.ts
index e2cae3c..b55c6f2 100644
--- a/src/app/store/office/effects/service.effects.ts
+++ b/src/app/store/office/effects/service.effects.ts
@@ -17,7 +17,7 @@
* under the License.
*/
import {Injectable} from '@angular/core';
-import {OfficeService} from '../../../sevices/office/office.service';
+import {OfficeService} from '../../../services/office/office.service';
import {Actions, Effect} from '@ngrx/effects';
import {Observable} from 'rxjs/Observable';
import {Action} from '@ngrx/store';
diff --git a/src/app/store/office/office.actions.ts b/src/app/store/office/office.actions.ts
index c2a8bc4..f03022b 100644
--- a/src/app/store/office/office.actions.ts
+++ b/src/app/store/office/office.actions.ts
@@ -18,7 +18,7 @@
*/
import {Action} from '@ngrx/store';
import {type} from '../util';
-import {FetchRequest} from '../../sevices/domain/paging/fetch-request.model';
+import {FetchRequest} from '../../services/domain/paging/fetch-request.model';
import {SearchResult} from '../../common/store/search.reducer';
export const SEARCH = type('[Office] Search');
diff --git a/src/app/store/role/effects/service.effects.spec.ts b/src/app/store/role/effects/service.effects.spec.ts
index 5d08b61..7cc2359 100644
--- a/src/app/store/role/effects/service.effects.spec.ts
+++ b/src/app/store/role/effects/service.effects.spec.ts
@@ -21,8 +21,8 @@
import {EffectsRunner, EffectsTestingModule} from '@ngrx/effects/testing';
import {RoleSearchApiEffects} from './service.effects';
import {Observable} from 'rxjs/Observable';
-import {IdentityService} from '../../../sevices/identity/identity.service';
-import {Role} from '../../../sevices/identity/domain/role.model';
+import {IdentityService} from '../../../services/identity/identity.service';
+import {Role} from '../../../services/identity/domain/role.model';
import {SearchAction, SearchCompleteAction} from '../role.actions';
import {emptySearchResult} from '../../../common/store/search.reducer';
diff --git a/src/app/store/role/effects/service.effects.ts b/src/app/store/role/effects/service.effects.ts
index 7173dc3..5945a90 100644
--- a/src/app/store/role/effects/service.effects.ts
+++ b/src/app/store/role/effects/service.effects.ts
@@ -22,9 +22,9 @@
import {Action} from '@ngrx/store';
import {of} from 'rxjs/observable/of';
import * as roleActions from '../role.actions';
-import {IdentityService} from '../../../sevices/identity/identity.service';
+import {IdentityService} from '../../../services/identity/identity.service';
import {emptySearchResult} from '../../../common/store/search.reducer';
-import {Role} from '../../../sevices/identity/domain/role.model';
+import {Role} from '../../../services/identity/domain/role.model';
const SYSTEM_ROLES: string[] = ['pharaoh', 'scheduler'];
diff --git a/src/app/store/security/authentication.reducer.spec.ts b/src/app/store/security/authentication.reducer.spec.ts
index 6b107aa..349d13b 100644
--- a/src/app/store/security/authentication.reducer.spec.ts
+++ b/src/app/store/security/authentication.reducer.spec.ts
@@ -19,7 +19,7 @@
import {mockAuthentication} from './testing/authentication.mock';
import {reducer} from './authentication.reducer';
import {LoginSuccessAction, LoginSuccessPayload, RefreshAccessTokenSuccessAction} from './security.actions';
-import {Authentication} from '../../sevices/identity/domain/authentication.model';
+import {Authentication} from '../../services/identity/domain/authentication.model';
describe('Authentication Reducer', () => {
diff --git a/src/app/store/security/authentication.reducer.ts b/src/app/store/security/authentication.reducer.ts
index 91fba56..fcf6c8c 100644
--- a/src/app/store/security/authentication.reducer.ts
+++ b/src/app/store/security/authentication.reducer.ts
@@ -18,7 +18,7 @@
*/
import * as security from './security.actions';
import {LoginSuccessPayload} from './security.actions';
-import {Authentication} from '../../sevices/identity/domain/authentication.model';
+import {Authentication} from '../../services/identity/domain/authentication.model';
export interface State {
username: string;
diff --git a/src/app/store/security/authorization.reducer.ts b/src/app/store/security/authorization.reducer.ts
index 0ad38ca..b2fe184 100644
--- a/src/app/store/security/authorization.reducer.ts
+++ b/src/app/store/security/authorization.reducer.ts
@@ -17,7 +17,7 @@
* under the License.
*/
import * as security from './security.actions';
-import {FimsPermission} from '../../sevices/security/authz/fims-permission.model';
+import {FimsPermission} from '../../services/security/authz/fims-permission.model';
export interface State {
permissions: FimsPermission[];
diff --git a/src/app/store/security/effects/notification.effects.ts b/src/app/store/security/effects/notification.effects.ts
index 3dedf2a..f67ede7 100644
--- a/src/app/store/security/effects/notification.effects.ts
+++ b/src/app/store/security/effects/notification.effects.ts
@@ -22,7 +22,7 @@
import {Action} from '@ngrx/store';
import * as securityActions from '../security.actions';
import {LoginSuccessAction} from '../security.actions';
-import {NotificationService, NotificationType} from '../../../sevices/notification/notification.service';
+import {NotificationService, NotificationType} from '../../../services/notification/notification.service';
@Injectable()
export class SecurityNotificationEffects {
diff --git a/src/app/store/security/effects/service.effects.spec.ts b/src/app/store/security/effects/service.effects.spec.ts
index 2ce07e2..3f87cc2 100644
--- a/src/app/store/security/effects/service.effects.spec.ts
+++ b/src/app/store/security/effects/service.effects.spec.ts
@@ -20,8 +20,8 @@
import {EffectsRunner, EffectsTestingModule} from '@ngrx/effects/testing';
import {SecurityApiEffects} from './service.effects';
import {Observable} from 'rxjs/Observable';
-import {IdentityService} from '../../../sevices/identity/identity.service';
-import {AuthenticationService} from '../../../sevices/security/authn/authentication.service';
+import {IdentityService} from '../../../services/identity/identity.service';
+import {AuthenticationService} from '../../../services/security/authn/authentication.service';
import {
ChangePasswordAction,
ChangePasswordSuccessAction,
@@ -35,11 +35,11 @@
RefreshAccessTokenSuccessAction,
RefreshTokenStartTimerAction
} from '../security.actions';
-import {PermittableGroupIdMapper} from '../../../sevices/security/authz/permittable-group-id-mapper';
+import {PermittableGroupIdMapper} from '../../../services/security/authz/permittable-group-id-mapper';
import {Store} from '@ngrx/store';
-import {FimsPermission} from '../../../sevices/security/authz/fims-permission.model';
-import {Permission} from '../../../sevices/identity/domain/permission.model';
-import {IdentityPermittableGroupIds} from '../../../sevices/identity/domain/permittable-group-ids.model';
+import {FimsPermission} from '../../../services/security/authz/fims-permission.model';
+import {Permission} from '../../../services/identity/domain/permission.model';
+import {IdentityPermittableGroupIds} from '../../../services/identity/domain/permittable-group-ids.model';
import {mockAuthentication} from '../testing/authentication.mock';
describe('Security Api Effects', () => {
diff --git a/src/app/store/security/effects/service.effects.ts b/src/app/store/security/effects/service.effects.ts
index 55845dd..4e0f582 100644
--- a/src/app/store/security/effects/service.effects.ts
+++ b/src/app/store/security/effects/service.effects.ts
@@ -22,14 +22,14 @@
import {Action, Store} from '@ngrx/store';
import {of} from 'rxjs/observable/of';
import * as securityActions from '../security.actions';
-import {AuthenticationService} from '../../../sevices/security/authn/authentication.service';
-import {PermissionId} from '../../../sevices/security/authz/permission-id.type';
-import {FimsPermission} from '../../../sevices/security/authz/fims-permission.model';
-import {Permission} from '../../../sevices/identity/domain/permission.model';
-import {PermittableGroupIdMapper} from '../../../sevices/security/authz/permittable-group-id-mapper';
+import {AuthenticationService} from '../../../services/security/authn/authentication.service';
+import {PermissionId} from '../../../services/security/authz/permission-id.type';
+import {FimsPermission} from '../../../services/security/authz/fims-permission.model';
+import {Permission} from '../../../services/identity/domain/permission.model';
+import {PermittableGroupIdMapper} from '../../../services/security/authz/permittable-group-id-mapper';
import * as fromRoot from '../../index';
-import {IdentityService} from '../../../sevices/identity/identity.service';
-import {Password} from '../../../sevices/identity/domain/password.model';
+import {IdentityService} from '../../../services/identity/identity.service';
+import {Password} from '../../../services/identity/domain/password.model';
@Injectable()
export class SecurityApiEffects {
diff --git a/src/app/store/security/security.actions.ts b/src/app/store/security/security.actions.ts
index 0cc5843..8dd520f 100644
--- a/src/app/store/security/security.actions.ts
+++ b/src/app/store/security/security.actions.ts
@@ -18,8 +18,8 @@
*/
import {Action} from '@ngrx/store';
import {type} from '../util';
-import {Authentication} from '../../sevices/identity/domain/authentication.model';
-import {FimsPermission} from '../../sevices/security/authz/fims-permission.model';
+import {Authentication} from '../../services/identity/domain/authentication.model';
+import {FimsPermission} from '../../services/security/authz/fims-permission.model';
export const LOGIN = type('[Security] Login');
export const LOGIN_SUCCESS = type('[Security] Login Success');
diff --git a/src/app/store/security/testing/authentication.mock.ts b/src/app/store/security/testing/authentication.mock.ts
index 9d37fe9..cc248f8 100644
--- a/src/app/store/security/testing/authentication.mock.ts
+++ b/src/app/store/security/testing/authentication.mock.ts
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import {Authentication} from '../../../sevices/identity/domain/authentication.model';
+import {Authentication} from '../../../services/identity/domain/authentication.model';
export function mockAuthentication(): Authentication {
return {
diff --git a/src/app/teller/auth/teller-auth.component.html b/src/app/teller/auth/teller-auth.component.html
new file mode 100644
index 0000000..b19b19e
--- /dev/null
+++ b/src/app/teller/auth/teller-auth.component.html
@@ -0,0 +1,47 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<div layout="column" layout-fill>
+ <div class="mat-content" layout-padding flex>
+ <div layout-gt-xs="row" layout-align-gt-xs="center start" class="margin">
+ <div flex-gt-xs="25" layout-align="center center" layout-margin>
+ <mat-card>
+ <mat-card-title>
+ unlock drawer
+ </mat-card-title>
+ <form [formGroup]="form" (ngSubmit)="auth()">
+ <mat-card-content layout="column">
+ <fims-text-input [form]="form" controlName="tellerCode" placeholder="{{'Teller number' | translate}}"></fims-text-input>
+ <div layout="row">
+ <mat-form-field layout-margin flex>
+ <input matInput placeholder="{{'Password' | translate}}" type="password" formControlName="password" autocomplete="new-password"/>
+ <mat-error *ngIf="form.get('password').hasError('required')" translate>
+ Required
+ </mat-error>
+ </mat-form-field>
+ </div>
+ <p class="mat-caption tc-red-700" *ngIf="error$ | async">{{'Sorry, that login did not work.' | translate}}</p>
+ </mat-card-content>
+ <mat-card-actions>
+ <button type="submit" mat-raised-button color="primary" [disabled]="form.invalid">{{'UNLOCK' | translate}}</button>
+ </mat-card-actions>
+ </form>
+ </mat-card>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/src/app/teller/auth/teller-auth.component.ts b/src/app/teller/auth/teller-auth.component.ts
new file mode 100644
index 0000000..8ed9904
--- /dev/null
+++ b/src/app/teller/auth/teller-auth.component.ts
@@ -0,0 +1,74 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import * as fromTeller from '../store/index';
+import {TellerStore} from '../store/index';
+import * as fromRoot from '../../store/index';
+import {UNLOCK_DRAWER} from '../store/teller.actions';
+import {Subscription} from 'rxjs/Subscription';
+import {FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {Observable} from 'rxjs/Observable';
+
+@Component({
+ templateUrl: './teller-auth.component.html'
+})
+export class TellerAuthComponent implements OnInit, OnDestroy {
+
+ private userIdSubscription: Subscription;
+
+ private userId: string;
+
+ form: FormGroup;
+
+ error$: Observable<boolean>;
+
+ constructor(private store: TellerStore, private formBuilder: FormBuilder) {
+ this.userIdSubscription = this.store.select(fromRoot.getUsername)
+ .subscribe(username => this.userId = username);
+ }
+
+ ngOnInit(): void {
+ this.form = this.formBuilder.group({
+ tellerCode: ['', Validators.required],
+ password: ['', Validators.required]
+ });
+
+ this.error$ = this.store.select(fromTeller.getAuthenticationError)
+ .map(error => !!error);
+ }
+
+ ngOnDestroy(): void {
+ this.userIdSubscription.unsubscribe();
+ }
+
+ auth(): void {
+ const tellerCode = this.form.get('tellerCode').value;
+ const password = this.form.get('password').value;
+
+ this.store.dispatch({
+ type: UNLOCK_DRAWER,
+ payload: {
+ employeeId: this.userId,
+ tellerCode,
+ password
+ }
+ });
+ }
+
+}
diff --git a/src/app/teller/customer/customer-detail.component.html b/src/app/teller/customer/customer-detail.component.html
new file mode 100644
index 0000000..105a65b
--- /dev/null
+++ b/src/app/teller/customer/customer-detail.component.html
@@ -0,0 +1,56 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over *ngIf="customer$ | async as customer" title="{{customer.givenName}} {{customer.surname}}" [navigateBackTo]="['../../../../']">
+ <fims-two-column-layout>
+ <ng-container left>
+ <fims-portrait [blob]="portrait$ | async"></fims-portrait>
+ <mat-nav-list>
+ <h3 mat-subheader translate>Transactions</h3>
+ <ng-template let-item let-last="last" ngFor [ngForOf]="availableActions$ | async">
+ <a mat-list-item [routerLink]="[item.relativeLink]" [queryParams]="{ transactionType: item.transactionType }">
+ <mat-icon matListAvatar>{{item.icon}}</mat-icon>
+ <h3 matLine translate>{{item.title}}</h3>
+ </a>
+ </ng-template>
+ </mat-nav-list>
+ </ng-container>
+ <mat-list right>
+ <h3 mat-subheader translate>Address</h3>
+ <mat-list-item>
+ <mat-icon matListAvatar>location_on</mat-icon>
+ <h3 matLine>{{customer.address?.street}}, {{customer.address?.city}}, {{customer.address?.postalCode}},
+ {{customer.address?.country}}</h3>
+ </mat-list-item>
+ <h3 mat-subheader translate>Contact information</h3>
+ <mat-list-item [ngSwitch]="detail.type" *ngFor="let detail of customer.contactDetails">
+ <mat-icon *ngSwitchCase="'EMAIL'" matListAvatar>email</mat-icon>
+ <mat-icon *ngSwitchCase="'PHONE'" matListAvatar>phone</mat-icon>
+ <mat-icon *ngSwitchCase="'MOBILE'" matListAvatar>smartphone</mat-icon>
+ <h3 matLine>{{detail.value}}</h3>
+ </mat-list-item>
+ <mat-list-item *ngIf="!customer.contactDetails?.length">
+ <h3 matLine translate>No contact details available</h3>
+ </mat-list-item>
+ <h3 mat-subheader translate>Birthday</h3>
+ <mat-list-item>
+ <mat-icon matListAvatar>cake</mat-icon>
+ <h3 matLine>{{customer.dateOfBirth | displayFimsDate}}</h3>
+ </mat-list-item>
+ </mat-list>
+ </fims-two-column-layout>
+</fims-layout-card-over>
diff --git a/src/app/teller/customer/customer-detail.component.ts b/src/app/teller/customer/customer-detail.component.ts
new file mode 100644
index 0000000..0588016
--- /dev/null
+++ b/src/app/teller/customer/customer-detail.component.ts
@@ -0,0 +1,75 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy} from '@angular/core';
+import * as fromTeller from '../store/index';
+import {TellerStore} from '../store/index';
+import {Customer} from '../../services/customer/domain/customer.model';
+import {Observable} from 'rxjs/Observable';
+import {Subscription} from 'rxjs/Subscription';
+import {LoadAllDepositProductsAction, LoadAllLoanProductsAction} from '../store/teller.actions';
+import {CustomerService} from '../../services/customer/customer.service';
+import {Action, AvailableActionService} from '../services/available-actions.service';
+
+@Component({
+ templateUrl: './customer-detail.component.html'
+})
+export class TellerCustomerDetailComponent implements OnDestroy {
+
+ private loadDepositProductsSubscription: Subscription;
+
+ private loadLoanProductsSubscription: Subscription;
+
+ portrait$: Observable<Blob>;
+
+ customer$: Observable<Customer>;
+
+ hasDepositProducts$: Observable<boolean>;
+
+ hasLoanProducts$: Observable<boolean>;
+
+ availableActions$: Observable<Action[]>;
+
+ constructor(private store: TellerStore, private customerService: CustomerService, private actionService: AvailableActionService) {
+ this.customer$ = store.select(fromTeller.getTellerSelectedCustomer)
+ .filter(customer => !!customer);
+
+ this.portrait$ = this.customer$
+ .flatMap(customer => this.customerService.getPortrait(customer.identifier));
+
+ this.hasDepositProducts$ = store.select(fromTeller.hasTellerCustomerDepositProducts);
+
+ this.hasLoanProducts$ = store.select(fromTeller.hasTellerCustomerLoanProducts);
+
+ this.loadDepositProductsSubscription = this.customer$
+ .map(customer => new LoadAllDepositProductsAction(customer.identifier))
+ .subscribe(this.store);
+
+ this.loadLoanProductsSubscription = this.customer$
+ .map(customer => new LoadAllLoanProductsAction(customer.identifier))
+ .subscribe(this.store);
+
+ this.availableActions$ = this.customer$
+ .mergeMap(customer => this.actionService.getAvailableActions(customer.identifier));
+ }
+
+ ngOnDestroy(): void {
+ this.loadDepositProductsSubscription.unsubscribe();
+ this.loadLoanProductsSubscription.unsubscribe();
+ }
+}
diff --git a/src/app/teller/customer/customer-index.component.html b/src/app/teller/customer/customer-index.component.html
new file mode 100644
index 0000000..ca721b3
--- /dev/null
+++ b/src/app/teller/customer/customer-index.component.html
@@ -0,0 +1,18 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<router-outlet></router-outlet>
diff --git a/src/app/teller/customer/customer-index.component.ts b/src/app/teller/customer/customer-index.component.ts
new file mode 100644
index 0000000..1b110bb
--- /dev/null
+++ b/src/app/teller/customer/customer-index.component.ts
@@ -0,0 +1,44 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {SelectCustomerAction} from '../store/teller.actions';
+import {TellerStore} from '../store/index';
+import {Subscription} from 'rxjs/Subscription';
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+
+@Component({
+ templateUrl: './customer-index.component.html'
+})
+export class TellerCustomerIndexComponent implements OnInit, OnDestroy {
+
+ private actionsSubscription: Subscription;
+
+ constructor(private route: ActivatedRoute, private store: TellerStore) {}
+
+ ngOnInit(): void {
+ this.actionsSubscription = this.route.params
+ .map(params => new SelectCustomerAction(params['id']))
+ .subscribe(this.store);
+ }
+
+ ngOnDestroy(): void {
+ this.actionsSubscription.unsubscribe();
+ }
+}
diff --git a/src/app/teller/customer/teller-customer-exists.guard.ts b/src/app/teller/customer/teller-customer-exists.guard.ts
new file mode 100644
index 0000000..1eadc70
--- /dev/null
+++ b/src/app/teller/customer/teller-customer-exists.guard.ts
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
+import * as fromTeller from '../store/index';
+import {TellerStore} from '../store/index';
+import {CustomerService} from '../../services/customer/customer.service';
+import {ExistsGuardService} from '../../common/guards/exists-guard';
+import {Observable} from 'rxjs/Observable';
+import {LoadCustomerAction} from '../store/teller.actions';
+import {of} from 'rxjs/observable/of';
+
+@Injectable()
+export class TellerCustomerExistsGuard implements CanActivate {
+
+ constructor(private store: TellerStore,
+ private customerService: CustomerService,
+ private existsGuardService: ExistsGuardService) {
+ }
+
+ hasCustomerInStore(id: string): Observable<boolean> {
+ const timestamp$: Observable<number> = this.store.select(fromTeller.getTellerCustomerLoadedAt)
+ .map(loadedAt => loadedAt[id]);
+
+ return this.existsGuardService.isWithinExpiry(timestamp$);
+ }
+
+ hasCustomerInApi(id: string): Observable<boolean> {
+ const getCustomer$: Observable<any> = this.customerService.getCustomer(id)
+ .map(customerEntity => new LoadCustomerAction({
+ resource: customerEntity
+ }))
+ .do((action: LoadCustomerAction) => this.store.dispatch(action))
+ .map(customer => !!customer);
+
+ return this.existsGuardService.routeTo404OnError(getCustomer$);
+ }
+
+ hasCustomer(id: string): Observable<boolean> {
+ return this.hasCustomerInStore(id)
+ .switchMap(inStore => {
+ if (inStore) {
+ return of(inStore);
+ }
+ return this.hasCustomerInApi(id);
+ });
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.hasCustomer(route.params['id']);
+ }
+}
diff --git a/src/app/teller/customer/transaction/cheque/create.component.html b/src/app/teller/customer/transaction/cheque/create.component.html
new file mode 100644
index 0000000..f06a271
--- /dev/null
+++ b/src/app/teller/customer/transaction/cheque/create.component.html
@@ -0,0 +1,33 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="Teller transaction">
+ <fims-cheque-transaction-form
+ #form
+ [customerName]="customerName$ | async"
+ [micrResolution]="micrResolution$ | async"
+ [micrResolutionError]="micrResolutionError"
+ [productInstances]="productInstances$ | async"
+ [transactionCosts]="transactionCosts$ | async"
+ [transactionCreated]="transactionCreated"
+ (onExpandMICR)="expandMICR($event)"
+ (onCreateTransaction)="createTransaction($event)"
+ (onConfirmTransaction)="confirmTransaction($event)"
+ (onCancelTransaction)="cancelTransaction()"
+ (onCancel)="cancel()">
+ </fims-cheque-transaction-form>
+</fims-layout-card-over>
diff --git a/src/app/teller/customer/transaction/cheque/create.component.ts b/src/app/teller/customer/transaction/cheque/create.component.ts
new file mode 100644
index 0000000..ecf06d1
--- /dev/null
+++ b/src/app/teller/customer/transaction/cheque/create.component.ts
@@ -0,0 +1,153 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {TellerTransaction, TransactionType} from '../../../../services/teller/domain/teller-transaction.model';
+import {TellerTransactionCosts} from '../../../../services/teller/domain/teller-transaction-costs.model';
+import {CONFIRM_TRANSACTION} from '../../../store/teller.actions';
+import * as fromTeller from '../../../store/index';
+import {TellerStore} from '../../../store/index';
+import * as fromRoot from '../../../../store/index';
+import {DepositAccountService} from '../../../../services/depositAccount/deposit-account.service';
+import {Observable} from 'rxjs/Observable';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Subscription} from 'rxjs/Subscription';
+import {ProductInstance} from '../../../../services/depositAccount/domain/instance/product-instance.model';
+import {Teller} from '../../../../services/teller/domain/teller.model';
+import {TransactionForm} from '../domain/transaction-form.model';
+import {ChequeTransactionFormComponent} from './form.component';
+import {ChequeService} from '../../../../services/cheque/cheque.service';
+import {MICRResolution} from '../../../../services/cheque/domain/micr-resolution.model';
+import {Error} from '../../../../services/domain/error.model';
+import {TellerTransactionService} from '../../../services/transaction.service';
+
+@Component({
+ templateUrl: './create.component.html'
+})
+export class CreateChequeTransactionFormComponent implements OnInit, OnDestroy {
+
+ private authenticatedTellerSubscription: Subscription;
+
+ private usernameSubscription: Subscription;
+
+ private tellerTransactionIdentifier: string;
+
+ private clerk: string;
+
+ @ViewChild('form') form: ChequeTransactionFormComponent;
+
+ transactionType: TransactionType;
+
+ customerName$: Observable<string>;
+
+ micrResolution$: Observable<MICRResolution>;
+
+ micrResolutionError: Error;
+
+ productInstances$: Observable<ProductInstance[]>;
+
+ transactionCosts$: Observable<TellerTransactionCosts>;
+
+ teller: Teller;
+
+ transactionCreated: boolean;
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: TellerStore,
+ private depositService: DepositAccountService, private chequeService: ChequeService,
+ private tellerTransactionService: TellerTransactionService) {}
+
+ ngOnInit(): void {
+ const selectedCustomer$ = this.store.select(fromTeller.getTellerSelectedCustomer)
+ .filter(customer => !!customer);
+
+ this.productInstances$ = selectedCustomer$
+ .switchMap(customer => this.depositService.fetchProductInstances(customer.identifier))
+ .map((instances: ProductInstance[]) => instances.filter(instance => instance.state === 'ACTIVE'));
+
+ this.authenticatedTellerSubscription = this.store.select(fromTeller.getAuthenticatedTeller)
+ .filter(teller => !!teller)
+ .subscribe(teller => { this.teller = teller; } );
+
+ this.usernameSubscription = this.store.select(fromRoot.getUsername)
+ .subscribe(username => this.clerk = username);
+
+ this.customerName$ = selectedCustomer$
+ .map(customer => `${customer.givenName} ${customer.surname}`);
+ }
+
+ ngOnDestroy(): void {
+ this.authenticatedTellerSubscription.unsubscribe();
+ this.usernameSubscription.unsubscribe();
+ }
+
+ expandMICR(identifier: string): void {
+ this.micrResolution$ = this.chequeService.expandMicr(identifier)
+ .do(resolution => this.micrResolutionError = null)
+ .catch(error => {
+ this.micrResolutionError = error;
+ return Observable.empty();
+ });
+ }
+
+ createTransaction(formData: TransactionForm): void {
+ const transaction: TellerTransaction = {
+ customerIdentifier: formData.customerIdentifier,
+ productIdentifier: formData.productIdentifier,
+ customerAccountIdentifier: formData.accountIdentifier,
+ targetAccountIdentifier: formData.targetAccountIdentifier,
+ amount: formData.amount,
+ clerk: this.clerk,
+ transactionDate: new Date().toISOString(),
+ cheque: formData.cheque,
+ transactionType: 'CCHQ'
+ };
+
+ this.transactionCosts$ = this.tellerTransactionService.createTransaction(this.teller.code, transaction)
+ .do(transactionCosts => this.tellerTransactionIdentifier = transactionCosts.tellerTransactionIdentifier)
+ .do(() => this.transactionCreated = true);
+ }
+
+ confirmTransaction(chargesIncluded: boolean): void {
+ this.store.dispatch({
+ type: CONFIRM_TRANSACTION,
+ payload: {
+ tellerCode: this.teller.code,
+ tellerTransactionIdentifier: this.tellerTransactionIdentifier,
+ command: 'CONFIRM',
+ chargesIncluded,
+ activatedRoute: this.route
+ }
+ });
+ }
+
+ cancelTransaction(): void {
+ this.store.dispatch({
+ type: CONFIRM_TRANSACTION,
+ payload: {
+ tellerCode: this.teller.code,
+ tellerTransactionIdentifier: this.tellerTransactionIdentifier,
+ command: 'CANCEL',
+ activatedRoute: this.route
+ }
+ });
+ }
+
+ cancel(): void {
+ this.router.navigate(['../../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/teller/customer/transaction/cheque/form.component.html b/src/app/teller/customer/transaction/cheque/form.component.html
new file mode 100644
index 0000000..7a64535
--- /dev/null
+++ b/src/app/teller/customer/transaction/cheque/form.component.html
@@ -0,0 +1,85 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #transactionStep label="{{'Transaction' | translate}}"
+ [state]="!invalid ? 'complete' : 'required'" [disabled]="transactionCreated">
+ <form [formGroup]="chequeForm" layout="row">
+ <fims-text-input [form]="chequeForm" controlName="chequeNumber" placeholder="{{'Cheque number' | translate}}"></fims-text-input>
+ <fims-text-input [form]="chequeForm" controlName="branchSortCode" placeholder="{{'Branch sort code' | translate}}"></fims-text-input>
+ <fims-text-input [form]="chequeForm" controlName="accountNumber" placeholder="{{'Account number' | translate}}"></fims-text-input>
+ </form>
+ <form [formGroup]="amountForm">
+ <div layout="row">
+ <fims-text-input [form]="amountForm" controlName="drawee" placeholder="{{'Issuing Bank' | translate}}"></fims-text-input>
+ <fims-text-input [form]="amountForm" controlName="drawer" placeholder="{{'Issuer' | translate}}"></fims-text-input>
+ <div>
+ <button mat-raised-button color="primary" (click)="validateCheque()" [disabled]="chequeForm.invalid">{{'DETERMINE FROM MICR' | translate}}</button>
+ </div>
+ </div>
+ <td-message *ngIf="!!micrResolutionError"
+ label="Issuing Bank/Issuer could not be determined in our system from the MICR you entered."
+ sublabel="Possible reasons: Cheque number has not been issued, Branch sort code is not known in system or account number does not exist in system"
+ color="accent"
+ icon="warning">
+ </td-message>
+ <fims-text-input [form]="amountForm" controlName="payee" placeholder="{{'Payee' | translate}}"></fims-text-input>
+ <fims-date-input [form]="amountForm" controlName="dateIssued" placeholder="{{'Date issued' | translate}}"></fims-date-input>
+ <mat-checkbox formControlName="openCheque" layout-margin translate>Is cheque open?</mat-checkbox>
+ <td-message *ngIf="!amountForm.get('openCheque').value" label="Please check identification card of member" color="accent" icon="warning"></td-message>
+ <fims-text-input type="number" [form]="amountForm" controlName="amount" placeholder="{{'Amount' | translate}}"></fims-text-input>
+ <div layout="row">
+ <mat-form-field layout-margin>
+ <mat-select formControlName="productInstance" placeholder="{{ 'Select account to transfer to' | translate }}">
+ <mat-option *ngFor="let instance of productInstances" [value]="instance">
+ {{instance.accountIdentifier}}({{instance.productIdentifier}})
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ </div>
+ </form>
+ <ng-template td-step-actions>
+ <button mat-raised-button color="primary" (click)="createTransaction()" [disabled]="createTransactionDisabled">{{'CREATE TRANSACTION' | translate}}</button>
+ <span flex></span>
+ <button mat-button (click)="cancel()" [disabled]="transactionCreated">{{'CANCEL' | translate}}</button>
+ </ng-template>
+ </td-step>
+ <td-step #confirmationStep label="{{'Confirmation' | translate}}">
+ <div layout-gt-xs="row" layout-align="center center">
+ <div layout-gt-xs="row" flex-gt-xs="90" layout-margin>
+ <div flex-gt-xs="25"></div>
+ <div flex-gt-xs="50">
+ <h3 translate>Costs</h3>
+ <fims-teller-transaction-cost
+ [transactionAmount]="amountForm.get('amount').value"
+ [transactionCosts]="transactionCosts">
+ </fims-teller-transaction-cost>
+ <div layout="row" layout-margin>
+ <mat-checkbox [(ngModel)]="chargesIncluded" translate>Fees are paid in cash</mat-checkbox>
+ </div>
+ </div>
+ </div>
+ </div>
+ </td-step>
+ <td-step label="{{'Final step' | translate}}" [state]="'complete'">
+ <ng-template td-step-summary>
+ <button mat-raised-button color="primary" [disabled]="!transactionCreated" (click)="confirmTransaction(chargesIncluded)">{{'CONFIRM TRANSACTION' | translate}}</button>
+ <span flex></span>
+ <button mat-button [disabled]="!transactionCreated" (click)="cancelTransaction()">{{'CANCEL TRANSACTION' | translate}}</button>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/teller/customer/transaction/cheque/form.component.ts b/src/app/teller/customer/transaction/cheque/form.component.ts
new file mode 100644
index 0000000..4bc5f91
--- /dev/null
+++ b/src/app/teller/customer/transaction/cheque/form.component.ts
@@ -0,0 +1,160 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild} from '@angular/core';
+import {TdStepComponent} from '@covalent/core';
+import {FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {FimsValidators} from '../../../../common/validator/validators';
+import {TellerTransactionCosts} from '../../../../services/teller/domain/teller-transaction-costs.model';
+import {ProductInstance} from '../../../../services/depositAccount/domain/instance/product-instance.model';
+import {TransactionForm} from '../domain/transaction-form.model';
+import {toMICRIdentifier} from '../../../../services/cheque/domain/mapper/fims-cheque.mapper';
+import {Cheque} from '../../../../services/teller/domain/cheque.model';
+import {MICRResolution} from '../../../../services/cheque/domain/micr-resolution.model';
+import {toShortISOString} from '../../../../services/domain/date.converter';
+
+@Component({
+ selector: 'fims-cheque-transaction-form',
+ templateUrl: './form.component.html'
+})
+export class ChequeTransactionFormComponent implements OnInit, OnChanges {
+
+ chequeForm: FormGroup;
+ amountForm: FormGroup;
+
+ chargesIncluded = true;
+
+ numberFormat = '1.2-2';
+
+ @ViewChild('transactionStep') transactionStep: TdStepComponent;
+ @ViewChild('confirmationStep') confirmationStep: TdStepComponent;
+
+ @Input('productInstances') productInstances: ProductInstance[];
+ @Input('transactionCosts') transactionCosts: TellerTransactionCosts;
+ @Input('transactionCreated') transactionCreated: boolean;
+ @Input('micrResolution') micrResolution: MICRResolution;
+ @Input('micrResolutionError') micrResolutionError: Error;
+ @Input('customerName') customerName: string;
+
+ @Output('onExpandMICR') onExpandMICR = new EventEmitter<string>();
+ @Output('onCreateTransaction') onCreateTransaction = new EventEmitter<TransactionForm>();
+ @Output('onConfirmTransaction') onConfirmTransaction = new EventEmitter<boolean>();
+ @Output('onCancelTransaction') onCancelTransaction = new EventEmitter<void>();
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder) {
+ this.chequeForm = this.formBuilder.group({
+ chequeNumber: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(8), FimsValidators.isNumber]],
+ branchSortCode: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(11)]],
+ accountNumber: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(34)]]
+ });
+
+ this.amountForm = this.formBuilder.group({
+ productInstance: ['', Validators.required],
+ drawee: ['', Validators.required],
+ drawer: ['', Validators.required],
+ payee: [{value: '', disabled: true}, Validators.required],
+ amount: ['', [Validators.required, FimsValidators.greaterThanValue(0)]],
+ dateIssued: ['', [Validators.required]],
+ openCheque: [false],
+ });
+ }
+
+ ngOnInit(): void {
+ this.transactionStep.open();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ const draweeControl = this.amountForm.get('drawee');
+ const drawerControl = this.amountForm.get('drawer');
+
+ if (changes.micrResolution && this.micrResolution) {
+ draweeControl.setValue(this.micrResolution.office);
+ drawerControl.setValue(this.micrResolution.customer);
+ }
+
+ if (changes.micrResolutionError && this.micrResolutionError) {
+ draweeControl.setValue('');
+ drawerControl.setValue('');
+ }
+
+ if (changes.transactionCreated && this.transactionCreated) {
+ this.confirmationStep.open();
+ }
+
+ if (changes.customerName) {
+ this.amountForm.get('payee').setValue(this.customerName);
+ }
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+ validateCheque(): void {
+ const chequeNumber = this.chequeForm.get('chequeNumber').value;
+ const branchSortCode = this.chequeForm.get('branchSortCode').value;
+ const accountNumber = this.chequeForm.get('accountNumber').value;
+ this.onExpandMICR.emit(toMICRIdentifier(chequeNumber, branchSortCode, accountNumber));
+ }
+
+ createTransaction(): void {
+ const productInstance: ProductInstance = this.amountForm.get('productInstance').value;
+
+ const cheque: Cheque = {
+ micr: {
+ chequeNumber: this.chequeForm.get('chequeNumber').value,
+ branchSortCode: this.chequeForm.get('branchSortCode').value,
+ accountNumber: this.chequeForm.get('accountNumber').value
+ },
+ drawee: this.amountForm.get('drawee').value,
+ drawer: this.amountForm.get('drawer').value,
+ payee: this.amountForm.get('payee').value,
+ amount: this.amountForm.get('amount').value,
+ dateIssued: toShortISOString(this.amountForm.get('dateIssued').value),
+ openCheque: this.amountForm.get('openCheque').value
+ };
+
+ const formData: TransactionForm = {
+ productIdentifier: productInstance.productIdentifier,
+ accountIdentifier: productInstance.accountIdentifier,
+ customerIdentifier: productInstance.customerIdentifier,
+ amount: this.amountForm.get('amount').value,
+ cheque
+ };
+
+ this.onCreateTransaction.emit(formData);
+ }
+
+ confirmTransaction(chargesIncluded: boolean): void {
+ this.onConfirmTransaction.emit(chargesIncluded);
+ }
+
+ cancelTransaction(): void {
+ this.onCancelTransaction.emit();
+ }
+
+ get createTransactionDisabled(): boolean {
+ return this.invalid || this.transactionCreated;
+ }
+
+ get invalid(): boolean {
+ return this.chequeForm.invalid || this.amountForm.invalid;
+ }
+
+}
diff --git a/src/app/teller/customer/transaction/components/cost.component.html b/src/app/teller/customer/transaction/components/cost.component.html
new file mode 100644
index 0000000..ed58636
--- /dev/null
+++ b/src/app/teller/customer/transaction/components/cost.component.html
@@ -0,0 +1,55 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<table td-data-table>
+ <thead>
+ <tr td-data-table-column-row>
+ <th td-data-table-column>
+ <span translate>Name</span>
+ </th>
+ <th td-data-table-column>
+ <span translate>Description</span>
+ </th>
+ <th td-data-table-column>
+ <span translate>Amount</span>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr td-data-table-row>
+ <td td-data-table-cell translate>Transaction amount</td>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell>{{transactionAmount}}</td>
+ </tr>
+ <tr td-data-table-row *ngFor="let row of transactionCosts?.charges">
+ <td td-data-table-cell>
+ {{row['name']}}
+ </td>
+ <td td-data-table-cell>
+ {{row['description'] }}
+ </td>
+ <td td-data-table-cell>
+ {{row['amount']}}
+ </td>
+ </tr>
+ <tr td-data-table-row>
+ <td td-data-table-cell translate>Total</td>
+ <td td-data-table-cell></td>
+ <td td-data-table-cell>{{transactionCosts?.totalAmount}}</td>
+ </tr>
+ </tbody>
+</table>
diff --git a/src/app/teller/customer/transaction/components/cost.component.ts b/src/app/teller/customer/transaction/components/cost.component.ts
new file mode 100644
index 0000000..32d262c
--- /dev/null
+++ b/src/app/teller/customer/transaction/components/cost.component.ts
@@ -0,0 +1,32 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, Input} from '@angular/core';
+import {TellerTransactionCosts} from '../../../../services/teller/domain/teller-transaction-costs.model';
+
+@Component({
+ templateUrl: './cost.component.html',
+ selector: 'fims-teller-transaction-cost'
+})
+export class TransactionCostComponent {
+
+ @Input('transactionAmount') transactionAmount: number;
+
+ @Input('transactionCosts') transactionCosts: TellerTransactionCosts;
+
+}
diff --git a/src/app/teller/customer/transaction/deposit/create.form.component.html b/src/app/teller/customer/transaction/deposit/create.form.component.html
new file mode 100644
index 0000000..0abfa29
--- /dev/null
+++ b/src/app/teller/customer/transaction/deposit/create.form.component.html
@@ -0,0 +1,31 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="Teller transaction">
+ <fims-teller-transaction-form
+ #form
+ [productInstances]="productInstances$ | async"
+ [transactionCosts]="transactionCosts$ | async"
+ [transactionCreated]="transactionCreated"
+ [transactionType]="transactionType"
+ [cashdrawLimit]="teller.cashdrawLimit"
+ (onCreateTransaction)="createTransaction($event)"
+ (onConfirmTransaction)="confirmTransaction($event)"
+ (onCancelTransaction)="cancelTransaction()"
+ (onCancel)="cancel()">
+ </fims-teller-transaction-form>
+</fims-layout-card-over>
diff --git a/src/app/teller/customer/transaction/deposit/create.form.component.ts b/src/app/teller/customer/transaction/deposit/create.form.component.ts
new file mode 100644
index 0000000..6af2363
--- /dev/null
+++ b/src/app/teller/customer/transaction/deposit/create.form.component.ts
@@ -0,0 +1,146 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {TellerTransaction, TransactionType} from '../../../../services/teller/domain/teller-transaction.model';
+import {TellerTransactionCosts} from '../../../../services/teller/domain/teller-transaction-costs.model';
+import {CONFIRM_TRANSACTION} from '../../../store/teller.actions';
+import * as fromTeller from '../../../store/index';
+import {TellerStore} from '../../../store/index';
+import * as fromRoot from '../../../../store/index';
+import {DepositAccountService} from '../../../../services/depositAccount/deposit-account.service';
+import {Observable} from 'rxjs/Observable';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Subscription} from 'rxjs/Subscription';
+import {DepositTransactionFormComponent} from './form.component';
+import {ProductInstance} from '../../../../services/depositAccount/domain/instance/product-instance.model';
+import {Teller} from '../../../../services/teller/domain/teller.model';
+import {TransactionForm} from '../domain/transaction-form.model';
+import {TellerTransactionService} from '../../../services/transaction.service';
+
+@Component({
+ templateUrl: './create.form.component.html'
+})
+export class CreateDepositTransactionFormComponent implements OnInit, OnDestroy {
+
+ private authenticatedTellerSubscription: Subscription;
+
+ private usernameSubscription: Subscription;
+
+ private tellerTransactionIdentifier: string;
+
+ private clerk: string;
+
+ @ViewChild('form') form: DepositTransactionFormComponent;
+
+ transactionType: TransactionType;
+
+ productInstances$: Observable<ProductInstance[]>;
+
+ transactionCosts$: Observable<TellerTransactionCosts>;
+
+ teller: Teller;
+
+ transactionCreated: boolean;
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: TellerStore,
+ private depositService: DepositAccountService, private tellerTransactionService: TellerTransactionService) {
+ }
+
+ ngOnInit(): void {
+ const transactionType$ = this.route.queryParams
+ .map(params => params['transactionType'])
+ .do(transactionType => this.transactionType = transactionType);
+
+ const allProductInstances$ = this.store.select(fromTeller.getTellerSelectedCustomer)
+ .switchMap(customer => this.depositService.fetchProductInstances(customer.identifier));
+
+ this.productInstances$ = Observable.combineLatest(
+ transactionType$,
+ allProductInstances$,
+ (type, productInstances) => this.filterProductInstances(type, productInstances)
+ );
+
+ this.authenticatedTellerSubscription = this.store.select(fromTeller.getAuthenticatedTeller)
+ .filter(teller => !!teller)
+ .subscribe(teller => {
+ this.teller = teller;
+ });
+
+ this.usernameSubscription = this.store.select(fromRoot.getUsername)
+ .subscribe(username => this.clerk = username);
+ }
+
+ filterProductInstances(transactionType: string, productInstances: ProductInstance[]): ProductInstance[] {
+ // If open account only show pending accounts otherwise only active
+ const filterByState = transactionType === 'ACCO' ? 'PENDING' : 'ACTIVE';
+ return productInstances
+ .filter(instance => instance.state === filterByState);
+ }
+
+ ngOnDestroy(): void {
+ this.authenticatedTellerSubscription.unsubscribe();
+ this.usernameSubscription.unsubscribe();
+ }
+
+ createTransaction(formData: TransactionForm): void {
+ const transaction: TellerTransaction = {
+ customerIdentifier: formData.customerIdentifier,
+ productIdentifier: formData.productIdentifier,
+ customerAccountIdentifier: formData.accountIdentifier,
+ targetAccountIdentifier: formData.targetAccountIdentifier,
+ amount: formData.amount,
+ clerk: this.clerk,
+ transactionDate: new Date().toISOString(),
+ transactionType: this.transactionType
+ };
+
+ this.transactionCosts$ = this.tellerTransactionService.createTransaction(this.teller.code, transaction)
+ .do(transactionCosts => this.tellerTransactionIdentifier = transactionCosts.tellerTransactionIdentifier)
+ .do(() => this.transactionCreated = true);
+ }
+
+ confirmTransaction(chargesIncluded: boolean): void {
+ this.store.dispatch({
+ type: CONFIRM_TRANSACTION,
+ payload: {
+ tellerCode: this.teller.code,
+ tellerTransactionIdentifier: this.tellerTransactionIdentifier,
+ command: 'CONFIRM',
+ chargesIncluded,
+ activatedRoute: this.route
+ }
+ });
+ }
+
+ cancelTransaction(): void {
+ this.store.dispatch({
+ type: CONFIRM_TRANSACTION,
+ payload: {
+ tellerCode: this.teller.code,
+ tellerTransactionIdentifier: this.tellerTransactionIdentifier,
+ command: 'CANCEL',
+ activatedRoute: this.route
+ }
+ });
+ }
+
+ cancel(): void {
+ this.router.navigate(['../../'], {relativeTo: this.route});
+ }
+}
diff --git a/src/app/teller/customer/transaction/deposit/form.component.html b/src/app/teller/customer/transaction/deposit/form.component.html
new file mode 100644
index 0000000..f68893f
--- /dev/null
+++ b/src/app/teller/customer/transaction/deposit/form.component.html
@@ -0,0 +1,71 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #transactionStep label="{{'Transaction' | translate}}"
+ [state]="form.valid ? 'complete' : form.pristine ? 'none' : 'required'" [disabled]="transactionCreated">
+ <form [formGroup]="form" layout="column">
+ <mat-form-field layout-margin>
+ <mat-select formControlName="productInstance" placeholder="{{ 'Select account' | translate }}">
+ <mat-option *ngFor="let instance of productInstances" [value]="instance">
+ {{instance.accountIdentifier}}({{instance.productIdentifier}})
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ <fims-account-select title="{{'Target account' | translate}}" formControlName="targetAccountIdentifier" *ngIf="enableTargetAccount">
+ <ng-container *ngIf="!form.get('targetAccountIdentifier').pristine && form.get('targetAccountIdentifier').hasError('required')" translate>
+ Required
+ </ng-container>
+ <ng-container *ngIf="form.get('targetAccountIdentifier').hasError('invalidAccount')" translate>
+ Invalid account
+ </ng-container>
+ </fims-account-select>
+ <fims-text-input type="number" [form]="form" controlName="amount" placeholder="{{'Amount' | translate}}"></fims-text-input>
+ <span *ngIf="checkCashdrawLimit" class="text-md" translate>Cashdraw limit is: {{cashdrawLimit | number:numberFormat}}</span>
+ <span *ngIf="checkBalanceLimit" class="text-md" translate>Balance limit is: {{balanceLimit | number:numberFormat}}</span>
+ </form>
+ <ng-template td-step-actions>
+ <button mat-raised-button color="primary" (click)="createTransaction()" [disabled]="createTransactionDisabled">{{'CREATE TRANSACTION' | translate}}</button>
+ <span flex></span>
+ <button mat-button (click)="cancel()" [disabled]="transactionCreated">{{'CANCEL' | translate}}</button>
+ </ng-template>
+ </td-step>
+ <td-step #confirmationStep label="{{'Confirmation' | translate}}">
+ <div layout-gt-xs="row" layout-align="center center">
+ <div layout-gt-xs="row" flex-gt-xs="90" layout-margin>
+ <div flex-gt-xs="25"></div>
+ <div flex-gt-xs="50">
+ <h3 translate>Costs</h3>
+ <fims-teller-transaction-cost
+ [transactionAmount]="form.get('amount').value"
+ [transactionCosts]="transactionCosts">
+ </fims-teller-transaction-cost>
+ <div layout="row" layout-margin>
+ <mat-checkbox [(ngModel)]="chargesIncluded" [disabled]="chargesIncludedDisabled" translate>Fees are paid in cash</mat-checkbox>
+ </div>
+ </div>
+ </div>
+ </div>
+ </td-step>
+ <td-step label="{{'Final step' | translate}}" [state]="'complete'">
+ <ng-template td-step-summary>
+ <button mat-raised-button color="primary" [disabled]="!transactionCreated" (click)="confirmTransaction(chargesIncluded)">{{'CONFIRM TRANSACTION' | translate}}</button>
+ <span flex></span>
+ <button mat-button [disabled]="!transactionCreated" (click)="cancelTransaction()">{{'CANCEL TRANSACTION' | translate}}</button>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/teller/customer/transaction/deposit/form.component.spec.ts b/src/app/teller/customer/transaction/deposit/form.component.spec.ts
new file mode 100644
index 0000000..e77c702
--- /dev/null
+++ b/src/app/teller/customer/transaction/deposit/form.component.spec.ts
@@ -0,0 +1,217 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {FimsSharedModule} from '../../../../common/common.module';
+import {CovalentDataTableModule, CovalentStepsModule} from '@covalent/core';
+import {MatButtonModule, MatCheckboxModule, MatInputModule, MatOptionModule, MatSelectModule} from '@angular/material';
+import {Component, DebugElement, ViewChild} from '@angular/core';
+import {DepositTransactionFormComponent} from './form.component';
+import {By} from '@angular/platform-browser';
+import {ProductInstance} from '../../../../services/depositAccount/domain/instance/product-instance.model';
+import {TranslateModule} from '@ngx-translate/core';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {AccountingService} from '../../../../services/accounting/accounting.service';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {setValueByCssSelector} from '../../../../common/testing/input-fields';
+import {TransactionCostComponent} from '../components/cost.component';
+import {clickOption} from '../../../../common/testing/select-fields';
+
+describe('Test transaction form', () => {
+
+ const productInstances: ProductInstance[] = [{
+ customerIdentifier: 'test',
+ accountIdentifier: 'test',
+ productIdentifier: '',
+ balance: 500
+ }];
+
+ let fixture: ComponentFixture<TestComponent>;
+ let component: TestComponent;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ NoopAnimationsModule,
+ TranslateModule.forRoot(),
+ FimsSharedModule,
+ FormsModule,
+ ReactiveFormsModule,
+ MatInputModule,
+ MatButtonModule,
+ MatSelectModule,
+ MatOptionModule,
+ MatCheckboxModule,
+ CovalentStepsModule,
+ CovalentDataTableModule
+ ],
+ providers: [
+ {
+ provide: AccountingService,
+ useValue: jasmine.createSpyObj('accountingService', ['findAccount'])
+ }
+ ],
+ declarations: [
+ TransactionCostComponent,
+ DepositTransactionFormComponent,
+ TestComponent
+ ]
+ });
+
+ });
+
+ function setup(cashdrawLimit: number, transactionType: string): any {
+ fixture = TestBed.createComponent(TestComponent);
+ component = fixture.componentInstance;
+
+ component.transactionType = transactionType;
+ component.productInstances = productInstances;
+ component.cashdrawLimit = cashdrawLimit;
+
+ fixture.detectChanges();
+ }
+
+ function transactionButton(): DebugElement {
+ const element = fixture.debugElement.query(By.css('td-steps > div:nth-child(1) > td-step-body > div > ' +
+ 'div.td-step-body > div > div.td-step-actions > button.mat-raised-button.mat-primary'));
+ return element;
+ }
+
+ function setAmount(value: string): void {
+ setValueByCssSelector(fixture, '#amount', value);
+ }
+
+ describe('test if create transaction is enabled', () => {
+
+ beforeEach(() => {
+ setup(1000, 'ACCC');
+
+ clickOption(fixture, 0);
+ });
+
+ it('when amount matches balance limit', () => {
+ const productInstance = component.form.form.get('productInstance').value;
+
+ expect(productInstance).toEqual(productInstances[0]);
+
+ setAmount('500');
+
+ const button: DebugElement = transactionButton();
+
+ expect(button.properties['disabled']).toBeFalsy('Button should be enabled');
+ });
+
+ it('when amount is 0 and type ACCC', () => {
+ const productInstance = component.form.form.get('productInstance').value;
+
+ setAmount('0');
+
+ expect(productInstance).toEqual(productInstances[0]);
+
+ const button: DebugElement = transactionButton();
+
+ expect(button.properties['disabled']).toBeFalsy('Button should be enabled');
+ });
+ });
+
+ describe('test if create transaction is disabled', () => {
+
+ describe('and type is ACCC', () => {
+ beforeEach(() => {
+ setup(1000, 'ACCC');
+
+ clickOption(fixture, 0);
+ });
+
+ it('when amount exeeds balance', () => {
+ const productInstance = component.form.form.get('productInstance').value;
+
+ expect(productInstance).toEqual(productInstances[0]);
+
+ setAmount('501');
+
+ const button: DebugElement = transactionButton();
+
+ expect(button.properties['disabled']).toBeTruthy('Button should be disabled');
+ });
+
+ it('when amount exeeds withdrawal limit', () => {
+ const productInstance = component.form.form.get('productInstance').value;
+
+ expect(productInstance).toEqual(productInstances[0]);
+
+ setAmount('1001');
+
+ const button: DebugElement = transactionButton();
+
+ expect(button.properties['disabled']).toBeTruthy('Button should be disabled');
+ });
+
+ it('when no amount is given', () => {
+ const productInstance = component.form.form.get('productInstance').value;
+
+ expect(productInstance).toEqual(productInstances[0]);
+
+ const button: DebugElement = transactionButton();
+
+ expect(button.properties['disabled']).toBeTruthy('Button should be disabled');
+ });
+ });
+
+
+ describe('and type is not ACCC', () => {
+ beforeEach(() => {
+ setup(1000, 'ACCO');
+
+ clickOption(fixture, 0);
+ });
+
+ it('when amount is 0', () => {
+ const productInstance = component.form.form.get('productInstance').value;
+
+ setAmount('0');
+
+ expect(productInstance).toEqual(productInstances[0]);
+
+ const button: DebugElement = transactionButton();
+
+ expect(button.properties['disabled']).toBeTruthy('Button should be disabled');
+ });
+ });
+
+ });
+});
+
+@Component({
+ template: `<fims-teller-transaction-form #form
+ [productInstances]="productInstances"
+ [cashdrawLimit]="cashdrawLimit"
+ [transactionType]="transactionType">
+ </fims-teller-transaction-form>`
+})
+class TestComponent {
+
+ productInstances: ProductInstance[];
+
+ cashdrawLimit = 1000;
+
+ transactionType = 'ACCC';
+
+ @ViewChild('form') form: DepositTransactionFormComponent;
+
+}
diff --git a/src/app/teller/customer/transaction/deposit/form.component.ts b/src/app/teller/customer/transaction/deposit/form.component.ts
new file mode 100644
index 0000000..11f7075
--- /dev/null
+++ b/src/app/teller/customer/transaction/deposit/form.component.ts
@@ -0,0 +1,195 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
+import {TdStepComponent} from '@covalent/core';
+import {FormBuilder, FormControl, FormGroup, ValidatorFn, Validators} from '@angular/forms';
+import {FimsValidators} from '../../../../common/validator/validators';
+import {TellerTransactionCosts} from '../../../../services/teller/domain/teller-transaction-costs.model';
+import {ProductInstance} from '../../../../services/depositAccount/domain/instance/product-instance.model';
+import {accountExists} from '../../../../common/validator/account-exists.validator';
+import {AccountingService} from '../../../../services/accounting/accounting.service';
+import {TransactionType} from '../../../../services/teller/domain/teller-transaction.model';
+import {TransactionForm} from '../domain/transaction-form.model';
+
+// List of types to check withdrawal limit
+const withdrawalCheckTypes: TransactionType[] = ['ACCC', 'CWDL'];
+
+// List of types to check balance limit
+const balanceCheckTypes: TransactionType[] = ['ACCT', 'ACCC', 'CWDL'];
+
+@Component({
+ selector: 'fims-teller-transaction-form',
+ templateUrl: './form.component.html'
+})
+export class DepositTransactionFormComponent implements OnInit {
+
+ form: FormGroup;
+
+ private _transactionCreated: boolean;
+
+ private _transactionType: TransactionType;
+
+ chargesIncluded = true;
+
+ chargesIncludedDisabled = false;
+
+ enableTargetAccount: boolean;
+
+ numberFormat = '1.2-2';
+
+ checkCashdrawLimit: boolean;
+
+ checkBalanceLimit: boolean;
+
+ balanceLimit: number;
+
+ @ViewChild('transactionStep') transactionStep: TdStepComponent;
+
+ @ViewChild('confirmationStep') confirmationStep: TdStepComponent;
+
+ @Input('productInstances') productInstances: ProductInstance[];
+
+ @Input('transactionCosts') transactionCosts: TellerTransactionCosts;
+
+ @Input('transactionCreated')
+ set transactionCreated(transactionCreated: boolean) {
+ this._transactionCreated = transactionCreated;
+ if (transactionCreated) {
+ this.confirmationStep.open();
+ }
+ };
+
+ @Input('error') error: string;
+
+ @Input('transactionType')
+ set transactionType(transactionType: TransactionType) {
+ this._transactionType = transactionType;
+
+ if (transactionType === 'ACCT') {
+ this.enableTargetAccount = true;
+ }
+
+ if (transactionType === 'ACCO') {
+ this.chargesIncludedDisabled = true;
+ }
+
+ this.checkCashdrawLimit = this.hasType(withdrawalCheckTypes, transactionType);
+ this.checkBalanceLimit = this.hasType(balanceCheckTypes, transactionType);
+ }
+
+ @Input('cashdrawLimit') cashdrawLimit: number;
+
+ @Output('onCreateTransaction') onCreateTransaction = new EventEmitter<TransactionForm>();
+
+ @Output('onConfirmTransaction') onConfirmTransaction = new EventEmitter<boolean>();
+
+ @Output('onCancelTransaction') onCancelTransaction = new EventEmitter<void>();
+
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ constructor(private formBuilder: FormBuilder, private accountingService: AccountingService) {
+ }
+
+ ngOnInit(): void {
+ this.form = this.formBuilder.group({
+ productInstance: ['', Validators.required],
+ amount: ['']
+ });
+
+ if (this.enableTargetAccount) {
+ this.form.addControl('targetAccountIdentifier', new FormControl('', [Validators.required], accountExists(this.accountingService)));
+ }
+
+ this.form.get('productInstance').valueChanges
+ .subscribe(productInstance => this.toggleProductInstance(productInstance));
+
+ this.transactionStep.open();
+ }
+
+ private toggleProductInstance(productInstance: ProductInstance): void {
+ const amountValidators: ValidatorFn[] = [Validators.required];
+
+ const valueValidator: ValidatorFn = this._transactionType === 'ACCC' ? FimsValidators.minValue(0) : FimsValidators.greaterThanValue(0);
+ amountValidators.push(valueValidator);
+
+ this.balanceLimit = productInstance.balance;
+
+ const maxValue = this.getAmountMaxValue(productInstance);
+
+ if (maxValue !== undefined) {
+ amountValidators.push(FimsValidators.maxValue(maxValue));
+ }
+
+ const amountControl = this.form.get('amount') as FormControl;
+
+ amountControl.setValidators(amountValidators);
+
+ amountControl.updateValueAndValidity();
+ }
+
+ private getAmountMaxValue(productInstance: ProductInstance): number {
+ if (this.checkBalanceLimit && this.checkCashdrawLimit) {
+ return Math.min(this.cashdrawLimit, productInstance.balance);
+ }
+
+ if (this.checkBalanceLimit && !this.checkCashdrawLimit) {
+ return productInstance.balance;
+ }
+ }
+
+ private hasType(types: TransactionType[], type: TransactionType): boolean {
+ return types.indexOf(type) > -1;
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+ createTransaction(): void {
+ const productInstance: ProductInstance = this.form.get('productInstance').value;
+ const targetAccountIdentifierControl: FormControl = this.form.get('targetAccountIdentifier') as FormControl;
+
+ const formData: TransactionForm = {
+ productIdentifier: productInstance.productIdentifier,
+ accountIdentifier: productInstance.accountIdentifier,
+ customerIdentifier: productInstance.customerIdentifier,
+ targetAccountIdentifier: targetAccountIdentifierControl ? targetAccountIdentifierControl.value : undefined,
+ amount: this.form.get('amount').value
+ };
+
+ this.onCreateTransaction.emit(formData);
+ }
+
+ confirmTransaction(chargesIncluded: boolean): void {
+ this.onConfirmTransaction.emit(chargesIncluded);
+ }
+
+ cancelTransaction(): void {
+ this.onCancelTransaction.emit();
+ }
+
+ get transactionCreated(): boolean {
+ return this._transactionCreated;
+ }
+
+ get createTransactionDisabled(): boolean {
+ return this.form.invalid || this.transactionCreated;
+ }
+
+}
diff --git a/src/app/teller/customer/transaction/domain/transaction-form.model.ts b/src/app/teller/customer/transaction/domain/transaction-form.model.ts
new file mode 100644
index 0000000..deffc7b
--- /dev/null
+++ b/src/app/teller/customer/transaction/domain/transaction-form.model.ts
@@ -0,0 +1,29 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Cheque} from '../../../../services/teller/domain/cheque.model';
+
+export interface TransactionForm {
+ customerIdentifier: string;
+ productIdentifier: string;
+ productCaseIdentifier?: string;
+ accountIdentifier: string;
+ targetAccountIdentifier?: string;
+ amount: number;
+ cheque?: Cheque;
+}
diff --git a/src/app/teller/customer/transaction/loan/create.form.component.html b/src/app/teller/customer/transaction/loan/create.form.component.html
new file mode 100644
index 0000000..f6bb58f
--- /dev/null
+++ b/src/app/teller/customer/transaction/loan/create.form.component.html
@@ -0,0 +1,30 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<fims-layout-card-over title="{{'Teller transaction' | translate}}">
+ <fims-loan-transaction-form
+ [transactionCreated]="transactionCreated"
+ [caseInstances]="caseInstances$ | async"
+ [transactionCosts]="transactionCosts$ | async"
+ [paymentHint]="paymentHint"
+ (onCreateTransaction)="createTransaction($event)"
+ (onConfirmTransaction)="confirmTransaction($event)"
+ (onCancelTransaction)="cancelTransaction()"
+ (onCaseSelected)="caseSelected($event)"
+ (onCancel)="cancel()">
+ </fims-loan-transaction-form>
+</fims-layout-card-over>
diff --git a/src/app/teller/customer/transaction/loan/create.form.component.ts b/src/app/teller/customer/transaction/loan/create.form.component.ts
new file mode 100644
index 0000000..a1c6886
--- /dev/null
+++ b/src/app/teller/customer/transaction/loan/create.form.component.ts
@@ -0,0 +1,140 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {TellerTransaction, TransactionType} from '../../../../services/teller/domain/teller-transaction.model';
+import {TellerTransactionCosts} from '../../../../services/teller/domain/teller-transaction-costs.model';
+import {CONFIRM_TRANSACTION} from '../../../store/teller.actions';
+import * as fromTeller from '../../../store/index';
+import {TellerStore} from '../../../store/index';
+import * as fromRoot from '../../../../store/index';
+import {Observable} from 'rxjs/Observable';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Subscription} from 'rxjs/Subscription';
+import {DepositTransactionFormComponent} from '../deposit/form.component';
+import {Teller} from '../../../../services/teller/domain/teller.model';
+import {PortfolioService} from '../../../../services/portfolio/portfolio.service';
+import {TransactionForm} from '../domain/transaction-form.model';
+import {FimsCase} from '../../../../services/portfolio/domain/fims-case.model';
+import {TellerTransactionService} from '../../../services/transaction.service';
+
+@Component({
+ templateUrl: './create.form.component.html'
+})
+export class CreateLoanTransactionFormComponent implements OnInit, OnDestroy {
+
+ private authenticatedTellerSubscription: Subscription;
+
+ private usernameSubscription: Subscription;
+
+ private tellerTransactionIdentifier: string;
+
+ private clerk: string;
+
+ private transactionType: TransactionType;
+
+ @ViewChild('form') form: DepositTransactionFormComponent;
+
+ caseInstances$: Observable<FimsCase[]>;
+
+ transactionCosts$: Observable<TellerTransactionCosts>;
+
+ paymentHint: string;
+
+ teller: Teller;
+
+ transactionCreated: boolean;
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: TellerStore,
+ private portfolioService: PortfolioService, private tellerTransactionService: TellerTransactionService) {
+ }
+
+ ngOnInit(): void {
+ this.route.queryParams.subscribe(params => this.transactionType = params['transactionType']);
+
+ this.caseInstances$ = this.store.select(fromTeller.getTellerSelectedCustomer)
+ .switchMap(customer => this.portfolioService.getAllCasesForCustomer(customer.identifier))
+ .map(casePage => casePage.elements.filter(element => element.currentState === 'ACTIVE'));
+
+ this.authenticatedTellerSubscription = this.store.select(fromTeller.getAuthenticatedTeller)
+ .filter(teller => !!teller)
+ .subscribe(teller => {
+ this.teller = teller;
+ });
+
+ this.usernameSubscription = this.store.select(fromRoot.getUsername)
+ .subscribe(username => this.clerk = username);
+ }
+
+ ngOnDestroy(): void {
+ this.authenticatedTellerSubscription.unsubscribe();
+ this.usernameSubscription.unsubscribe();
+ }
+
+ createTransaction(formData: TransactionForm): void {
+ const transaction: TellerTransaction = {
+ customerIdentifier: formData.customerIdentifier,
+ productIdentifier: formData.productIdentifier,
+ productCaseIdentifier: formData.productCaseIdentifier,
+ customerAccountIdentifier: formData.accountIdentifier,
+ amount: formData.amount,
+ clerk: this.clerk,
+ transactionDate: new Date().toISOString(),
+ transactionType: this.transactionType
+ };
+
+ this.transactionCosts$ = this.tellerTransactionService.createTransaction(this.teller.code, transaction)
+ .do(transactionCosts => this.tellerTransactionIdentifier = transactionCosts.tellerTransactionIdentifier)
+ .do(() => this.transactionCreated = true);
+ }
+
+ confirmTransaction(chargesIncluded: boolean): void {
+ this.store.dispatch({
+ type: CONFIRM_TRANSACTION,
+ payload: {
+ tellerCode: this.teller.code,
+ tellerTransactionIdentifier: this.tellerTransactionIdentifier,
+ command: 'CONFIRM',
+ chargesIncluded,
+ activatedRoute: this.route
+ }
+ });
+ }
+
+ cancelTransaction(): void {
+ this.store.dispatch({
+ type: CONFIRM_TRANSACTION,
+ payload: {
+ tellerCode: this.teller.code,
+ tellerTransactionIdentifier: this.tellerTransactionIdentifier,
+ command: 'CANCEL',
+ activatedRoute: this.route
+ }
+ });
+ }
+
+ caseSelected(caseInstance: FimsCase): void {
+ this.portfolioService.getCostComponentsForAction(caseInstance.productIdentifier, caseInstance.identifier, 'ACCEPT_PAYMENT')
+ .map(payment => payment.balanceAdjustments.ey * -1)
+ .subscribe(paymentHint => this.paymentHint = paymentHint.toString());
+ }
+
+ cancel(): void {
+ this.router.navigate(['../../'], {relativeTo: this.route});
+ }
+}
diff --git a/src/app/teller/customer/transaction/loan/form.component.html b/src/app/teller/customer/transaction/loan/form.component.html
new file mode 100644
index 0000000..c0f0e2e
--- /dev/null
+++ b/src/app/teller/customer/transaction/loan/form.component.html
@@ -0,0 +1,62 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<td-steps mode="'vertical'">
+ <td-step #transactionStep label="{{'Transaction' | translate}}"
+ [state]="form.valid ? 'complete' : form.pristine ? 'none' : 'required'" [disabled]="transactionCreated">
+ <form [formGroup]="form" layout="column">
+ <mat-form-field layout-margin>
+ <mat-select formControlName="caseInstance" placeholder="{{ 'Select account' | translate }}">
+ <mat-option *ngFor="let instance of caseInstances" [value]="instance">
+ {{instance.customerLoanAccountIdentifier}}({{instance.productIdentifier}})
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ <fims-text-input type="number" [form]="form" controlName="amount" placeholder="{{'Repayment amount' | translate}}"></fims-text-input>
+ <p>Expected payment: {{paymentHint}}</p>
+ </form>
+ <ng-template td-step-actions>
+ <button mat-raised-button color="primary" (click)="createTransaction()" [disabled]="createTransactionDisabled">{{'CREATE TRANSACTION' | translate}}</button>
+ <span flex></span>
+ <button mat-button (click)="cancel()" [disabled]="transactionCreated">{{'CANCEL' | translate}}</button>
+ </ng-template>
+ </td-step>
+ <td-step #confirmationStep label="{{'Confirmation' | translate}}">
+ <div layout-gt-xs="row" layout-align="center center">
+ <div layout-gt-xs="row" flex-gt-xs="90" layout-margin>
+ <div flex-gt-xs="25"></div>
+ <div flex-gt-xs="50">
+ <h3 translate>Costs</h3>
+ <fims-teller-transaction-cost
+ [transactionAmount]="form.get('amount').value"
+ [transactionCosts]="transactionCosts">
+ </fims-teller-transaction-cost>
+ <div layout="row" layout-margin>
+ <mat-checkbox [(ngModel)]="chargesIncluded" [disabled]="true" translate>Fees are paid in cash</mat-checkbox>
+ </div>
+ </div>
+ </div>
+ </div>
+ </td-step>
+ <td-step label="{{'Final step' | translate}}" [state]="'complete'">
+ <ng-template td-step-summary>
+ <button mat-raised-button color="primary" [disabled]="!transactionCreated" (click)="confirmTransaction(chargesIncluded)">{{'CONFIRM TRANSACTION' | translate}}</button>
+ <span flex></span>
+ <button mat-button [disabled]="!transactionCreated" (click)="cancelTransaction()">{{'CANCEL TRANSACTION' | translate}}</button>
+ </ng-template>
+ </td-step>
+</td-steps>
diff --git a/src/app/teller/customer/transaction/loan/form.component.ts b/src/app/teller/customer/transaction/loan/form.component.ts
new file mode 100644
index 0000000..1418585
--- /dev/null
+++ b/src/app/teller/customer/transaction/loan/form.component.ts
@@ -0,0 +1,106 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
+import {TransactionForm} from '../domain/transaction-form.model';
+import {FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {FimsValidators} from '../../../../common/validator/validators';
+import {FimsCase} from '../../../../services/portfolio/domain/fims-case.model';
+import {TellerTransactionCosts} from '../../../../services/teller/domain/teller-transaction-costs.model';
+import {TdStepComponent} from '@covalent/core';
+
+@Component({
+ selector: 'fims-loan-transaction-form',
+ templateUrl: './form.component.html'
+})
+export class LoanTransactionFormComponent implements OnInit {
+
+ private _transactionCreated: boolean;
+
+ chargesIncluded = true;
+
+ form: FormGroup;
+
+ @Input('caseInstances') caseInstances: FimsCase[];
+ @Input('transactionCosts') transactionCosts: TellerTransactionCosts;
+ @Input('transactionCreated') set transactionCreated(transactionCreated: boolean) {
+ this._transactionCreated = transactionCreated;
+ if (transactionCreated) {
+ this.confirmationStep.open();
+ }
+ };
+
+ @Input('paymentHint') paymentHint: string;
+
+ @Output('onCreateTransaction') onCreateTransaction = new EventEmitter<TransactionForm>();
+ @Output('onConfirmTransaction') onConfirmTransaction = new EventEmitter<boolean>();
+ @Output('onCancelTransaction') onCancelTransaction = new EventEmitter<void>();
+ @Output('onCaseSelected') onCaseSelected = new EventEmitter<FimsCase>();
+ @Output('onCancel') onCancel = new EventEmitter<void>();
+
+ @ViewChild('transactionStep') transactionStep: TdStepComponent;
+ @ViewChild('confirmationStep') confirmationStep: TdStepComponent;
+
+ constructor(private formBuilder: FormBuilder) {}
+
+ ngOnInit(): void {
+ this.form = this.formBuilder.group({
+ caseInstance: ['', Validators.required],
+ amount: ['', [Validators.required, FimsValidators.greaterThanValue(0)]],
+ });
+
+ this.form.get('caseInstance').valueChanges
+ .subscribe(caseInstance => this.onCaseSelected.emit(caseInstance));
+
+ this.transactionStep.open();
+ }
+
+ createTransaction(): void {
+ const fimsCase: FimsCase = this.form.get('caseInstance').value;
+
+ const formData: TransactionForm = {
+ productIdentifier: fimsCase.productIdentifier,
+ productCaseIdentifier: fimsCase.identifier,
+ accountIdentifier: fimsCase.customerLoanAccountIdentifier,
+ customerIdentifier: fimsCase.parameters.customerIdentifier,
+ amount: this.form.get('amount').value
+ };
+
+ this.onCreateTransaction.emit(formData);
+ }
+
+ confirmTransaction(chargesIncluded: boolean): void {
+ this.onConfirmTransaction.emit(chargesIncluded);
+ }
+
+ cancelTransaction(): void {
+ this.onCancelTransaction.emit();
+ }
+
+ cancel(): void {
+ this.onCancel.emit();
+ }
+
+ get transactionCreated(): boolean {
+ return this._transactionCreated;
+ }
+
+ get createTransactionDisabled(): boolean {
+ return this.form.invalid || this.transactionCreated;
+ }
+}
diff --git a/src/app/teller/services/available-actions.service.spec.ts b/src/app/teller/services/available-actions.service.spec.ts
new file mode 100644
index 0000000..ad86e0b
--- /dev/null
+++ b/src/app/teller/services/available-actions.service.spec.ts
@@ -0,0 +1,178 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {AvailableActionService} from './available-actions.service';
+import {DepositAccountService} from '../../services/depositAccount/deposit-account.service';
+import {Observable} from 'rxjs/Observable';
+import {PortfolioService} from '../../services/portfolio/portfolio.service';
+import {fakeAsync, TestBed, tick} from '@angular/core/testing';
+import {AvailableTransactionType} from '../../services/depositAccount/domain/instance/available-transaction-type.model';
+import {FimsCasePage} from '../../services/portfolio/domain/fims-case-page.model';
+import {FimsCase} from '../../services/portfolio/domain/fims-case.model';
+import {CaseState} from '../../services/portfolio/domain/case-state.model';
+
+describe('AvailableActionService', () => {
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ AvailableActionService,
+ {
+ provide: DepositAccountService,
+ useValue: jasmine.createSpyObj('depositService', ['fetchPossibleTransactionTypes'])
+ },
+ {
+ provide: PortfolioService,
+ useValue: jasmine.createSpyObj('portfolioService', ['getAllCasesForCustomer'])
+ }
+ ]
+ });
+ });
+
+ function setupDeposit(transactionTypes: AvailableTransactionType[]) {
+ const depositService = TestBed.get(DepositAccountService);
+ depositService.fetchPossibleTransactionTypes.and.returnValue(Observable.of(transactionTypes));
+ }
+
+ function setupPortfolio(cases: FimsCasePage) {
+ const portfolioService = TestBed.get(PortfolioService);
+ portfolioService.getAllCasesForCustomer.and.returnValue(Observable.of(cases));
+ }
+
+ function mockCase(currentState: CaseState): FimsCase {
+ return {
+ identifier: 'test',
+ productIdentifier: 'test',
+ currentState,
+ interest: 1,
+ parameters: null,
+ depositAccountIdentifier: 'test'
+ };
+ }
+
+ it('should merge deposit, loan actions', fakeAsync(() => {
+ setupDeposit([
+ { transactionType: 'ACCC' }
+ ]);
+
+ setupPortfolio({
+ elements: [mockCase('ACTIVE')],
+ totalElements: 1,
+ totalPages: 1
+ });
+
+ const actionService = TestBed.get(AvailableActionService);
+
+ let result = null;
+
+ actionService.getAvailableActions('test').subscribe(_result => result = _result);
+
+ tick();
+
+ // 1 deposit, 1 case
+ expect(result.length).toBe(2);
+ }));
+
+ it('output deposit actions when deposit actions found', fakeAsync(() => {
+ setupDeposit([
+ { transactionType: 'ACCC' },
+ { transactionType: 'CWDL' }
+ ]);
+
+ const actionService = TestBed.get(AvailableActionService);
+
+ let result = null;
+
+ actionService.getAvailableDepositActions('test').subscribe(_result => result = _result);
+
+ tick();
+
+ expect(result.length).toBe(2);
+ }));
+
+ it('not output any deposit actions when no deposit actions found', fakeAsync(() => {
+ setupDeposit([]);
+
+ const actionService = TestBed.get(AvailableActionService);
+
+ let result = null;
+
+ actionService.getAvailableDepositActions('test').subscribe(_result => result = _result);
+
+ tick();
+
+ expect(result).toEqual([]);
+ }));
+
+ it('should output actions when active cases found', fakeAsync(() => {
+ setupPortfolio({
+ elements: [mockCase('ACTIVE')],
+ totalElements: 1,
+ totalPages: 1
+ });
+
+ const actionService = TestBed.get(AvailableActionService);
+
+ let result = null;
+
+ actionService.getAvailableLoanActions('test').subscribe(_result => result = _result);
+
+ tick();
+
+ expect(result.length).toEqual(1);
+ }));
+
+ describe('should not output any loan actions', () => {
+ it('when no cases found', fakeAsync(() => {
+ setupPortfolio({
+ elements: [],
+ totalElements: 0,
+ totalPages: 0
+ });
+
+ const actionService = TestBed.get(AvailableActionService);
+
+ let result = null;
+
+ actionService.getAvailableLoanActions('test').subscribe(_result => result = _result);
+
+ tick();
+
+ expect(result).toEqual([]);
+ }));
+
+ it('when no active cases found', fakeAsync(() => {
+ setupPortfolio({
+ elements: [mockCase('PENDING')],
+ totalElements: 1,
+ totalPages: 1
+ });
+
+ const actionService = TestBed.get(AvailableActionService);
+
+ let result = null;
+
+ actionService.getAvailableLoanActions('test').subscribe(_result => result = _result);
+
+ tick();
+
+ expect(result).toEqual([]);
+ }));
+ });
+
+});
diff --git a/src/app/teller/services/available-actions.service.ts b/src/app/teller/services/available-actions.service.ts
new file mode 100644
index 0000000..087e1ef
--- /dev/null
+++ b/src/app/teller/services/available-actions.service.ts
@@ -0,0 +1,90 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {DepositAccountService} from '../../services/depositAccount/deposit-account.service';
+import {Observable} from 'rxjs/Observable';
+import {TransactionType} from '../../services/teller/domain/teller-transaction.model';
+import {PortfolioService} from '../../services/portfolio/portfolio.service';
+import {FetchRequest} from '../../services/domain/paging/fetch-request.model';
+import {FimsCase} from '../../services/portfolio/domain/fims-case.model';
+
+export interface Action {
+ transactionType: TransactionType;
+ icon: string;
+ title: string;
+ relativeLink: string;
+}
+
+const depositActions: Action[] = [
+ { transactionType: 'ACCO', icon: 'create', title: 'Open account', relativeLink: 'transaction/deposit' },
+ { transactionType: 'ACCC', icon: 'close', title: 'Close account', relativeLink: 'transaction/deposit' },
+ { transactionType: 'ACCT', icon: 'swap_horiz', title: 'Account transfer', relativeLink: 'transaction/deposit'},
+ { transactionType: 'CDPT', icon: 'arrow_forward', title: 'Cash deposit', relativeLink: 'transaction/deposit'},
+ { transactionType: 'CWDL', icon: 'arrow_back', title: 'Cash withdrawal', relativeLink: 'transaction/deposit'},
+ { transactionType: 'CCHQ', icon: 'import_contacts', title: 'Cash cheque', relativeLink: 'transaction/cheque'}
+];
+
+const loanActions: Action[] = [
+ { transactionType: 'PPAY', icon: 'arrow_forward', title: 'Repay loan', relativeLink: 'transaction/loan' }
+];
+
+@Injectable()
+export class AvailableActionService {
+
+ constructor(private depositService: DepositAccountService, private portfolioService: PortfolioService) {}
+
+ getAvailableActions(customerIdentifier: string): Observable<Action[]> {
+ const depositActions$ = this.getAvailableDepositActions(customerIdentifier);
+ const loanActions$ = this.getAvailableLoanActions(customerIdentifier);
+ return Observable.combineLatest(
+ depositActions$,
+ loanActions$,
+ (availableDepositActions, availableLoanActions) =>
+ availableDepositActions.concat(availableLoanActions)
+ );
+ }
+
+ getAvailableDepositActions(customerIdentifier: string): Observable<Action[]> {
+ return this.depositService.fetchPossibleTransactionTypes(customerIdentifier)
+ .map(types => depositActions.filter(action =>
+ !!types.find(type => action.transactionType === type.transactionType)
+ ));
+ }
+
+ getAvailableLoanActions(customerIdentifier: string): Observable<Action[]> {
+ return this.hasActivateLoans(customerIdentifier)
+ .map(hasLoanProducts => hasLoanProducts ? loanActions : []);
+ }
+
+ private hasActivateLoans(customerIdentifier: string): Observable<boolean> {
+ const fetchRequest: FetchRequest = {
+ page: {
+ pageIndex: 0,
+ size: 100
+ }
+ };
+ return this.portfolioService.getAllCasesForCustomer(customerIdentifier, fetchRequest)
+ .map(page => page.totalElements > 0 && this.hasActiveLoan(page.elements));
+ }
+
+ private hasActiveLoan(cases: FimsCase[]): boolean {
+ return !!cases.find(element => element.currentState === 'ACTIVE');
+ }
+
+}
diff --git a/src/app/teller/services/transaction.service.ts b/src/app/teller/services/transaction.service.ts
new file mode 100644
index 0000000..aaafe39
--- /dev/null
+++ b/src/app/teller/services/transaction.service.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {NotificationService, NotificationType} from '../../services/notification/notification.service';
+import {TellerService} from '../../services/teller/teller-service';
+import {TellerTransaction} from '../../services/teller/domain/teller-transaction.model';
+import {Observable} from 'rxjs/Observable';
+import {TellerTransactionCosts} from '../../services/teller/domain/teller-transaction-costs.model';
+
+@Injectable()
+export class TellerTransactionService {
+
+ constructor(private tellerService: TellerService, private notificationService: NotificationService) {
+ }
+
+ createTransaction(tellerCode: string, tellerTransaction: TellerTransaction): Observable<TellerTransactionCosts> {
+ return this.tellerService.createTransaction(tellerCode, tellerTransaction)
+ .catch((error: Error) => {
+ this.notificationService.send({
+ type: NotificationType.ALERT,
+ title: 'Invalid transaction',
+ message: error.message
+ });
+ return Observable.empty();
+ });
+ }
+}
diff --git a/src/app/teller/store/authentication.reducer.ts b/src/app/teller/store/authentication.reducer.ts
new file mode 100644
index 0000000..67ebdc3
--- /dev/null
+++ b/src/app/teller/store/authentication.reducer.ts
@@ -0,0 +1,78 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as teller from './teller.actions';
+import {Teller} from '../../services/teller/domain/teller.model';
+
+export interface State {
+ teller: Teller;
+ authenticated: boolean;
+ loading: boolean;
+ error: Error;
+}
+
+const initialState: State = {
+ teller: null,
+ authenticated: false,
+ loading: false,
+ error: null
+};
+
+export function reducer(state = initialState, action: teller.Actions): State {
+ switch (action.type) {
+
+ case teller.UNLOCK_DRAWER: {
+ return Object.assign({}, state, {
+ loading: true,
+ authenticated: false
+ });
+ }
+
+ case teller.UNLOCK_DRAWER_SUCCESS: {
+ const teller: Teller = action.payload;
+ return Object.assign({}, state, {
+ loading: false,
+ teller,
+ authenticated: true
+ });
+ }
+
+ case teller.UNLOCK_DRAWER_FAIL: {
+ const error = action.payload;
+ return Object.assign({}, state, {
+ loading: false,
+ error
+ });
+ }
+
+ case teller.LOCK_DRAWER_SUCCESS: {
+ return initialState;
+ }
+
+ default:
+ return state;
+ }
+}
+
+export const getAuthenticated = (state: State) => state.authenticated;
+
+export const getTeller = (state: State) => state.teller;
+
+export const getError = (state: State) => state.error;
+
+export const getLoading = (state: State) => state.loading;
diff --git a/src/app/teller/store/customer-deposit-products.reducer.ts b/src/app/teller/store/customer-deposit-products.reducer.ts
new file mode 100644
index 0000000..5e7b37d
--- /dev/null
+++ b/src/app/teller/store/customer-deposit-products.reducer.ts
@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as tellers from './teller.actions';
+import {ResourceState} from '../../common/store/resource.reducer';
+import {ProductInstance} from '../../services/depositAccount/domain/instance/product-instance.model';
+import {idsToHashWithCurrentTimestamp, resourcesToHash} from '../../common/store/reducer.helper';
+
+export const initialState: ResourceState = {
+ ids: [],
+ entities: {},
+ loadedAt: {},
+ selectedId: null,
+};
+
+export function reducer(state = initialState, action: tellers.Actions): ResourceState {
+
+ switch (action.type) {
+
+ case tellers.LOAD_ALL_DEPOSIT_PRODUCTS: {
+ return initialState;
+ }
+
+ case tellers.LOAD_ALL_DEPOSIT_PRODUCTS_SUCCESS: {
+ const depositProducts: ProductInstance[] = action.payload;
+
+ const ids = depositProducts.map(depositProduct => depositProduct.accountIdentifier);
+
+ const entities = resourcesToHash(depositProducts, 'accountIdentifier');
+
+ const loadedAt = idsToHashWithCurrentTimestamp(ids);
+
+ return {
+ ids: [ ...ids ],
+ entities: entities,
+ loadedAt: loadedAt,
+ selectedId: state.selectedId
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
diff --git a/src/app/teller/store/customer-loan-products.reducer.ts b/src/app/teller/store/customer-loan-products.reducer.ts
new file mode 100644
index 0000000..58ff570
--- /dev/null
+++ b/src/app/teller/store/customer-loan-products.reducer.ts
@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as tellers from './teller.actions';
+import {ResourceState} from '../../common/store/resource.reducer';
+import {idsToHashWithCurrentTimestamp, resourcesToHash} from '../../common/store/reducer.helper';
+import {FimsCase} from '../../services/portfolio/domain/fims-case.model';
+
+export const initialState: ResourceState = {
+ ids: [],
+ entities: {},
+ loadedAt: {},
+ selectedId: null,
+};
+
+export function reducer(state = initialState, action: tellers.Actions): ResourceState {
+
+ switch (action.type) {
+
+ case tellers.LOAD_ALL_LOAN_PRODUCTS: {
+ return initialState;
+ }
+
+ case tellers.LOAD_ALL_LOAN_PRODUCTS_SUCCESS: {
+ const caseInstances: FimsCase[] = action.payload;
+
+ const ids = caseInstances.map(caseInstance => caseInstance.identifier);
+
+ const entities = resourcesToHash(caseInstances);
+
+ const loadedAt = idsToHashWithCurrentTimestamp(ids);
+
+ return {
+ ids: [ ...ids ],
+ entities: entities,
+ loadedAt: loadedAt,
+ selectedId: state.selectedId
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
diff --git a/src/app/teller/store/effects/notification.effects.ts b/src/app/teller/store/effects/notification.effects.ts
new file mode 100644
index 0000000..3b8d557
--- /dev/null
+++ b/src/app/teller/store/effects/notification.effects.ts
@@ -0,0 +1,70 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {NotificationService, NotificationType} from '../../../services/notification/notification.service';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as tellerActions from '../teller.actions';
+
+@Injectable()
+export class TellerNotificationEffects {
+
+ @Effect({ dispatch: false })
+ unlockDrawerSuccess$: Observable<Action> = this.actions$
+ .ofType(tellerActions.UNLOCK_DRAWER_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Teller drawer unlocked'
+ }));
+
+ @Effect({ dispatch: false })
+ lockDrawerSuccess$: Observable<Action> = this.actions$
+ .ofType(tellerActions.LOCK_DRAWER_SUCCESS)
+ .do(() => this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: 'Teller drawer is now locked'
+ }));
+
+ @Effect({ dispatch: false })
+ confirmTransactionSuccess: Observable<Action> = this.actions$
+ .ofType(tellerActions.CONFIRM_TRANSACTION_SUCCESS)
+ .map(action => action.payload)
+ .do((payload) => {
+ const action: string = payload.command === 'CONFIRM' ? 'confirmed' : 'canceled';
+ this.notificationService.send({
+ type: NotificationType.MESSAGE,
+ message: `Transaction successfully ${action}`
+ });
+ });
+
+ @Effect({ dispatch: false })
+ confirmTransactionFail: Observable<Action> = this.actions$
+ .ofType(tellerActions.CONFIRM_TRANSACTION_FAIL)
+ .map(action => action.payload)
+ .do((error) => {
+ this.notificationService.send({
+ title: 'Invalid transaction',
+ type: NotificationType.ALERT,
+ message: error.message
+ });
+ });
+
+ constructor(private actions$: Actions, private notificationService: NotificationService) {}
+}
diff --git a/src/app/teller/store/effects/products.service.effects.ts b/src/app/teller/store/effects/products.service.effects.ts
new file mode 100644
index 0000000..5b9967a
--- /dev/null
+++ b/src/app/teller/store/effects/products.service.effects.ts
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {DepositAccountService} from '../../../services/depositAccount/deposit-account.service';
+import {PortfolioService} from '../../../services/portfolio/portfolio.service';
+import {Observable} from 'rxjs/Observable';
+import * as tellerActions from '../teller.actions';
+import {Action} from '@ngrx/store';
+import {of} from 'rxjs/observable/of';
+
+@Injectable()
+export class TellerProductsApiEffects {
+
+ @Effect()
+ loadAllDepositProducts$: Observable<Action> = this.actions$
+ .ofType(tellerActions.LOAD_ALL_DEPOSIT_PRODUCTS)
+ .map((action: tellerActions.LoadAllDepositProductsAction) => action.payload)
+ .mergeMap(customerId =>
+ this.depositService.fetchProductInstances(customerId)
+ .map(productInstances => new tellerActions.LoadAllDepositProductsSuccessAction(productInstances))
+ .catch(() => of(new tellerActions.LoadAllDepositProductsSuccessAction([])))
+ );
+
+ @Effect()
+ loadAllLoanProducts$: Observable<Action> = this.actions$
+ .ofType(tellerActions.LOAD_ALL_LOAN_PRODUCTS)
+ .map((action: tellerActions.LoadAllLoanProductsAction) => action.payload)
+ .mergeMap(customerId =>
+ this.portfolioService.getAllCasesForCustomer(customerId)
+ .map(casePage => new tellerActions.LoadAllLoanProductsSuccessAction(casePage.elements))
+ .catch((error) => of(new tellerActions.LoadAllLoanProductsSuccessAction([])))
+ );
+
+ constructor(private actions$: Actions, private depositService: DepositAccountService, private portfolioService: PortfolioService) {}
+}
diff --git a/src/app/teller/store/effects/route.effects.ts b/src/app/teller/store/effects/route.effects.ts
new file mode 100644
index 0000000..6982e2d
--- /dev/null
+++ b/src/app/teller/store/effects/route.effects.ts
@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Action} from '@ngrx/store';
+import {Observable} from 'rxjs/Observable';
+import {Router} from '@angular/router';
+import {Actions, Effect} from '@ngrx/effects';
+import {Injectable} from '@angular/core';
+import * as tellerActions from '../../store/teller.actions';
+
+@Injectable()
+export class TellerRouteEffects {
+
+ @Effect({dispatch: false})
+ unlockDrawerSuccess$: Observable<Action> = this.actions$
+ .ofType(tellerActions.UNLOCK_DRAWER_SUCCESS)
+ .do((payload) => this.router.navigate(['/teller']));
+
+ @Effect({dispatch: false})
+ lockDrawerSuccess$: Observable<Action> = this.actions$
+ .ofType(tellerActions.LOCK_DRAWER_SUCCESS)
+ .do((payload) => this.router.navigate(['/teller/auth']));
+
+ @Effect({dispatch: false})
+ confirmTransactionSuccess$: Observable<Action> = this.actions$
+ .ofType(tellerActions.CONFIRM_TRANSACTION_SUCCESS)
+ .map(action => action.payload)
+ .do((payload) => this.router.navigate(['../../'], { relativeTo: payload.activatedRoute }));
+
+ constructor(private actions$: Actions, private router: Router) {
+ }
+}
diff --git a/src/app/teller/store/effects/service.effects.ts b/src/app/teller/store/effects/service.effects.ts
new file mode 100644
index 0000000..7683219
--- /dev/null
+++ b/src/app/teller/store/effects/service.effects.ts
@@ -0,0 +1,62 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {Actions, Effect} from '@ngrx/effects';
+import {TellerService} from '../../../services/teller/teller-service';
+import {Observable} from 'rxjs/Observable';
+import {Action} from '@ngrx/store';
+import * as tellerActions from '../teller.actions';
+import {of} from 'rxjs/observable/of';
+
+@Injectable()
+export class TellerApiEffects {
+
+ @Effect()
+ unlockDrawer$: Observable<Action> = this.actions$
+ .ofType(tellerActions.UNLOCK_DRAWER)
+ .map((action: tellerActions.UnlockDrawerAction) => action.payload)
+ .mergeMap(payload =>
+ this.tellerService.unlockDrawer(payload.tellerCode, {employeeIdentifier: payload.employeeId, password: payload.password})
+ .map(teller => new tellerActions.UnlockDrawerSuccessAction(teller))
+ .catch((error) => of(new tellerActions.UnlockDrawerFailAction(error)))
+ );
+
+ @Effect()
+ lockDrawer$: Observable<Action> = this.actions$
+ .ofType(tellerActions.LOCK_DRAWER)
+ .map((action: tellerActions.LockDrawerAction) => action.payload)
+ .mergeMap(payload =>
+ this.tellerService.executeCommand(payload.tellerCode, 'PAUSE')
+ .map(() => new tellerActions.LockDrawerSuccessAction())
+ .catch((error) => of(new tellerActions.LockDrawerSuccessAction()))
+ );
+
+ @Effect()
+ confirmTransaction$: Observable<Action> = this.actions$
+ .ofType(tellerActions.CONFIRM_TRANSACTION)
+ .map((action: tellerActions.ConfirmTransactionAction) => action.payload)
+ .mergeMap(payload =>
+ this.tellerService.confirmTransaction(payload.tellerCode, payload.tellerTransactionIdentifier, payload.command,
+ payload.chargesIncluded)
+ .map(() => new tellerActions.ConfirmTransactionSuccessAction(payload))
+ .catch((error) => of(new tellerActions.ConfirmTransactionFailAction(error)))
+ );
+
+ constructor(private actions$: Actions, private tellerService: TellerService) {}
+}
diff --git a/src/app/teller/store/index.ts b/src/app/teller/store/index.ts
new file mode 100644
index 0000000..dd59393
--- /dev/null
+++ b/src/app/teller/store/index.ts
@@ -0,0 +1,79 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import * as fromRoot from '../../store';
+import {ActionReducer, Store} from '@ngrx/store';
+import {createReducer} from '../../store/index';
+import * as fromAuthentication from './authentication.reducer';
+import * as fromDepositProducts from './customer-deposit-products.reducer';
+import * as fromLoanProducts from './customer-loan-products.reducer';
+import {createSelector} from 'reselect';
+import {
+ createResourceReducer,
+ getResourceAll,
+ getResourceLoadedAt,
+ getResourceSelected,
+ ResourceState
+} from '../../common/store/resource.reducer';
+
+export interface State extends fromRoot.State {
+ tellerAuthentication: fromAuthentication.State;
+ tellerCustomers: ResourceState;
+ tellerCustomerDepositProducts: ResourceState;
+ tellerCustomerLoanProducts: ResourceState;
+}
+
+const reducers = {
+ tellerAuthentication: fromAuthentication.reducer,
+ tellerCustomers: createResourceReducer('Teller Customer'),
+ tellerCustomerDepositProducts: fromDepositProducts.reducer,
+ tellerCustomerLoanProducts: fromLoanProducts.reducer,
+};
+
+export const tellerModuleReducer: ActionReducer<State> = createReducer(reducers);
+
+export class TellerStore extends Store<State> {}
+
+export function tellerStoreFactory(appStore: Store<fromRoot.State>) {
+ appStore.replaceReducer(tellerModuleReducer);
+ return appStore;
+}
+
+export const getAuthenticationState = (state: State) => state.tellerAuthentication;
+
+export const isAuthenticated = createSelector(getAuthenticationState, fromAuthentication.getAuthenticated);
+export const getAuthenticationError = createSelector(getAuthenticationState, fromAuthentication.getError);
+export const getAuthenticationLoading = createSelector(getAuthenticationState, fromAuthentication.getLoading);
+export const getAuthenticatedTeller = createSelector(getAuthenticationState, fromAuthentication.getTeller);
+
+export const getTellerCustomersState = (state: State) => state.tellerCustomers;
+
+export const getTellerCustomerLoadedAt = createSelector(getTellerCustomersState, getResourceLoadedAt);
+export const getTellerSelectedCustomer = createSelector(getTellerCustomersState, getResourceSelected);
+
+export const getTellerCustomerDepositProductsState = (state: State) => state.tellerCustomerDepositProducts;
+export const getAllTellerCustomerDepositProducts = createSelector(getTellerCustomerDepositProductsState, getResourceAll);
+export const hasTellerCustomerDepositProducts = createSelector(getAllTellerCustomerDepositProducts, (products) => {
+ return products.length > 0;
+});
+
+export const getTellerCustomerLoanProductsState = (state: State) => state.tellerCustomerLoanProducts;
+export const getAllTellerCustomerLoanProducts = createSelector(getTellerCustomerLoanProductsState, getResourceAll);
+export const hasTellerCustomerLoanProducts = createSelector(getAllTellerCustomerLoanProducts, (products) => {
+ return products.length > 0;
+});
diff --git a/src/app/teller/store/teller.actions.ts b/src/app/teller/store/teller.actions.ts
new file mode 100644
index 0000000..8f19ff1
--- /dev/null
+++ b/src/app/teller/store/teller.actions.ts
@@ -0,0 +1,160 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {type} from '../../store/util';
+import {Action} from '@ngrx/store';
+import {LoadResourcePayload, SelectResourcePayload} from '../../common/store/resource.reducer';
+import {ProductInstance} from '../../services/depositAccount/domain/instance/product-instance.model';
+import {RoutePayload} from '../../common/store/route-payload';
+import {Teller} from '../../services/teller/domain/teller.model';
+import {FimsCase} from '../../services/portfolio/domain/fims-case.model';
+
+export const UNLOCK_DRAWER = type('[Teller] Unlock Drawer');
+export const UNLOCK_DRAWER_SUCCESS = type('[Teller] Unlock Drawer Success');
+export const UNLOCK_DRAWER_FAIL = type('[Teller] Unlock Drawer Fail');
+export const LOCK_DRAWER = type('[Teller] Lock Drawer');
+export const LOCK_DRAWER_SUCCESS = type('[Teller] Lock Drawer Success');
+
+export const LOAD_CUSTOMER = type('[Teller Customer] Load');
+export const SELECT_CUSTOMER = type('[Teller Customer] Select');
+
+export const LOAD_ALL_DEPOSIT_PRODUCTS = type('[Teller Customer] Deposit Product Load All');
+export const LOAD_ALL_DEPOSIT_PRODUCTS_SUCCESS = type('[Teller Customer] Deposit Product Load All Success');
+export const LOAD_ALL_LOAN_PRODUCTS = type('[Teller Customer] Loan Product Load All');
+export const LOAD_ALL_LOAN_PRODUCTS_SUCCESS = type('[Teller Customer] Loan Product Load All Success');
+
+export const CONFIRM_TRANSACTION = type('[Teller Customer] Confirm Transaction');
+export const CONFIRM_TRANSACTION_SUCCESS = type('[Teller Customer] Confirm Transaction Success');
+export const CONFIRM_TRANSACTION_FAIL = type('[Teller Customer] Confirm Transaction Fail');
+
+export interface UnlockDrawerPayload {
+ tellerCode: string;
+ employeeId: string;
+ password: string;
+}
+
+export interface LockDrawerPayload {
+ tellerCode: string;
+}
+
+export interface ConfirmTransactionPayload extends RoutePayload {
+ tellerCode: string;
+ tellerTransactionIdentifier: string;
+ command: string;
+ chargesIncluded: boolean;
+}
+
+export class UnlockDrawerAction implements Action {
+ readonly type = UNLOCK_DRAWER;
+
+ constructor(public payload: UnlockDrawerPayload) { }
+}
+
+export class UnlockDrawerSuccessAction implements Action {
+ readonly type = UNLOCK_DRAWER_SUCCESS;
+
+ constructor(public payload: Teller) { }
+}
+
+export class UnlockDrawerFailAction implements Action {
+ readonly type = UNLOCK_DRAWER_FAIL;
+
+ constructor(public payload: Error) { }
+}
+
+export class LockDrawerAction implements Action {
+ readonly type = LOCK_DRAWER;
+
+ constructor(public payload: LockDrawerPayload) { }
+}
+
+export class LockDrawerSuccessAction implements Action {
+ readonly type = LOCK_DRAWER_SUCCESS;
+
+ constructor() { }
+}
+
+export class LoadCustomerAction implements Action {
+ readonly type = LOAD_CUSTOMER;
+
+ constructor(public payload: LoadResourcePayload) { }
+}
+
+export class SelectCustomerAction implements Action {
+ readonly type = SELECT_CUSTOMER;
+
+ constructor(public payload: SelectResourcePayload) { }
+}
+
+export class LoadAllDepositProductsAction implements Action {
+ readonly type = LOAD_ALL_DEPOSIT_PRODUCTS;
+
+ constructor(public payload: string) { }
+}
+
+export class LoadAllDepositProductsSuccessAction implements Action {
+ readonly type = LOAD_ALL_DEPOSIT_PRODUCTS_SUCCESS;
+
+ constructor(public payload: ProductInstance[]) { }
+}
+
+export class LoadAllLoanProductsAction implements Action {
+ readonly type = LOAD_ALL_LOAN_PRODUCTS;
+
+ constructor(public payload: string) { }
+}
+
+export class LoadAllLoanProductsSuccessAction implements Action {
+ readonly type = LOAD_ALL_LOAN_PRODUCTS_SUCCESS;
+
+ constructor(public payload: FimsCase[]) { }
+}
+
+export class ConfirmTransactionAction implements Action {
+ readonly type = CONFIRM_TRANSACTION;
+
+ constructor(public payload: ConfirmTransactionPayload) {}
+}
+
+export class ConfirmTransactionSuccessAction implements Action {
+ readonly type = CONFIRM_TRANSACTION_SUCCESS;
+
+ constructor(public payload: ConfirmTransactionPayload) {}
+}
+
+export class ConfirmTransactionFailAction implements Action {
+ readonly type = CONFIRM_TRANSACTION_FAIL;
+
+ constructor(public payload: Error) {}
+}
+
+export type Actions
+ = UnlockDrawerAction
+ | UnlockDrawerSuccessAction
+ | UnlockDrawerFailAction
+ | LockDrawerAction
+ | LockDrawerSuccessAction
+ | LoadCustomerAction
+ | SelectCustomerAction
+ | LoadAllDepositProductsAction
+ | LoadAllDepositProductsSuccessAction
+ | LoadAllLoanProductsAction
+ | LoadAllLoanProductsSuccessAction
+ | ConfirmTransactionAction
+ | ConfirmTransactionSuccessAction
+ | ConfirmTransactionFailAction;
diff --git a/src/app/teller/teller-login.guard.ts b/src/app/teller/teller-login.guard.ts
new file mode 100644
index 0000000..c47ece5
--- /dev/null
+++ b/src/app/teller/teller-login.guard.ts
@@ -0,0 +1,40 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Injectable} from '@angular/core';
+import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router';
+import {Observable} from 'rxjs/Observable';
+import * as fromTeller from './store/index';
+import {TellerStore} from './store/index';
+
+@Injectable()
+export class TellerLoginGuard implements CanActivate {
+
+ constructor(private store: TellerStore, private router: Router, private route: ActivatedRoute) {}
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
+ return this.store.select(fromTeller.isAuthenticated)
+ .map(isAuthenticated => {
+ if (!isAuthenticated) {
+ this.router.navigate(['/teller/auth'], { relativeTo: this.route });
+ return false;
+ }
+ return true;
+ });
+ }
+}
diff --git a/src/app/teller/teller.index.component.html b/src/app/teller/teller.index.component.html
new file mode 100644
index 0000000..6fd5727
--- /dev/null
+++ b/src/app/teller/teller.index.component.html
@@ -0,0 +1,51 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<section>
+ <h2 class="mat-display-1 text-upper text-center push-bottom">Search member </h2>
+ <div layout-gt-xs="row" layout-align="center center">
+ <div layout-gt-xs="row" flex-gt-xs="90" layout-margin>
+ <div flex-gt-xs="25"></div>
+ <div flex-gt-xs="50">
+ <mat-card>
+ <mat-card-title>
+ <td-search-input placeholder="Search here" [showUnderline]="false" [debounce]="500" (searchDebounce)="search($event)" (clear)="clearSearch()">
+ </td-search-input>
+ </mat-card-title>
+ <mat-card-subtitle>Search results</mat-card-subtitle>
+ <mat-divider></mat-divider>
+ <mat-list>
+ <ng-template let-item let-last="last" ngFor [ngForOf]="customer$ | async">
+ <mat-list-item>
+ <mat-icon mat-list-icon class="fill-grey-700">face</mat-icon>
+ <h4 matLine>{{item.surname}}, {{item.givenName}}</h4>
+ <button mat-raised-button color="accent" title="{{'SHOW' | translate}}" (click)="showCustomer(item.identifier)">{{'SHOW' | translate}}</button>
+ </mat-list-item>
+ <mat-divider mat-inset *ngIf="!last"></mat-divider>
+ </ng-template>
+ </mat-list>
+ <mat-divider></mat-divider>
+ </mat-card>
+ </div>
+ <div flex-gt-xs="25"></div>
+ </div>
+ </div>
+</section>
+<router-outlet></router-outlet>
+<a mat-fab color="accent" class="mat-fab-position-bottom-right" (click)="logout()" title="{{'Pause' | translate}}">
+ <mat-icon>pause</mat-icon>
+</a>
diff --git a/src/app/teller/teller.index.component.ts b/src/app/teller/teller.index.component.ts
new file mode 100644
index 0000000..ead4c30
--- /dev/null
+++ b/src/app/teller/teller.index.component.ts
@@ -0,0 +1,76 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy} from '@angular/core';
+import * as fromTeller from './store/index';
+import {TellerStore} from './store/index';
+import * as fromRoot from '../store/index';
+import {LOCK_DRAWER} from './store/teller.actions';
+import {Subscription} from 'rxjs/Subscription';
+import {ActivatedRoute, Router} from '@angular/router';
+import {SEARCH} from '../store/customer/customer.actions';
+import {Customer} from '../services/customer/domain/customer.model';
+import {Observable} from 'rxjs/Observable';
+import {Teller} from '../services/teller/domain/teller.model';
+
+@Component({
+ templateUrl: './teller.index.component.html'
+})
+export class TellerIndexComponent implements OnDestroy {
+
+ private tellerCodeSubscription: Subscription;
+
+ private teller: Teller;
+
+ customer$: Observable<Customer[]>;
+
+ constructor(private router: Router, private route: ActivatedRoute, private store: TellerStore) {
+ this.tellerCodeSubscription = store.select(fromTeller.getAuthenticatedTeller)
+ .subscribe(teller => this.teller = teller);
+
+ this.customer$ = store.select(fromRoot.getSearchCustomers);
+ }
+
+ ngOnDestroy(): void {
+ this.tellerCodeSubscription.unsubscribe();
+ }
+
+ logout(): void {
+ this.store.dispatch({
+ type: LOCK_DRAWER,
+ payload: {
+ tellerCode: this.teller.code
+ }
+ });
+ }
+
+ showCustomer(identifier: string): void {
+ this.router.navigate(['customers/detail', identifier], { relativeTo: this.route });
+ }
+
+ search(searchTerm: string): void {
+ this.store.dispatch({
+ type: SEARCH,
+ payload: {
+ searchTerm
+ }
+ });
+ }
+
+ clearSearch(): void {}
+}
diff --git a/src/app/teller/teller.module.ts b/src/app/teller/teller.module.ts
new file mode 100644
index 0000000..ba18111
--- /dev/null
+++ b/src/app/teller/teller.module.ts
@@ -0,0 +1,109 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {NgModule} from '@angular/core';
+import {CommonModule} from '@angular/common';
+import {TellerRoutes} from './teller.routing';
+import {RouterModule} from '@angular/router';
+import {TellerStore, tellerStoreFactory} from './store/index';
+import {Store} from '@ngrx/store';
+import {TellerIndexComponent} from './teller.index.component';
+import {TellerLoginGuard} from './teller-login.guard';
+import {TellerAuthComponent} from './auth/teller-auth.component';
+import {EffectsModule} from '@ngrx/effects';
+import {TellerApiEffects} from './store/effects/service.effects';
+import {
+ MatAutocompleteModule,
+ MatButtonModule,
+ MatCardModule,
+ MatCheckboxModule,
+ MatIconModule,
+ MatInputModule,
+ MatListModule,
+ MatSelectModule,
+ MatToolbarModule
+} from '@angular/material';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {TranslateModule} from '@ngx-translate/core';
+import {TellerRouteEffects} from './store/effects/route.effects';
+import {CovalentDataTableModule, CovalentMessageModule, CovalentSearchModule, CovalentStepsModule} from '@covalent/core';
+import {TellerCustomerExistsGuard} from './customer/teller-customer-exists.guard';
+import {TellerCustomerDetailComponent} from './customer/customer-detail.component';
+import {TellerProductsApiEffects} from './store/effects/products.service.effects';
+import {TellerCustomerIndexComponent} from './customer/customer-index.component';
+import {FimsSharedModule} from '../common/common.module';
+import {DepositTransactionFormComponent} from './customer/transaction/deposit/form.component';
+import {TellerNotificationEffects} from './store/effects/notification.effects';
+import {LoanTransactionFormComponent} from './customer/transaction/loan/form.component';
+import {CreateLoanTransactionFormComponent} from './customer/transaction/loan/create.form.component';
+import {TransactionCostComponent} from './customer/transaction/components/cost.component';
+import {CreateDepositTransactionFormComponent} from './customer/transaction/deposit/create.form.component';
+import {ChequeTransactionFormComponent} from './customer/transaction/cheque/form.component';
+import {CreateChequeTransactionFormComponent} from './customer/transaction/cheque/create.component';
+import {TellerTransactionService} from './services/transaction.service';
+import {AvailableActionService} from './services/available-actions.service';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(TellerRoutes),
+ TranslateModule,
+ FimsSharedModule,
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ MatIconModule,
+ MatButtonModule,
+ MatInputModule,
+ MatCardModule,
+ MatListModule,
+ MatToolbarModule,
+ MatAutocompleteModule,
+ MatSelectModule,
+ MatCheckboxModule,
+ CovalentMessageModule,
+ CovalentStepsModule,
+ CovalentSearchModule,
+ CovalentDataTableModule,
+ EffectsModule.run(TellerApiEffects),
+ EffectsModule.run(TellerRouteEffects),
+ EffectsModule.run(TellerProductsApiEffects),
+ EffectsModule.run(TellerNotificationEffects)
+ ],
+ declarations: [
+ TellerIndexComponent,
+ TellerAuthComponent,
+ TellerCustomerIndexComponent,
+ TellerCustomerDetailComponent,
+ CreateDepositTransactionFormComponent,
+ DepositTransactionFormComponent,
+ LoanTransactionFormComponent,
+ CreateLoanTransactionFormComponent,
+ TransactionCostComponent,
+ CreateChequeTransactionFormComponent,
+ ChequeTransactionFormComponent
+ ],
+ providers: [
+ TellerLoginGuard,
+ TellerCustomerExistsGuard,
+ TellerTransactionService,
+ AvailableActionService,
+ { provide: TellerStore, useFactory: tellerStoreFactory, deps: [Store]}
+ ]
+})
+export class TellerModule { }
diff --git a/src/app/teller/teller.routing.ts b/src/app/teller/teller.routing.ts
new file mode 100644
index 0000000..7357be1
--- /dev/null
+++ b/src/app/teller/teller.routing.ts
@@ -0,0 +1,66 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Routes} from '@angular/router';
+import {TellerLoginGuard} from './teller-login.guard';
+import {TellerAuthComponent} from './auth/teller-auth.component';
+import {TellerIndexComponent} from './teller.index.component';
+import {TellerCustomerDetailComponent} from './customer/customer-detail.component';
+import {TellerCustomerExistsGuard} from './customer/teller-customer-exists.guard';
+import {TellerCustomerIndexComponent} from './customer/customer-index.component';
+import {CreateDepositTransactionFormComponent} from './customer/transaction/deposit/create.form.component';
+import {CreateLoanTransactionFormComponent} from './customer/transaction/loan/create.form.component';
+import {CreateChequeTransactionFormComponent} from './customer/transaction/cheque/create.component';
+
+export const TellerRoutes: Routes = [
+ {
+ path: '',
+ canActivate: [TellerLoginGuard],
+ data: { title: 'Teller management', hasPermission: { id: 'teller_operations', accessLevel: 'READ' } },
+ children: [
+ {
+ path: '',
+ component: TellerIndexComponent
+ },
+ {
+ path: 'customers/detail/:id',
+ component: TellerCustomerIndexComponent,
+ data: {
+ hasPermission: { id: 'customer_customers', accessLevel: 'READ' }
+ },
+ canActivate: [ TellerCustomerExistsGuard ],
+ children: [
+ {
+ path: '',
+ component: TellerCustomerDetailComponent,
+ data: {title: 'View Customer'}
+ },
+ {path: 'transaction/deposit', component: CreateDepositTransactionFormComponent, data: { title: 'Create transaction' } },
+ {path: 'transaction/loan', component: CreateLoanTransactionFormComponent, data: { title: 'Create transaction' } },
+ {path: 'transaction/cheque', component: CreateChequeTransactionFormComponent, data: { title: 'Create transaction' }},
+ {path: 'identifications', loadChildren: '../customers/detail/identityCard/identity-card.module#IdentityCardModule'},
+ ]
+ }
+ ]
+ },
+ {
+ path: 'auth',
+ component: TellerAuthComponent,
+ data: { title: 'Teller login' }
+ }
+];
diff --git a/src/app/user/password.component.html b/src/app/user/password.component.html
new file mode 100644
index 0000000..c626dee
--- /dev/null
+++ b/src/app/user/password.component.html
@@ -0,0 +1,61 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<div layout="column" layout-fill>
+ <div class="mat-content" layout-padding flex>
+ <div layout-gt-xs="row" layout-align-gt-xs="center start" class="margin">
+ <div flex-gt-xs="25" layout-align="center center">
+ <mat-card>
+ <mat-card-title translate>Change password</mat-card-title>
+ <mat-card-subtitle *ngIf="forced" translate>please change your password in order to access fims</mat-card-subtitle>
+ <mat-card-content>
+ <form [formGroup]="passwordForm" layout-align="center center" layout-margin>
+ <div layout="row">
+ <mat-form-field layout-margin flex>
+ <input matInput placeholder="{{'New Password' | translate}}" type="password" formControlName="newPassword" autocomplete="new-password"/>
+ <mat-error *ngIf="passwordForm.get('newPassword').hasError('required')" translate>
+ Required
+ </mat-error>
+ <mat-error *ngIf="passwordForm.get('newPassword').hasError('minlength')">
+ {{ 'Must have at least characters.' | translate:{ value: passwordForm.get('newPassword').getError('minlength')['requiredLength']} }}
+ </mat-error>
+ </mat-form-field>
+ </div>
+ <div layout="row">
+ <mat-form-field layout-margin flex>
+ <input matInput placeholder="{{'Confirm New Password' | translate}}" type="password" formControlName="confirmNewPassword" autocomplete="new-password"/>
+ <mat-error *ngIf="passwordForm.get('confirmNewPassword').hasError('required')" translate>
+ Required
+ </mat-error>
+ </mat-form-field>
+ </div>
+ <div layout="row">
+ <span *ngIf="passwordForm.hasError('mismatch')" layout-margin class="tc-red-700" translate>
+ Passwords must match.
+ </span>
+ <span *ngIf="error" layout-margin class="tc-red-700">{{error | translate}}</span>
+ </div>
+ </form>
+ </mat-card-content>
+ <mat-card-actions>
+ <button flex mat-raised-button color="primary" [disabled]="passwordForm.invalid" (click)="changePassword()">{{'Change password' | translate}}</button>
+ </mat-card-actions>
+ </mat-card>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/src/app/user/password.component.ts b/src/app/user/password.component.ts
new file mode 100644
index 0000000..3f06d25
--- /dev/null
+++ b/src/app/user/password.component.ts
@@ -0,0 +1,84 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Component, OnDestroy, OnInit} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {FimsValidators} from '../common/validator/validators';
+import {Subscription} from 'rxjs/Subscription';
+import {Store} from '@ngrx/store';
+import * as fromRoot from '../store';
+import {CHANGE_PASSWORD} from '../store/security/security.actions';
+
+@Component({
+ selector: 'fims-user-password',
+ templateUrl: './password.component.html'
+})
+export class PasswordComponent implements OnInit, OnDestroy {
+
+ private usernameSubscription: Subscription;
+
+ private passwordErrorSubscription: Subscription;
+
+ private currentUser: string;
+
+ passwordForm: FormGroup;
+
+ error: string;
+
+ forced: boolean;
+
+ constructor(private formBuilder: FormBuilder, private route: ActivatedRoute, private store: Store<fromRoot.State>) {}
+
+ ngOnInit() {
+ this.route.queryParams.subscribe((queryParams) => {
+ this.forced = queryParams['forced'] === 'true';
+ });
+
+ this.usernameSubscription = this.store.select(fromRoot.getUsername)
+ .subscribe(username => this.currentUser = username);
+
+ this.passwordErrorSubscription = this.store.select(fromRoot.getPasswordError)
+ .filter(error => !!error)
+ .subscribe(error => this.error = 'There was an error changing your password');
+
+ this.passwordForm = this.createFormGroup();
+ }
+
+ ngOnDestroy(): void {
+ this.usernameSubscription.unsubscribe();
+ this.passwordErrorSubscription.unsubscribe();
+ }
+
+ private createFormGroup(): FormGroup {
+ return this.formBuilder.group({
+ newPassword: ['', [Validators.required, Validators.minLength(8)]],
+ confirmNewPassword: ['', Validators.required]
+ }, { validator: FimsValidators.matchValues('newPassword', 'confirmNewPassword')});
+ }
+
+ changePassword() {
+ const newPassword: string = this.passwordForm.get('newPassword').value;
+
+ this.store.dispatch({ type: CHANGE_PASSWORD, payload: {
+ username: this.currentUser,
+ password: newPassword
+ }});
+ }
+
+}
diff --git a/src/app/user/user.module.ts b/src/app/user/user.module.ts
new file mode 100644
index 0000000..fb6d369
--- /dev/null
+++ b/src/app/user/user.module.ts
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {NgModule} from '@angular/core';
+import {RouterModule} from '@angular/router';
+import {ReactiveFormsModule} from '@angular/forms';
+import {PasswordComponent} from './password.component';
+import {UserRoutes} from './user.routing';
+import {MatButtonModule, MatCardModule, MatInputModule} from '@angular/material';
+import {CommonModule} from '@angular/common';
+import {TranslateModule} from '@ngx-translate/core';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(UserRoutes),
+ TranslateModule,
+ CommonModule,
+ ReactiveFormsModule,
+ MatCardModule,
+ MatInputModule,
+ MatButtonModule
+ ],
+ declarations: [
+ PasswordComponent
+ ]
+})
+export class UserModule {}
diff --git a/src/app/user/user.routing.ts b/src/app/user/user.routing.ts
new file mode 100644
index 0000000..e43e38a
--- /dev/null
+++ b/src/app/user/user.routing.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {Routes} from '@angular/router';
+import {PasswordComponent} from './password.component';
+
+export const UserRoutes: Routes = [
+ { path: '', component: PasswordComponent }
+];
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
new file mode 100644
index 0000000..f34fcf0
--- /dev/null
+++ b/src/assets/i18n/en.json
@@ -0,0 +1,24 @@
+{
+ "Account entries for account": "Account entries for account {{value}}",
+ "Account with value": "Account {{value}}",
+ "BEGINNING_BALANCE": "Flat",
+ "Create new loan for member": "Create new loan for member {{value}}",
+ "CURRENT_BALANCE": "Declining",
+ "Edit loan for member": "Edit loan for member {{value}}",
+ "Issued by": "Issued by {{value}}",
+ "Please verify the following tasks before you can action this member": "Please verify the following tasks before you can {{value}} this member",
+ "Only characters allowed.": "Only {{value}} characters allowed.",
+ "Value precision must be smaller or equals": "Value precision must be smaller or equals {{value}}",
+ "Only numbers allowed.": "Only {{value}} numbers allowed.",
+ "Value must be greater than or equal to": "Value must be greater than or equal to {{value}}",
+ "Value must be smaller than or equal to": "Value must be smaller than or equal to {{value}}",
+ "Value must be greater than": "Value must be greater than {{value}}",
+ "Value must be smaller than": "Value must be smaller than {{value}}",
+ "Value scale must be smaller than or equal to": "Value scale must be smaller than or equal to {{value}}",
+ "Max file size": "File can't exceed size of {{value}}KB",
+ "Must have at least characters.": "Must have at least {{value}} characters.",
+ "Must have decimal places": "Must have {{value}} decimal places",
+ "Member is assigned to office:": "Member is assigned to office: {{value}}",
+ "Ledger with name": "Ledger {{value}}",
+ "Subledgers of": "Subledgers of {{value}}"
+}
diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json
new file mode 100644
index 0000000..31d75ee
--- /dev/null
+++ b/src/assets/i18n/es.json
@@ -0,0 +1,696 @@
+{
+ "Interests will be calculated on a daily basis": "",
+ "Account": "Cuenta",
+ "Account can't be deleted": "",
+ "Account Closing": "",
+ "Account details": "Detalles de cuenta",
+ "Account entries for account": "Entradas de cuenta {{value}}",
+ "Account entries": "Entrada de cuenta",
+ "Account has account entries": "",
+ "Account is going to be deleted": "",
+ "Account is going to be saved": "",
+ "Account Opening": "",
+ "Account Transfer": "",
+ "Account settings": "Configuración de cuentas",
+ "Account with value": "Account {{value}}",
+ "Accounting": "Contabilidad",
+ "Accounts for ledger": "Cuentas para el libro principal",
+ "Accounts": "Cuentas",
+ "Accrue account": "",
+ "Accrue account(Liability accounts only)": "",
+ "Action": "Acción",
+ "Actions": "Acciones",
+ "ACTIVATE": "ACTIVAR",
+ "Active": "Activo",
+ "ACTIVE": "ACTIVO",
+ "Activities": "Actividades",
+ "Add account": "Agregar cuenta",
+ "Add action": "Añadir acción",
+ "Add detail": "",
+ "ADD DOCUMENT": "",
+ "ADD FEE": "",
+ "Add field": "",
+ "Add identification card scan": "",
+ "Add Journal Entry": "Añadir entrada de diario",
+ "Add moratorium": "Agregar mora",
+ "Add new fee": "Añadir nuevo cargo",
+ "Add new task definition": "Agregar nueva definición de tarea",
+ "Add task for member": "Añadir tarea al cliente",
+ "ADD TASK": "AÑADIR TAREA",
+ "ADD STEP": "",
+ "Address": "Dirección",
+ "Affected Accounts": "Cuentas afectadas",
+ "Allow for write-off": "",
+ "Amount disbursed/Total": "Cantidad desembolsada/Total",
+ "Amount paid": "Cantidad paga",
+ "Amount to pay": "Monto a pagar",
+ "Amount": "Cantidad",
+ "amount": "Cantidad",
+ "Annually": "",
+ "Applied on": "",
+ "Applied when": "Aplicar cuando",
+ "April": "Abril",
+ "Arrears allowance reserve account": "Cuenta de reserva de asignaciones atrasadas",
+ "Arrears allowance(Expense accounts only)": "",
+ "Arrears allowance": "Indemnización por atrasos",
+ "Assign member to employee(optional)": "Asignar cliente a empleado (opcional)",
+ "Assign member to office(optional)": "Asignar cliente a un oficial (opcional)",
+ "Assign employee to office(optional)": "Asignar empleado a oficina (opcional)",
+ "Assign role to employee": "Asignar rol a empleado",
+ "Assign product": "",
+ "Assign": "Designar",
+ "Assigned employee": "",
+ "Assigned Office": "Asignar oficina",
+ "Assigned office": "Desginar oficina",
+ "Assigned role": "Rol asignado",
+ "August": "Agosto",
+ "Balance range": "Rango de balance",
+ "Balance": "Balance",
+ "BEGINNING_BALANCE": "Comienzo del balance",
+ "Birthday": "Fecha de nacimiento",
+ "Branch offices": "Sucursal",
+ "Browse...": "",
+ "CANCEL": "CANCELAR",
+ "Cash account": "",
+ "Cash account(Asset accounts only)": "",
+ "Cash account for loans approved but not yet disbursed.": "",
+ "Cash account holding funds for loans which are approved but not yet disbursed.": "",
+ "Cash Deposit": "",
+ "Cash in": "",
+ "Cash on hand": "",
+ "Cash out": "",
+ "Cash Withdrawal": "",
+ "Change custom field": "",
+ "Change language": "Cambiar idioma",
+ "Change password": "Cambiar contraseña",
+ "Change state": "Cambiar estado",
+ "Change the status of the member": "Cambiar el estado del cliente",
+ "Change the status of this account": "Cambiar estado de cuenta",
+ "Change the status of this deposit product": "",
+ "Change": "Cambiar",
+ "Fee is going to be created": "Cargo a ser creado",
+ "Fee is going to be deleted": "Cargo a ser eliminado",
+ "Fee is going to be updated": "Cargo a ser actualizado",
+ "Fee method": "Método de cargo",
+ "Fee name": "",
+ "Fee task": "Cargar tarea",
+ "Fees": "Cargos",
+ "Fees are paid in cash": "",
+ "Chart of accounts": "",
+ "Checking": "",
+ "Cheques receivable account": "",
+ "Cheques receivable account(Asset accounts only)": "",
+ "Choose a file to upload(max size 512 KB)": "",
+ "Choose one task": "Elegir una tarea",
+ "City": "Ciudad",
+ "Clerk": "Empleado",
+ "Click here to upload": "",
+ "CLOSE": "CERRAR",
+ "CLOSED": "CERRADO",
+ "Code": "",
+ "Confirm action": "",
+ "Confirm deletion": "Confirmar eliminación",
+ "Confirm New Password": "Confirmar nueva contraseña",
+ "Confirmation": "",
+ "Contact information": "Información de contacto",
+ "Contact Information": "Información de contacto",
+ "CONTINUE": "CONTINUAR",
+ "Count": "",
+ "Country short name": "Abreviación de país",
+ "Country": "País",
+ "CREATE ACCOUNT": "CREAR CUENTA",
+ "Create and edit roles to manage access levels within fims.": "Crear y editar roles para administrar niveles de acceso",
+ "Create and edit your employees and assign them to offices.": "Crear y editar sus empleados y asignarlos a oficinas",
+ "Create and edit your offices here.": "Crear y editar sus oficiales ",
+ "Create branch office": "Crear sucursal",
+ "Create custom fields": "",
+ "CREATE CUSTOM FIELDS": "",
+ "CREATE MEMBER LOAN": "CREAR PRÉSTAMO A CLIENTE",
+ "CREATE MEMBER": "CREAR CLIENTE",
+ "CREATE DENOMINATION": "",
+ "CREATE DEPOSIT ACCOUNT": "",
+ "CREATE DOCUMENT": "",
+ "CREATE IDENTIFICATION CARD": "",
+ "CREATE IDENTIFICATION CARD SCAN": "",
+ "CREATE EMPLOYEE": "CREAR EMPLEADO",
+ "Create headquarter": "Crear oficina principal",
+ "CREATE JOURNAL ENTRY": "CREAR ENTRADA DE DIARIO",
+ "CREATE LEDGER": "CREAR LIBRO PRINCIPAL",
+ "Create ledger": "Crear libro principal",
+ "Create loan for member": "Crear préstamo a cliente",
+ "Create new account": "Crear nueva cuenta",
+ "Create new member": "Crear nuevo cliente",
+ "Create denomination": "",
+ "Create new denomination": "",
+ "Create new deposit account for member": "",
+ "Create new deposit product": "",
+ "Create new document": "",
+ "Create new identification card": "",
+ "Create new employee": "Crear nuevo empleado",
+ "Create new journal entry": "Crear nueva entrada de diario",
+ "Create new ledger": "Crear nuevo libro principal",
+ "Create new loan account": "Crear nueva cuenta de préstamo",
+ "Create new loan for member": "Crear nuevo préstamo para el cliente {{value}}",
+ "Create new office": "Crear nueva oficina",
+ "Create new product": "Crear nuevo producto",
+ "Create new role": "Crear nuevo rol",
+ "Create new task for member": "Crear nueva tarea para el cliente",
+ "Create new task": "Crear nueva tarea",
+ "Create new transaction type": "",
+ "CREATE OFFICE": "CREAR OFICINA",
+ "CREATE PAGE": "",
+ "CREATE PRODUCT": "CREAR PRODUCTO",
+ "CREATE TRANSACTION TYPE": "",
+ "Create subledger": "Crear un sub libro principal",
+ "Create/edit members for your offices.": "Crear/editar clientes para sus oficinas",
+ "Create/edit ledgers and accounts for your General Ledger.": "Crear/editar libro principal y cuentas de su Libro Mayor",
+ "CREATED": "CREADO",
+ "Created by": "",
+ "Credit": "Crédito",
+ "Creditor": "Acreedor",
+ "Currency": "",
+ "Currency code": "Código de moneda",
+ "Currency digits": "Dígitos de moneda",
+ "Current password": "Contraseña actual",
+ "Current selection": "Selección actual",
+ "Current status": "Estado actual",
+ "CURRENT_BALANCE": "Balance Actual",
+ "Current balance": "Balance Actual",
+ "Custom field": "",
+ "Custom fields": "Campos personalizados",
+ "Member Address": "Dirección del cliente",
+ "Member contact(optional)": "Contacto del cliente (opcional)",
+ "Member deposit account": "",
+ "Member details": "Detalles del cliente",
+ "Member is assigned to office:": "Oficial es asignado directamente a un cliente",
+ "Member loan is going to be created": "Préstamo del cliente a ser creado",
+ "Member loan is going to be updated": "Préstamo del cliente a ser actualizado",
+ "Member loan": "Préstamo de cliente",
+ "Member loans": "",
+ "Member loan ledger": "",
+ "Member": "Cliente",
+ "Members": "Clientes",
+ "Cycle": "Ciclo",
+ "Dashboard": "Tablero",
+ "Data type": "",
+ "Date": "Fecha",
+ "Date must be before today": "",
+ "Date must be after today": "",
+ "Day of birth": "Fecha de nacimiento",
+ "Days late": "",
+ "Days late can't overlap with other days late.": "",
+ "Debit": "Débito",
+ "Debitor": "Deudor",
+ "Debtor": "Deudor",
+ "December": "Diciembre",
+ "DELETE ACCOUNT": "",
+ "DELETE IDENTIFICATION CARD": "",
+ "DELETE LEDGER": "ELIMINAR LIBRO CONTABLE",
+ "DELETE PORTRAIT": "",
+ "DELETE ROLE": "",
+ "DELETE TASK": "",
+ "Delete document": "",
+ "Delete this account": "",
+ "Delete this catalog": "",
+ "Delete this identification card": "",
+ "Delete this employee": "Eliminar empleado",
+ "Delete this field": "",
+ "Delete this ledger": "Eliminar este libro principal",
+ "Delete this product": "",
+ "Delete this office": "Eliminar ésta oficina",
+ "Delete this role": "",
+ "Delete this task": "",
+ "Delete": "Eliminar",
+ "Denomination required": "",
+ "Deposit account is going to be saved": "",
+ "Deposit account used for fees and disbursal": "",
+ "Deposit product": "",
+ "Deposit products": "",
+ "Description": "Descripción",
+ "Description(Optional)": "Descripción (opcional)",
+ "Description(optional)": "Descripción (opcional)",
+ "Deselect": "Deseleccionar",
+ "Details": "Detalles",
+ "DISABLE PRODUCT": "",
+ "DISBURSE LOAN": "DESEMBOLSAR PRÉSTAMO",
+ "Disbursement fee": "",
+ "Disbursement fee income account(Revenue accounts only)": "",
+ "DISTRIBUTE DIVIDEND": "",
+ "Distribute dividends": "",
+ "Dividend distributions": "",
+ "Dividend is going to be distributed": "",
+ "Dividend rate": "",
+ "Do you want to delete ledger {{value}}": "¿Quiere eliminar el libro {{value}} contable?",
+ "Do you want to delete this account?": "",
+ "Do you want to delete this identification card?": "",
+ "Do you want to delete this ledger?": "",
+ "Do you want to delete this role?": "",
+ "Do you want to delete the portrait?": "",
+ "Do you want to delete this task?": "",
+ "Document could not be locked": "",
+ "Document is going to be deleted": "",
+ "Document is going to be saved": "",
+ "Document locked": "",
+ "Document not locked": "",
+ "Due date": "",
+ "Edit account": "Editar cuenta",
+ "Edit configuration": "",
+ "Edit document": "",
+ "Edit fee": "Editar cargo",
+ "Edit field": "",
+ "Edit member loan": "Editar préstamo del cliente",
+ "Edit member": "Editar clientes",
+ "Edit deposit product": "",
+ "Edit identification card": "",
+ "Edit employee": "Editar empleado",
+ "Edit ledger": "Editar libro principal",
+ "Edit loan for member": "Editar préstamo del cliente {{value}}",
+ "Edit office": "Editar oficina",
+ "Edit product": "Editar producto",
+ "Edit role": "Editar rol",
+ "Edit task definition": "Editar definición de tarea",
+ "Edit task": "Editar tarea",
+ "Edit transaction type": "",
+ "Email(optional)": "Correo electrónico (opcional)",
+ "Employee already assigned": "",
+ "Employees can only be assigned to one teller. Please choose a different employee or unassign the employee first.": "",
+ "Employee contact(optional)": "Contacto del empleado (opcional)",
+ "Employee details": "Detalles de empleado",
+ "Employees": "Empleados",
+ "ENABLE PRODUCT": "",
+ "End date": "Fecha de finalización",
+ "Enter your comment here...": "Escriba su comentario aquí...",
+ "Equity ledger": "",
+ "Equity ledger(Equity ledgers only)": "",
+ "Execute task": "Ejecutar tarea",
+ "Expense account": "",
+ "Expense account(Expense accounts only)": "",
+ "Expenses": "",
+ "Expiration date": "Fecha de finalización",
+ "February": "Febrero",
+ "Fee income accounts": "Cuentas de ingreso de comisiones",
+ "Fetching data for chart of accounts...": "",
+ "Fetching results...": "",
+ "Final step": "",
+ "Financial products": "Productos financieros",
+ "First name": "Primer nombre",
+ "First Name": "Primer nombre",
+ "First": "Primero",
+ "Fixed term?": "",
+ "Flexible interest during the term?": "",
+ "Four eyes": "Cuatro ojos",
+ "Friday": "Viernes",
+ "General Ledger": "Libro mayor",
+ "Go back to member": "Volver a cliente",
+ "Go back to member loan": "",
+ "Go back to deposit product": "",
+ "Go back to identification cards": "",
+ "Go to account": "Ir a cuenta",
+ "Go to accounts": "Ir a cuentas",
+ "Go to member": "Ir a cliente",
+ "Go to members": "Ir a clientes",
+ "Go to general ledger": "Ir al libro principal",
+ "Go to ledger": "Ir al libro principal",
+ "Go to overview": "Ir a resumen",
+ "Go to parent": "Ir a principal",
+ "Go to parent ledger": "",
+ "Go to profile": "Ir al perfil",
+ "GO TO TASKS": "",
+ "Gross Profit": "",
+ "fims login": "Registración Fims",
+ "Hint": "",
+ "Hint(Optional)": "",
+ "Holders": "Titulares",
+ "Id": "Identificación",
+ "Identification card": "",
+ "Identification cards": "",
+ "Identification card available": "Tarjeta de identificación disponible",
+ "Identification": "Identificación",
+ "Identifier": "Identificador",
+ "Identities": "",
+ "Identity card(optional)": "Cédula de identidad (opcional)",
+ "in": "en",
+ "Include empty entries?": "¿Incluir entradas vacías?",
+ "Income": "",
+ "Income statement": "",
+ "Initial balance": "Balance inicial",
+ "Interest accrual account(Asset accounts only)": "",
+ "Interest creditor account": "Cuenta de acreedores de intereses",
+ "Interest income account(Revenue accounts only)": "",
+ "interest is applied": "Interés aplicado",
+ "Interest payable:": "",
+ "Interest range": "Alcance de los intereses",
+ "Interest range?": "¿Alcance de los intereses?",
+ "Interest settings": "Configuración de intereses",
+ "Interest": "Interés",
+ "Invalid account": "",
+ "Invalid country": "",
+ "Invalid date range": "Rango de datos inválido",
+ "Invalid transaction": "",
+ "Issued by": "Emitido por",
+ "Issued ID": "Identificación emitida",
+ "Issuer": "Emisor",
+ "Issuing Bank": "",
+ "It seems the resource you requested is either not available or you don't have the permission to access it.": "El recurso solicitado puede no estar disponible o usted no tiene permisos para acceder a él.",
+ "January": "Enero",
+ "Journal entries": "Diario de entradas",
+ "Journal Entry details": "Detalles de entrada de diario",
+ "Journal": "",
+ "July": "Julio",
+ "June": "Junio",
+ "Label": "",
+ "Last name": "Apellido",
+ "Last Name": "Apellido",
+ "Last modified by": "",
+ "Last transaction": "",
+ "Last": "Último",
+ "Late fee": "",
+ "Late fee accrual account(Asset accounts only)": "",
+ "Late fee income account(Revenue accounts only)": "",
+ "Latest activity": "Última actividad",
+ "Ledger and account settings": "",
+ "Ledger can't be deleted": "Libro contable no puede ser eliminado",
+ "Ledger details": "Detalles del libro principal",
+ "Ledger has accounts or sub ledgers": "Libro contable tiene cuentas o sub libros",
+ "Ledger in which to create individual member loan account.": "",
+ "Ledger used to create member loan account.": "",
+ "Ledger with name": "Libro contable con nombre",
+ "Ledger": "Libro principal",
+ "Length": "",
+ "Loan accounts": "Cuentas de préstamos",
+ "Loan details": "Detalle de préstamo",
+ "Loan funds allocation": "",
+ "loan is approved": "Préstamo aprobado",
+ "loan is closed": "Préstamo cerrado",
+ "loan is denied": "Préstamo denegado",
+ "loan is disbursed": "Préstamo desembolsado",
+ "loan is opened": "Préstamo abierto",
+ "loan is recovered": "Préstamo recuperado",
+ "loan is written off": "Préstamo amortizado",
+ "Loan product id": "Identificador del producto de préstamo",
+ "Loan product operations": "",
+ "Loan products": "Productos de préstamo",
+ "Loan origination fee": "",
+ "Loan origination fee income account(Revenue accounts only)": "",
+ "Loan": "Préstamo",
+ "Loans in process ledger": "",
+ "LOCK": "BLOQUEAR",
+ "LOCKED": "BLOQUEADO",
+ "Loss provision": "",
+ "Loss provision configuration": "",
+ "Manage member loans": "Administrar préstamos de clientes",
+ "Manage member tasks": "Administrar tareas de clientes",
+ "Manage members": "Administrar clientes",
+ "Manage member deposit accounts": "",
+ "Manage deposit products": "",
+ "Manage documents": "",
+ "Manage employees": "Administrar empleados",
+ "Manage fees": "",
+ "Manage identification cards": "",
+ "Manage ledger accounts": "Administrar contabilidad",
+ "Manage loan accounts": "Administrar cuentas de préstamos",
+ "Manage Loan product fees": "Administrar cargos del producto de préstamo",
+ "Manage Loan products": "Administrar productos de préstamo",
+ "Manage loan products": "Administrar productos",
+ "Manage offices": "Administrar oficinas",
+ "Manage payments": "Administrar pagos",
+ "Manage roles and permissions": "Administrar roles y permisos",
+ "Manage roles": "Administrar roles",
+ "Manage task definitions": "Administrar definiciones de tareas",
+ "Manage tasks of this product": "Administrar tareas para este producto",
+ "Manage tasks": "Administrar tareas",
+ "Manage transaction types": "",
+ "Management": "Administración",
+ "Mandatory": "Obligatorio",
+ "Mandatory:": "Obligatorio:",
+ "Mandatory tasks(These tasks must be executed)": "",
+ "March": "Marzo",
+ "Maturity": "",
+ "Maximum dispersal amount": "Cantidad máxima de dispersión",
+ "Maximum dispersal count": "Conteo máximo de dispersión",
+ "Maximum interest rate": "Interés máximo",
+ "Maximum principal amount": "Cantidad máxima principal",
+ "Max digits": "",
+ "Max file size": "{{value}}KB",
+ "Max value": "",
+ "May": "Mayo",
+ "Message": "Mensaje",
+ "Message(Optional)": "Mensaje (opcional)",
+ "Middle name(optional)": "Segundo nombre (opcional)",
+ "Min value": "",
+ "Minimum balance": "",
+ "Minimum interest rate": "Interés mínimo",
+ "Minimum principal amount": "Cantidad mínima principal",
+ "Mobile(optional)": "Celular (opcional)",
+ "Monday": "Lunes",
+ "Month": "",
+ "MONTHS": "MESES",
+ "Monthly": "",
+ "Moratorium": "Mora",
+ "Multiple disbursals?": "¿Múltiples desembolsos?",
+ "Must be greater than minimum": "Debe ser mayor al mínimo",
+ "Must be greater than or equal minimum": "",
+ "Must have at least characters.": "{{value}}",
+ "Must have decimal places": "",
+ "Name": "Nombre",
+ "Navigate to ledger of this account": "Navegar a través del libro principal de ésta cuenta",
+ "Navigate to reference account": "Navegar en la cuenta de referencia",
+ "Navigation": "Navegación",
+ "Net Income (Loss)": "",
+ "New Password": "Nueva contraseña",
+ "Next repayment": "Próximo pagos",
+ "No account selected": "No se seleccionó cuenta",
+ "No account was found.": "No se encontró la cuenta",
+ "No address available": "Dirección no disponible",
+ "No contact details available": "No hay detalles de contacto disponibles",
+ "No member was found.": "Cliente no encontrado",
+ "No data for chart of accounts available.": "",
+ "No employee was found.": "Empleado no encontrado",
+ "No file selected yet.": "",
+ "No Headquarter found": "No se encontró oficina principal",
+ "No holders available": "No hay titulares disponibles",
+ "No identification card available": "No hay tarjeta de identificación disponible",
+ "No office assigned to employee, yet.": "No se ha asignado oficina al empleado aún.",
+ "No office assigned": "Oficina sin asignar",
+ "No office was found.": "No se encontró la oficina",
+ "No product selected": "No se seleccionó producto",
+ "No product was found.": "No se encontró el producto",
+ "No tasks available": "",
+ "No reference account available": "No hay referencia de cuenta disponible",
+ "No results to display.": "",
+ "No role assigned to employee, yet.": "No se ha asignado rol al empleado aún.",
+ "No role was found.": "No fue encontrado ningún rol.",
+ "No selection": "Sin seleccionar",
+ "No signature authorities available": "No hay autoridades de firma disponibles",
+ "None": "",
+ "Note(Optional)": "Nota (opcional)",
+ "Note value": "",
+ "November": "Noviembre",
+ "Number": "Numero",
+ "October": "Octubre",
+ "of": "",
+ "Office Address(optional)": "Dirección de oficina (opcional)",
+ "Office can't be deleted": "",
+ "Office details": "Detalles de oficina",
+ "Office has either branch offices, employees or teller assigned to it.": "",
+ "Offices": "Oficinas",
+ "Oh no, it looks like you don't have a headquarter created yet. No worries you can do it now!": "¡Oh no! No se ha creado una oficina principal aún. No se preocupe, podemos crearla ahora.",
+ "OK": "OK",
+ "on the": "el",
+ "on": "el",
+ "Only 32 characters allowed.": "Únicamente 32 caracteres permitidos",
+ "Only 5 figures allowed": "Sólo se permiten 5 caracteres",
+ "Only characters allowed.": "Sólo caracteres permitidos",
+ "Only numbers allowed.": "Sólo números permitidos",
+ "OPEN": "ABIERTO",
+ "Opened on": "",
+ "Options": "",
+ "Optional tasks": "",
+ "Page is going to be deleted": "",
+ "Page is going to be uploaded": "",
+ "Page number": "",
+ "Pages uploaded": "",
+ "Parent Ledger": "",
+ "Parent office": "Oficina padre",
+ "Passport": "Pasaporte",
+ "Password": "Contraseña",
+ "Passwords must match.": "Las contraseñas deben coincidir",
+ "Payment cycle": "Ciclo de pago",
+ "payment is accepted": "Pago aceptado",
+ "payment is late": "Pago atrasado",
+ "Payment": "Pago",
+ "Payments": "Pagos",
+ "PENDING": "PENDIENTE",
+ "Percent provision": "",
+ "Period": "Período",
+ "Phone(optional)": "Teléfono (opcional)",
+ "Planned payments": "Pagos programados",
+ "Please add minimum one debtor.": "",
+ "Please add minimum one creditor.": "",
+ "please change your password in order to access fims": "Por favor, modifique su contraseña para poder acceder a Fims",
+ "Please choose a different page number": "",
+ "Please enter a different identifier.": "Por favor, introducir un identificador distinto",
+ "Please make sure all pages are uploaded and are in sequence": "",
+ "Please verify the following tasks before you can action this member": "Por favor, verifique las siguientes tareas antes de activar éste cliente",
+ "Portrait can't exceed size of 512KB.": "",
+ "Portrait is going to be uploaded": "",
+ "Postal code(optional)": "Código postal (opcional)",
+ "Press the upload button below to upload the portrait.": "",
+ "Principal Amount": "Cantidad principal",
+ "Processing fee": "",
+ "Processing fee income account(Revenue accounts only)": "",
+ "Product details": "Detalles de producto",
+ "Product is already assigned to a member.": "",
+ "Product is going to be created": "Producto a ser creado",
+ "Product is going to be deleted": "",
+ "Product is going to be disabled": "Producto a ser deshabilitado",
+ "Product is going to be enabled": "Producto a ser habilitado",
+ "Product is going to be updated": "Producto a ser actualizado",
+ "Product not enabled": "",
+ "Product": "Producto",
+ "Proportional?": "",
+ "Quick access": "Acceso rápido",
+ "Read": "Leer",
+ "Recent activities": "Actividades recientes",
+ "Reference account": "Cuenta de referencia",
+ "Region(optional)": "Región (opcional)",
+ "Remaining principal": "Principal remanente",
+ "Remove": "Suprimir",
+ "Remove detail": "",
+ "REMOVE DOCUMENT": "",
+ "REMOVE FEE": "",
+ "Remove field": "",
+ "REMOVE STEP": "",
+ "Repay every": "Pagar cada",
+ "REPAY LOAN NOW": "REPAGAR PRÉSTAMO AHORA",
+ "Required": "Obligatorio",
+ "Required, Only 2 characters allowed": "Obligatorio, sólo 2 caracteres permitidos",
+ "Return disbursement": "",
+ "Resource not available": "Recurso no disponible",
+ "Results": "Resultados",
+ "Role": "",
+ "Roles": "Roles",
+ "Roles/Permissions": "Roles/Permisos",
+ "Role is going to be deleted": "",
+ "Role is going to be saved": "",
+ "Rows per page": "",
+ "Saturday": "Sábado",
+ "SAVE FEE": "GUARDAR CARGO",
+ "SAVE ROLE": "GUARDAR ROL",
+ "SAVE TASK": "GUARDAR TAREA",
+ "SAVE": "GUARDAR",
+ "Savings": "",
+ "Scans uploaded": "",
+ "Search beneficiary": "",
+ "Search results": "Buscar resultados",
+ "Search": "Búsqueda",
+ "Search...": "Búsqueda...",
+ "Second": "Segundo",
+ "Select loan product": "Elegir producto de préstamo",
+ "Select product": "",
+ "Select": "Seleccionar",
+ "Selected file:": "",
+ "September": "Setiembre",
+ "Service not available": "Servicio no disponible",
+ "Settings": "Configuraciones",
+ "Share": "",
+ "SHOW": "VER",
+ "Show accounts when displayed in chart?": "",
+ "Short name": "",
+ "sign in via your current account": "Registrarse a través de su cuenta actual",
+ "Sign In": "Registrarse",
+ "Sign Out": "Desconectarse",
+ "Signature Authorities": "Autoridades de firma",
+ "Signature authorities": "Autoridades de firma\n",
+ "Sorry, but you are not allowed to see this page.": "Lo sentimos, pero no está autorizado a ver ésta página",
+ "Sorry, that login did not work": "",
+ "Sorry, there was a problem executing your command": "",
+ "Sorry, there was a problem executing your task": "",
+ "Start date": "Fecha de inicio",
+ "State": "Estado",
+ "Street": "Calle",
+ "Subledger": "Sub libro principal",
+ "Subtotal": "",
+ "Sum of Debtors and sum of Creditors must match.": "",
+ "Sunday": "Domingo",
+ "Task details": "Detalles de tarea",
+ "Task is going to be created": "Tarea a ser creada",
+ "Task is going to be updated": "Tarea a ser actualizada",
+ "Task is going to be deleted": "",
+ "Task needs to be executed, before loan": "Tareas a ser ejecutadas antes del préstamo",
+ "Tasks": "Tareas",
+ "Teller account": "",
+ "Teller account(Asset accounts only)": "",
+ "Teller is not paused": "",
+ "Teller must be paused to create denominations": "",
+ "Tenant": "",
+ "Term": "Plazo",
+ "Term period": "",
+ "There was an error changing your password": "",
+ "Third": "Tercero",
+ "This product can be assigned to a member": "",
+ "This is a four eyes task and must be executed by somebody else than you.": "",
+ "This teller requires a denomination before it can be closed.": "",
+ "Thursday": "Jueves",
+ "To assign this product to a member it needs to be enabled first": "",
+ "Total": "Total",
+ "Total expenses": "",
+ "Transaction date": "Fecha de transacción",
+ "Transaction type": "",
+ "Trial balance": "Balance de comprobación",
+ "Tuesday": "Martes",
+ "Type": "Tipo",
+ "Unassign": "No designar",
+ "Unique, Required.": "Unico, requerido",
+ "UNLOCK": "DESBLOQUEAR",
+ "UPDATE ACCOUNT": "ACTUALIZAR CUENTA",
+ "Update custom field": "",
+ "UPDATE FEE": "ACTUALIZAR CARGO",
+ "UPDATE MEMBER LOAN": "ACTUALIZAR PRÉSTAMO DE CLIENTE",
+ "UPDATE MEMBER": "ACTUALIZAR CLIENTE",
+ "UPDATE DEPOSIT ACCOUNT": "",
+ "UPDATE DOCUMENT": "",
+ "UPDATE IDENTIFICATION CARD": "",
+ "UPDATE EMPLOYEE": "ACTUALIZAR EMPLEADO",
+ "UPDATE LEDGER": "ACTUALIZAR LIBRO PRINCIPAL",
+ "UPDATE LOSS CONFIGURATION": "",
+ "UPDATE OFFICE": "ACTUALIZAR OFICINA",
+ "UPDATE PRODUCT": "ACTUALIZAR PRODUCTO",
+ "UPDATE TASK": "ACTUALIZAR TAREA",
+ "UPDATE TRANSACTION TYPE": "",
+ "Upload new identification card scan": "",
+ "Upload new page": "",
+ "Upload portrait": "",
+ "Username": "Nombre de usuario",
+ "Valid for status changes to:": "Validar cambios de estado a:",
+ "Vault account": "",
+ "Vault account(Asset accounts only)": "",
+ "Value must be greater than or equal to": "El valor debe ser mayor o igual",
+ "Value must be greater than": "El valor debe ser mayor",
+ "Value must be smaller than or equal to": "El valor debe ser menor o igual",
+ "Value must be smaller than": "El valor debe ser menor",
+ "Value scale must be smaller than or equal to": "{{value}}",
+ "Values can't overlap with other values.": "",
+ "View": "",
+ "View accounts for this ledger": "Ver cuentas para este libro principal",
+ "View chart of accounts": "",
+ "View member": "Ver cliente",
+ "View identification cards": "",
+ "View income statement": "",
+ "View employees": "Ver empleados",
+ "View General Ledger": "Ver libro mayor",
+ "View headquarter office": "Ver oficina principal",
+ "View journal entries": "Ver diario de entradas",
+ "View payments": "Ver pagos",
+ "View roles": "Ver roles",
+ "View transaction types": "",
+ "View trial balance": "Ver balance de comprobación",
+ "View subledgers": "",
+ "We are very sorry, it seems there is a problem with our servers. Please contact your administrator if the problem occurs.": "Lo sentimos, pero hay un problema con nuestros servidores. Por favor contacte a su administrador si el problema vuelve a suceder.",
+ "Wednesday": "Miércoles",
+ "WEEKS": "SEMANAS",
+ "Write off": "Pedir por escrito",
+ "Year": "",
+ "YEARS": "AÑOS",
+ "You can lock this document": ""
+}
diff --git a/src/assets/images/ic_account_circle_black_48dp_2x.png b/src/assets/images/ic_account_circle_black_48dp_2x.png
new file mode 100644
index 0000000..c6b56c3
--- /dev/null
+++ b/src/assets/images/ic_account_circle_black_48dp_2x.png
Binary files differ
diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts
index 3612073..94eeec3 100644
--- a/src/environments/environment.prod.ts
+++ b/src/environments/environment.prod.ts
@@ -1,3 +1,21 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
export const environment = {
production: true
};
diff --git a/src/environments/environment.ts b/src/environments/environment.ts
index b7f639a..865aeb9 100644
--- a/src/environments/environment.ts
+++ b/src/environments/environment.ts
@@ -1,8 +1,21 @@
-// 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`.
-
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
export const environment = {
production: false
};
diff --git a/src/favicon.png b/src/favicon.png
new file mode 100644
index 0000000..85e8e66
--- /dev/null
+++ b/src/favicon.png
Binary files differ
diff --git a/src/index.html b/src/index.html
index d85cec1..354e928 100644
--- a/src/index.html
+++ b/src/index.html
@@ -1,16 +1,34 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
<!doctype html>
-<html lang="en">
+<html>
<head>
<meta charset="utf-8">
- <title>FineractCnWebApp</title>
+ <title>fineractcn-group-finance</title>
<base href="/">
-
+ <link rel="icon" type="image/png" href="favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1">
- <link rel="icon" type="image/x-icon" href="favicon.ico">
- <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
- <app-root></app-root>
-
+ <fims-app>
+ <div style="padding: 20%;text-align:center;">
+ Loading fineractcn-group-finance...
+ </div>
+ </fims-app>
</body>
</html>
diff --git a/src/main.ts b/src/main.ts
index 7042750..61db9db 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,9 +1,28 @@
-import { enableProdMode } from '@angular/core';
-import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
-import 'hammerjs';
-import 'material-design-icons/iconfont/material-icons.css'
-import { AppModule } from './app/app.module';
-import { environment } from './environments/environment';
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {enableProdMode} from '@angular/core';
+import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
+
+import {environment} from './environments/environment';
+import {AppModule} from './app/';
+// tslint:disable-next-line:no-import-side-effect
import './rxjs.imports';
if (environment.production) {
@@ -23,4 +42,4 @@
{ provide: 'reportingBaseUrl', useValue: '/api/reporting/v1' },
{ provide: 'chequeBaseUrl', useValue: '/api/cheques/v1' },
{ provide: 'payrollBaseUrl', useValue: '/api/payroll/v1' }
-]).bootstrapModule(AppModule);
\ No newline at end of file
+]).bootstrapModule(AppModule);
diff --git a/src/polyfills.ts b/src/polyfills.ts
index eb16e86..733aefe 100644
--- a/src/polyfills.ts
+++ b/src/polyfills.ts
@@ -1,84 +1,38 @@
/**
- * This file includes polyfills needed by Angular and is loaded before the app.
- * You can add your own extra polyfills to this file.
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
*
- * 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.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * 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
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
*/
-
-/***************************************************************************************************
- * 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';
- import 'core-js/es6/reflect';
+// This file includes polyfills needed by Angular 2 and is loaded before
+// the app. You can add your own extra polyfills to this file.
+/* tslint:disable:no-import-side-effect */
+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/set';
+import 'core-js/es6/reflect';
import 'core-js/es7/reflect';
-
-
-
-/** 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 'zone.js/dist/zone';
diff --git a/src/rxjs.imports.ts b/src/rxjs.imports.ts
index 5992076..3af5609 100644
--- a/src/rxjs.imports.ts
+++ b/src/rxjs.imports.ts
@@ -1,3 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+/* tslint:disable:no-import-side-effect */
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';
diff --git a/src/styles.scss b/src/styles.scss
index f14b648..84ece87 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -1,44 +1,37 @@
-/* You can add global styles to this file, and also import other style files */
-@import "~@angular/material/prebuilt-themes/indigo-pink.css";
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
-body{
- margin:0;
+// Custom style for loading elements without height
+.will-load {
+ min-height: 80px;
}
-.heading{
- margin-left:2%;
- padding-top:5px;
- padding-bottom: 5px;
- height:15px;
- }
-
-.fineract-button{
- margin-top:5px;
- margin-left:75%;
- width:150px;
+// Href line height wasn't right for md-icon-button
+a[md-icon-button] {
+ line-height: 36px;
}
-.main-div {
- display:flex;
- flex-direction: column;
- margin-left: 2%;
- margin-right:2%;
- border-radius: 5px 5px 5px 5px;
- background-color: white;
- min-height: 100%;
+// Stretch tab labels until there is an attribute for it
+.md-tab-label {
+ flex-grow: 1;
+}
- }
-
- .filter{
- width: 50%;
- }
-
- .page-space{
-
- margin-left: 2%;
- background-color: white;
-
- }
-
- .fineract-form{
- margin-left: 2%;
- }
\ No newline at end of file
+// Disable final step in stepper components
+td-steps > div:last-child > td-step-header {
+ pointer-events: none;
+}
diff --git a/src/test.ts b/src/test.ts
index 1631789..9c0c42f 100644
--- a/src/test.ts
+++ b/src/test.ts
@@ -1,13 +1,38 @@
-// This file is required by karma.conf.js and loads recursively all the .spec and framework files
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+/* tslint:disable:no-import-side-effect */
+import 'zone.js/dist/long-stack-trace-zone';
+import 'zone.js/dist/proxy.js';
+import 'zone.js/dist/sync-test';
+import 'zone.js/dist/jasmine-patch';
+import 'zone.js/dist/async-test';
+import 'zone.js/dist/fake-async-test';
+import './rxjs.imports';
+import {getTestBed} from '@angular/core/testing';
+import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing';
-import 'zone.js/dist/zone-testing';
-import { getTestBed } from '@angular/core/testing';
-import {
- BrowserDynamicTestingModule,
- platformBrowserDynamicTesting
-} from '@angular/platform-browser-dynamic/testing';
+// Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
+declare var __karma__: any;
+declare var require: any;
-declare const require: any;
+// Prevent Karma from running prematurely.
+__karma__.loaded = function (): void { /* noop */ };
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
@@ -18,3 +43,5 @@
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);
+// Finally, start Karma to run the tests.
+__karma__.start();
diff --git a/src/theme.scss b/src/theme.scss
new file mode 100644
index 0000000..ac0df48
--- /dev/null
+++ b/src/theme.scss
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+
+@import '~@angular/material/theming';
+@import '~@covalent/core/theming/all-theme';
+
+// Plus imports for other components in your app.
+
+// Include the base styles for Angular Material core. We include this here so that you only
+// have to load a single css file for Angular Material in your app.
+@include mat-core();
+
+// Define the palettes for your theme using the Material Design palettes available in palette.scss
+// (imported above). For each palette, you can optionally specify a default, lighter, and darker
+// hue.
+$primary: mat-palette($mat-green, 700);
+
+$accent: mat-palette($mat-blue, 800, A100, A400);
+
+// The warn palette is optional (defaults to red).
+$warn: mat-palette($mat-red, 600);
+
+// Create the theme object (a Sass map containing all of the palettes).
+$theme: mat-light-theme($primary, $accent, $warn);
+
+// Include theme styles for core and each component used in your app.
+// Alternatively, you can import and @include the theme mixins for each component
+// that you are using.
+@include angular-material-theme($theme);
+@include covalent-theme($theme);
+
+// Active icon color in list nav
+mat-nav-list {
+ [mat-list-item].active {
+ mat-icon[matlistavatar] {
+ background-color: mat-color($accent);
+ color: mat-color($accent, default-contrast)
+ }
+ mat-icon[matlisticon] {
+ color: mat-color($accent);
+ }
+ }
+}
diff --git a/src/tsconfig.app.json b/src/tsconfig.app.json
index 39ba8db..3c80422 100644
--- a/src/tsconfig.app.json
+++ b/src/tsconfig.app.json
@@ -2,12 +2,13 @@
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
- "baseUrl": "./",
"module": "es2015",
+ "baseUrl": "",
"types": []
},
"exclude": [
"test.ts",
- "**/*.spec.ts"
+ "**/*.spec.ts",
+ "app/common/testing/**"
]
}
diff --git a/src/tsconfig.spec.json b/src/tsconfig.spec.json
index ac22a29..510e3f1 100644
--- a/src/tsconfig.spec.json
+++ b/src/tsconfig.spec.json
@@ -2,8 +2,9 @@
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/spec",
- "baseUrl": "./",
"module": "commonjs",
+ "target": "es5",
+ "baseUrl": "",
"types": [
"jasmine",
"node"
diff --git a/src/typings.d.ts b/src/typings.d.ts
index ef5c7bd..c50e59b 100644
--- a/src/typings.d.ts
+++ b/src/typings.d.ts
@@ -1,4 +1,26 @@
-/* SystemJS module definition */
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// Typings reference file, see links for more information
+// https://github.com/typings/typings
+// https://www.typescriptlang.org/docs/handbook/writing-declaration-files.html
+
declare var module: NodeModule;
interface NodeModule {
id: string;
diff --git a/tsconfig.json b/tsconfig.json
index 2e2f5a9..ba8e68b 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,20 +1,21 @@
{
"compileOnSave": false,
"compilerOptions": {
- "outDir": "./dist/out-tsc",
"baseUrl": "src",
- "sourceMap": true,
"declaration": false,
- "moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
+ "lib": ["es6", "dom"],
+ "module": "es6",
+ "moduleResolution": "node",
+ "outDir": "../dist/out-tsc",
+ "sourceMap": true,
"target": "es5",
"typeRoots": [
- "node_modules/@types"
+ "./node_modules/@types"
],
- "lib": [
- "es2017",
- "dom"
+ "types": [
+ "jasmine", "hammerjs"
]
}
}
diff --git a/tslint.json b/tslint.json
index 9963d6c..ab27a82 100644
--- a/tslint.json
+++ b/tslint.json
@@ -11,15 +11,11 @@
"check-space"
],
"curly": true,
- "deprecation": {
- "severity": "warn"
- },
"eofline": true,
"forin": true,
"import-blacklist": [
true,
- "rxjs",
- "rxjs/Rx"
+ "rxjs"
],
"import-spacing": true,
"indent": [
@@ -35,14 +31,8 @@
"member-access": false,
"member-ordering": [
true,
- {
- "order": [
- "static-field",
- "instance-field",
- "static-method",
- "instance-method"
- ]
- }
+ "static-before-instance",
+ "variables-before-functions"
],
"no-arg": true,
"no-bitwise": true,
@@ -60,6 +50,7 @@
"no-empty": false,
"no-empty-interface": true,
"no-eval": true,
+ "no-import-side-effect": true,
"no-inferrable-types": [
true,
"ignore-params"
@@ -73,6 +64,9 @@
"no-trailing-whitespace": true,
"no-unnecessary-initializer": true,
"no-unused-expression": true,
+ "no-unused-variable": [
+ true
+ ],
"no-use-before-declare": true,
"no-var-keyword": true,
"object-literal-sort-keys": false,
@@ -90,7 +84,6 @@
],
"radix": true,
"semicolon": [
- true,
"always"
],
"triple-equals": [
@@ -107,6 +100,7 @@
"variable-declaration": "nospace"
}
],
+ "typeof-compare": true,
"unified-signatures": true,
"variable-name": false,
"whitespace": [
@@ -120,16 +114,15 @@
"directive-selector": [
true,
"attribute",
- "app",
+ "fims",
"camelCase"
],
"component-selector": [
true,
"element",
- "app",
+ "fims",
"kebab-case"
],
- "no-output-on-prefix": true,
"use-input-property-decorator": true,
"use-output-property-decorator": true,
"use-host-property-decorator": true,
@@ -138,6 +131,9 @@
"use-life-cycle-interface": true,
"use-pipe-transform-interface": true,
"component-class-suffix": true,
- "directive-class-suffix": true
+ "directive-class-suffix": true,
+ "no-access-missing-member": true,
+ "templates-use-public": true,
+ "invoke-injectable": true
}
}