diff --git a/.gitignore b/.gitignore index 34bbb74fe..abb54589a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # See http://help.github.com/ignore-files/ for more about ignoring files. +.env # Compiled output /dist diff --git a/Dockerfile b/Dockerfile index 570f4e469..3b393dc22 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,34 @@ +############### +### STAGE 1: Build app +############### FROM node:16-alpine as builder +RUN apk add --no-cache git + WORKDIR /usr/src/app ENV PATH /usr/src/app/node_modules/.bin:$PATH -COPY package.json /usr/src/app/package.json +COPY ./ /usr/src/app/ + +RUN npm cache clear --force + +RUN npm config set fetch-retry-maxtimeout 120000 RUN npm install --location=global @angular/cli@12.2.17 RUN npm install -COPY . . +RUN ng build --configuration production --output-path=/dist -RUN ng build --configuration production +############### +### STAGE 2: Serve app with nginx ### +############### +FROM nginx:alpine -FROM nginx:1.19.3 - -COPY --from=builder /usr/src/app/dist/web-app /usr/share/nginx/html +COPY --from=builder /dist /usr/share/nginx/html EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] +# When the container starts, replace the env.js with values from environment variables +CMD ["/bin/sh", "-c", "envsubst < /usr/share/nginx/html/assets/env.template.js > /usr/share/nginx/html/assets/env.js && exec nginx -g 'daemon off;'"] diff --git a/README.md b/README.md index bd0877f62..1f1f21e7c 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,59 @@ docker run -d -p 4200:80 openmf/web-app:latest Access the webapp on http://localhost:4200 in your browser. +### Docker compose +It is possible to do a 'one-touch' installation of Mifos X Web App using containers (AKA "Docker"). +Fineract now packs the mifos community-app web UI in it's docker deploy. + +As Prerequisites, you must have `docker` and `docker-compose` installed on your machine; see +[Docker Install](https://docs.docker.com/install/) and +[Docker Compose Install](https://docs.docker.com/compose/install/). + +Now to run a new MifosX Web App instance you can simply: + +1. `git clone https://github.com/openMF/web-app.git ; cd web-app` +1. for windows, use `git clone https://github.com/openMF/web-app.git --config core.autocrlf=input ; cd web-app` +1. `docker-compose up -d` +1. Access the webapp on http://localhost:4200 in your browser. + +You can also setup different confiurations for the MifosX Web App using environment variables: + +1. Use environment variables (best choice if you run with Docker Compose): + +Fineract backend settings +``` +FINERACT_API_URLS +``` +Value to set a Fineract server list (environments) to be used, Default value: +``` +https://dev.mifos.io,https://demo.mifos.io,https://qa.mifos.io,https://staging.mifos.io,https://mobile.mifos.io,https://demo.fineract.dev,https://localhost:8443 +``` + +``` +FINERACT_API_URL +``` +Default value used from the Fineract server list. Default value: +``` +https://localhost:8443 +``` + +``` +FINERACT_PLATFORM_TENANT_IDENTIFIER +``` +Fineract Tenant identifier to be used, It must be aligned with the Fineract `tenants` table. Default value: +``` +default +``` + +Setting for Languages (i18n) still under development +``` +MIFOS_DEFAULT_LANGUAGE=en-US +``` +``` +MIFOS_SUPPORTED_LANGUAGES=en-US,fr-FR +``` + +For more information look the env.sample file in the root directory of the project ## Want to help? [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/openMF/web-app/issues) diff --git a/angular.json b/angular.json index 623e2fb6b..694e13e9c 100644 --- a/angular.json +++ b/angular.json @@ -116,7 +116,7 @@ "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", - "karmaConfig": "src/karma.conf.js", + "karmaConfig": "src/karma.conf.ts", "polyfills": "src/polyfills.ts", "tsConfig": "src/tsconfig.spec.json", "scripts": [], diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..ccaf94cc9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3.5' + +services: + mifosx-web-app: + build: + context: . + dockerfile: Dockerfile + ports: + - '4200:80' + environment: + - FINERACT_API_URLS=https://dev.mifos.io,https://demo.mifos.io,https://qa.mifos.io,https://staging.mifos.io,https://mobile.mifos.io,https://demo.fineract.dev,https://localhost:8443 + - FINERACT_API_URL=https://localhost:8443 + - FINERACT_API_PROVIDER=/fineract-provider/api + - FINERACT_API_VERSION=/v1 + - FINERACT_PLATFORM_TENANT_IDENTIFIER=default + - MIFOS_DEFAULT_LANGUAGE=en-US + - MIFOS_SUPPORTED_LANGUAGES=en-US,fr-FR diff --git a/env.sample b/env.sample new file mode 100644 index 000000000..72a105baf --- /dev/null +++ b/env.sample @@ -0,0 +1,14 @@ + +FINERACT_API_URLS=https://dev.mifos.io,https://demo.mifos.io,https://qa.mifos.io,https://staging.mifos.io,https://mobile.mifos.io,https://demo.fineract.dev,https://localhost:8443 + +FINERACT_API_URL=https://localhost:8443 + +FINERACT_API_PROVIDER=/fineract-provider/api + +FINERACT_API_VERSION=/v1 + +FINERACT_PLATFORM_TENANT_IDENTIFIER=default + +MIFOS_DEFAULT_LANGUAGE=en-US + +MIFOS_SUPPORTED_LANGUAGES=en-US,fr-FR diff --git a/package-lock.json b/package-lock.json index 0993fe478..9aa749194 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,16 +53,19 @@ "@types/lodash": "4.14.132", "@types/node": "12.11.1", "codelyzer": "6.0.0", + "git-describe": "^4.1.1", "hads": "^3.0.3", "htmlhint": "0.11.0", "https-proxy-agent": "2.2.1", - "jasmine-core": "~3.5.0", + "jasmine-core": "^4.2.0", "jasmine-spec-reporter": "~5.0.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.1.0", "karma-cli": "2.0.0", + "karma-coverage": "^2.2.0", "karma-coverage-istanbul-reporter": "~3.0.2", - "karma-jasmine": "~4.0.0", + "karma-jasmine": "^5.1.0", + "karma-jasmine-html-reporter": "^2.0.0", "karma-junit-reporter": "1.2.0", "protractor": "~7.0.0", "puppeteer": "^15.3.2", @@ -3268,6 +3271,12 @@ "integrity": "sha512-6d8Q5fqS9DWOXEhMDiF6/2FjyHdmP/jSTAUyeQR7QwrFeNmYyzmvGxD5aLIHL445HjWgibs0eAig+KPnbaesXA==", "dev": true }, + "node_modules/@types/semver": { + "version": "7.3.10", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.10.tgz", + "integrity": "sha512-zsv3fsC7S84NN6nPK06u79oWgrPVd0NvOyqgghV1haPaFcVxIrP4DLomRwGAXk0ui4HZA7mOcSFL98sMVW9viw==", + "dev": true + }, "node_modules/@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", @@ -9191,6 +9200,32 @@ } ] }, + "node_modules/git-describe": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/git-describe/-/git-describe-4.1.1.tgz", + "integrity": "sha512-JC8ganO5kO80G8+XE98TDDjnMXQN3Estk3qdJuG2EGRF/l6zuMTMcN+8OSfQZ5FWpqIRLB015anWX4aSRgnxAQ==", + "dev": true, + "dependencies": { + "@types/semver": "^7.3.8", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=4.0.0" + }, + "optionalDependencies": { + "semver": "^5.6.0" + } + }, + "node_modules/git-describe/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -11258,9 +11293,9 @@ } }, "node_modules/jasmine-core": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", - "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.2.0.tgz", + "integrity": "sha512-OcFpBrIhnbmb9wfI8cqPSJ50pv3Wg4/NSgoZIqHzIwO/2a9qivJWzv8hUvaREIMYYJBas6AvfXATFdVuzzCqVw==", "dev": true }, "node_modules/jasmine-spec-reporter": { @@ -11599,6 +11634,23 @@ "node": ">= 6" } }, + "node_modules/karma-coverage": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.0.tgz", + "integrity": "sha512-gPVdoZBNDZ08UCzdMHHhEImKrw1+PAOQOIiffv1YsvxFhBjqvo/SVXNk4tqn1SYqX0BJZT6S/59zgxiBe+9OuA==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/karma-coverage-istanbul-reporter": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz", @@ -11615,26 +11667,79 @@ "url": "https://github.com/sponsors/mattlewis92" } }, - "node_modules/karma-jasmine": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-4.0.2.tgz", - "integrity": "sha512-ggi84RMNQffSDmWSyyt4zxzh2CQGwsxvYYsprgyR1j8ikzIduEdOlcLvXjZGwXG/0j41KUXOWsUCBfbEHPWP9g==", + "node_modules/karma-coverage/node_modules/istanbul-lib-instrument": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz", + "integrity": "sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A==", "dev": true, "dependencies": { - "jasmine-core": "^3.6.0" + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" }, "engines": { - "node": ">= 10" - }, - "peerDependencies": { - "karma": "*" + "node": ">=8" } }, - "node_modules/karma-jasmine/node_modules/jasmine-core": { - "version": "3.99.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz", - "integrity": "sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg==", - "dev": true + "node_modules/karma-coverage/node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma-coverage/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/karma-coverage/node_modules/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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma-jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", + "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", + "dev": true, + "dependencies": { + "jasmine-core": "^4.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "karma": "^6.0.0" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.0.0.tgz", + "integrity": "sha512-SB8HNNiazAHXM1vGEzf8/tSyEhkfxuDdhYdPBX2Mwgzt0OuF2gicApQ+uvXLID/gXyJQgvrM9+1/2SxZFUUDIA==", + "dev": true, + "peerDependencies": { + "jasmine-core": "^4.0.0", + "karma": "^6.0.0", + "karma-jasmine": "^5.0.0" + } }, "node_modules/karma-junit-reporter": { "version": "1.2.0", @@ -24940,6 +25045,12 @@ "integrity": "sha512-6d8Q5fqS9DWOXEhMDiF6/2FjyHdmP/jSTAUyeQR7QwrFeNmYyzmvGxD5aLIHL445HjWgibs0eAig+KPnbaesXA==", "dev": true }, + "@types/semver": { + "version": "7.3.10", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.10.tgz", + "integrity": "sha512-zsv3fsC7S84NN6nPK06u79oWgrPVd0NvOyqgghV1haPaFcVxIrP4DLomRwGAXk0ui4HZA7mOcSFL98sMVW9viw==", + "dev": true + }, "@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", @@ -29572,6 +29683,26 @@ } } }, + "git-describe": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/git-describe/-/git-describe-4.1.1.tgz", + "integrity": "sha512-JC8ganO5kO80G8+XE98TDDjnMXQN3Estk3qdJuG2EGRF/l6zuMTMcN+8OSfQZ5FWpqIRLB015anWX4aSRgnxAQ==", + "dev": true, + "requires": { + "@types/semver": "^7.3.8", + "lodash": "^4.17.21", + "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "optional": true + } + } + }, "glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -31183,9 +31314,9 @@ } }, "jasmine-core": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", - "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.2.0.tgz", + "integrity": "sha512-OcFpBrIhnbmb9wfI8cqPSJ50pv3Wg4/NSgoZIqHzIwO/2a9qivJWzv8hUvaREIMYYJBas6AvfXATFdVuzzCqVw==", "dev": true }, "jasmine-spec-reporter": { @@ -31513,6 +31644,58 @@ "resolve": "^1.3.3" } }, + "karma-coverage": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.0.tgz", + "integrity": "sha512-gPVdoZBNDZ08UCzdMHHhEImKrw1+PAOQOIiffv1YsvxFhBjqvo/SVXNk4tqn1SYqX0BJZT6S/59zgxiBe+9OuA==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + }, + "dependencies": { + "istanbul-lib-instrument": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz", + "integrity": "sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "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 + } + } + }, "karma-coverage-istanbul-reporter": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz", @@ -31527,22 +31710,21 @@ } }, "karma-jasmine": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-4.0.2.tgz", - "integrity": "sha512-ggi84RMNQffSDmWSyyt4zxzh2CQGwsxvYYsprgyR1j8ikzIduEdOlcLvXjZGwXG/0j41KUXOWsUCBfbEHPWP9g==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", + "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", "dev": true, "requires": { - "jasmine-core": "^3.6.0" - }, - "dependencies": { - "jasmine-core": { - "version": "3.99.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz", - "integrity": "sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg==", - "dev": true - } + "jasmine-core": "^4.1.0" } }, + "karma-jasmine-html-reporter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.0.0.tgz", + "integrity": "sha512-SB8HNNiazAHXM1vGEzf8/tSyEhkfxuDdhYdPBX2Mwgzt0OuF2gicApQ+uvXLID/gXyJQgvrM9+1/2SxZFUUDIA==", + "dev": true, + "requires": {} + }, "karma-junit-reporter": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/karma-junit-reporter/-/karma-junit-reporter-1.2.0.tgz", diff --git a/package.json b/package.json index 5089b1d22..98d668306 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,8 @@ "e2e": "npm run env -s && ng e2e", "translations:extract": "ngx-translate-extract --input ./src --output ./src/translations/template.json --format=json --clean -sort --marker extract", "docs": "hads ./docs -o", - "env": "ngx-scripts env npm_package_version", - "postinstall": "ngcc" + "env": "node version.js", + "postinstall": "node version.js && ngcc" }, "dependencies": { "@angular-devkit/schematics": "^14.0.4", @@ -69,16 +69,19 @@ "@types/lodash": "4.14.132", "@types/node": "12.11.1", "codelyzer": "6.0.0", + "git-describe": "^4.1.1", "hads": "^3.0.3", "htmlhint": "0.11.0", "https-proxy-agent": "2.2.1", - "jasmine-core": "~3.5.0", + "jasmine-core": "^4.2.0", "jasmine-spec-reporter": "~5.0.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.1.0", "karma-cli": "2.0.0", + "karma-coverage": "^2.2.0", "karma-coverage-istanbul-reporter": "~3.0.2", - "karma-jasmine": "~4.0.0", + "karma-jasmine": "^5.1.0", + "karma-jasmine-html-reporter": "^2.0.0", "karma-junit-reporter": "1.2.0", "protractor": "~7.0.0", "puppeteer": "^15.3.2", diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 2608b930a..6adf8bb44 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -21,7 +21,7 @@ const routes: Routes = [ * Configures the fallback route. */ @NgModule({ - imports: [RouterModule.forRoot(routes)], + imports: [RouterModule.forRoot(routes, { useHash: true })], exports: [RouterModule], providers: [] }) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 83bd3f0fb..207b10821 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { HttpClientModule } from '@angular/common/http'; +import { HttpClient, HttpClientModule } from '@angular/common/http'; import { ServiceWorkerModule } from '@angular/service-worker'; /** Environment Configuration */ @@ -38,7 +38,9 @@ import { TasksModule } from './tasks/tasks.module'; /** Main Routing Module */ import { AppRoutingModule } from './app-routing.module'; -import { DatePipe } from '@angular/common'; +import { DatePipe, LocationStrategy } from '@angular/common'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; /** * App Module @@ -47,6 +49,15 @@ import { DatePipe } from '@angular/common'; */ @NgModule({ imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: (http: HttpClient, locationStrategy: LocationStrategy) => { + return new TranslateHttpLoader(http, `${ window.location.protocol }//${ window.location.host }${locationStrategy.getBaseHref()}/assets/translations/`, '.json'); + }, + deps: [HttpClient, LocationStrategy] + } + }), BrowserModule, BrowserAnimationsModule, HttpClientModule, diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 381301e55..1ec8ed3a7 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -51,15 +51,7 @@ import { APP_BASE_HREF, LocationStrategy } from '@angular/common'; imports: [ SharedModule, HttpClientModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useFactory: (http: HttpClient, locationStrategy: LocationStrategy) => { - return new TranslateHttpLoader(http, `${ window.location.protocol }//${ window.location.host }${locationStrategy.getBaseHref()}/assets/translations/`, '.json'); - }, - deps: [HttpClient, LocationStrategy] - } - }), + TranslateModule.forRoot(), RouterModule ], declarations: [ diff --git a/src/app/core/http/api-prefix.interceptor.ts b/src/app/core/http/api-prefix.interceptor.ts index 9371577f0..5574075c1 100644 --- a/src/app/core/http/api-prefix.interceptor.ts +++ b/src/app/core/http/api-prefix.interceptor.ts @@ -6,23 +6,27 @@ import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/c import { Observable } from 'rxjs'; /** Environment Configuration */ -import { environment } from 'environments/environment'; +import { SettingsService } from 'app/settings/settings.service'; /** - * Http request interceptor to prefix a request with `environment.serverUrl`. + * Http request interceptor to prefix a request with `serverUrl`. */ @Injectable() export class ApiPrefixInterceptor implements HttpInterceptor { + /** + * @param {SettingsService} settingsService Settings Service + */ + constructor(private settingsService: SettingsService) {} /** - * Intercepts a Http request and prefixes it with `environment.serverUrl`. + * Intercepts a Http request and prefixes it with `serverUrl`. */ intercept(request: HttpRequest, next: HttpHandler): Observable> { /** * Ignore URLs that are complete for i18n */ if (!request.url.includes('http:') && !request.url.includes('https:')) { - request = request.clone({ url: environment.serverUrl + request.url }); + request = request.clone({ url: this.settingsService.serverUrl + request.url }); } return next.handle(request); diff --git a/src/app/core/i18n/i18n.service.ts b/src/app/core/i18n/i18n.service.ts index fe5af51f3..762735065 100644 --- a/src/app/core/i18n/i18n.service.ts +++ b/src/app/core/i18n/i18n.service.ts @@ -2,16 +2,11 @@ import { Injectable } from '@angular/core'; /** Translation Imports */ -import { TranslateService, LangChangeEvent } from '@ngx-translate/core'; +import { TranslateService } from '@ngx-translate/core'; /** Custom Services */ import { Logger } from '../logger/logger.service'; -/** Other Imports */ -import { includes } from 'lodash'; -import * as enUS from '../../../translations/en-US.json'; -import * as frFR from '../../../translations/fr-FR.json'; - /** Initialize Logger */ const log = new Logger('I18nService'); @@ -19,71 +14,18 @@ const log = new Logger('I18nService'); * Pass-through function to mark a string for translation extraction. * Running `npm translations:extract` will include the given string by using this. * @param {string} s The string to extract for translation. - * @return {string} The same string. + * @return {string} The translated string. */ export function extract(s: string) { - return s; + return I18nService.translate(s); } @Injectable() export class I18nService { - /** Key to store current language of application in local storage. */ - private languageStorageKey = 'mifosXLanguage'; - - defaultLanguage: string; - supportedLanguages: string[]; - constructor(private translateService: TranslateService) { } - /** - * Initializes i18n for the application. - * Loads language from local storage if present, or sets default language. - * @param {!string} defaultLanguage The default language to use. - * @param {Array.} supportedLanguages The list of supported languages. - */ - init(defaultLanguage: string, supportedLanguages: string[]) { - this.defaultLanguage = defaultLanguage; - this.supportedLanguages = supportedLanguages; - this.language = ''; - - this.translateService.onLangChange - .subscribe((event: LangChangeEvent) => { localStorage.setItem(this.languageStorageKey, event.lang); }); + public static translate(key: string): string { + return key; } - - /** - * Sets the current language. - * Note: The current language is saved to the local storage. - * If no parameter is specified, the language is loaded from local storage (if present). - * @param {string} language The IETF language code to set. - */ - set language(language: string) { - language = language || localStorage.getItem(this.languageStorageKey) || this.translateService.getBrowserCultureLang(); - let isSupportedLanguage = includes(this.supportedLanguages, language); - - // If no exact match is found, search without the region - if (language && !isSupportedLanguage) { - language = language.split('-')[0]; - language = this.supportedLanguages.find(supportedLanguage => supportedLanguage.startsWith(language)) || ''; - isSupportedLanguage = Boolean(language); - } - - // Fallback if language is not supported - if (!isSupportedLanguage) { - language = this.defaultLanguage; - } - - log.debug(`Language set to ${language}`); - localStorage.setItem(this.languageStorageKey, language); - this.translateService.setDefaultLang(language); - } - - /** - * Gets the current language. - * @return {string} The current language code. - */ - get language(): string { - return this.translateService.currentLang; - } - } diff --git a/src/app/core/utils/dates.ts b/src/app/core/utils/dates.ts index 043b7c24a..5f29fd694 100644 --- a/src/app/core/utils/dates.ts +++ b/src/app/core/utils/dates.ts @@ -1,6 +1,7 @@ import { DatePipe } from '@angular/common'; import { Injectable } from '@angular/core'; import { SettingsService } from 'app/settings/settings.service'; +import * as moment from 'moment'; @Injectable({ providedIn: 'root' @@ -10,13 +11,20 @@ export class Dates { constructor(private settingsService: SettingsService, private datePipe: DatePipe) {} - public getDate(timestamp: any) { + public getDate(timestamp: any): string { const dateFormat = this.settingsService.dateFormat; return this.datePipe.transform(timestamp, dateFormat); } - public formatDate(timestamp: any, dateFormat: string) { + public formatDate(timestamp: any, dateFormat: string): string { return this.datePipe.transform(timestamp, dateFormat); } + public parseDate(value: any): Date { + if (value instanceof Array) { + return moment(value.join('-'), 'YYYY-MM-DD').toDate(); + } else { + return moment(value).toDate(); + } + } } diff --git a/src/app/loans/loans-view/loan-documents-tab/loan-documents-tab.component.ts b/src/app/loans/loans-view/loan-documents-tab/loan-documents-tab.component.ts index 7e45a829c..da8f0281b 100644 --- a/src/app/loans/loans-view/loan-documents-tab/loan-documents-tab.component.ts +++ b/src/app/loans/loans-view/loan-documents-tab/loan-documents-tab.component.ts @@ -13,6 +13,7 @@ import { LoansService } from 'app/loans/loans.service'; /** Dialog Components */ import { LoanAccountLoadDocumentsDialogComponent } from 'app/loans/custom-dialog/loan-account-load-documents-dialog/loan-account-load-documents-dialog.component'; import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; +import { SettingsService } from 'app/settings/settings.service'; /** * Overdue charges tab component @@ -50,6 +51,7 @@ export class LoanDocumentsTabComponent implements OnInit { */ constructor(private route: ActivatedRoute, private loansService: LoansService, + private settingsService: SettingsService, public dialog: MatDialog) { this.route.data.subscribe((data: { loanDocuments: any }) => { this.getLoanDocumentsData(data.loanDocuments); @@ -72,7 +74,7 @@ export class LoanDocumentsTabComponent implements OnInit { getLoanDocumentsData(data: any) { data.forEach((ele: any) => { let loandocs = {}; - loandocs = environment.serverUrl + '/loans/' + ele.parentEntityId + '/documents/' + ele.id + '/attachment?tenantIdentifier=' + environment.fineractPlatformTenantId; + loandocs = this.settingsService.serverUrl + '/loans/' + ele.parentEntityId + '/documents/' + ele.id + '/attachment?tenantIdentifier=' + environment.fineractPlatformTenantId; ele.docUrl = loandocs; if (ele.fileName) { if (ele.fileName.toLowerCase().indexOf('.jpg') !== -1 || ele.fileName.toLowerCase().indexOf('.jpeg') !== -1 || ele.fileName.toLowerCase().indexOf('.png') !== -1) { diff --git a/src/app/pipes/date-format.pipe.spec.ts b/src/app/pipes/date-format.pipe.spec.ts index b9c6144c0..d9aef5171 100644 --- a/src/app/pipes/date-format.pipe.spec.ts +++ b/src/app/pipes/date-format.pipe.spec.ts @@ -1,8 +1,19 @@ +import { TestBed, waitForAsync } from '@angular/core/testing'; +import { SettingsService } from 'app/settings/settings.service'; import { DateFormatPipe } from './date-format.pipe'; describe('DateFormatPipe', () => { + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ SettingsService ], + declarations: [ DateFormatPipe ] + }) + .compileComponents(); + })); + it('create an instance', () => { - const pipe = new DateFormatPipe(); + const pipe = new DateFormatPipe(new SettingsService()); expect(pipe).toBeTruthy(); }); }); diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-interest-rate-chart-step/fixed-deposit-product-interest-rate-chart-step.component.html b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-interest-rate-chart-step/fixed-deposit-product-interest-rate-chart-step.component.html index 176eb2931..18c8a6258 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-interest-rate-chart-step/fixed-deposit-product-interest-rate-chart-step.component.html +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-interest-rate-chart-step/fixed-deposit-product-interest-rate-chart-step.component.html @@ -60,6 +60,10 @@ +
+

It's required to add at least one Slab

+
+ diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-interest-rate-chart-step/fixed-deposit-product-interest-rate-chart-step.component.ts b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-interest-rate-chart-step/fixed-deposit-product-interest-rate-chart-step.component.ts index d0189d692..132cd5461 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-interest-rate-chart-step/fixed-deposit-product-interest-rate-chart-step.component.ts +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-interest-rate-chart-step/fixed-deposit-product-interest-rate-chart-step.component.ts @@ -228,7 +228,7 @@ export class FixedDepositProductInterestRateChartStepComponent implements OnInit 'fromDate': ['', Validators.required], 'endDate': [''], 'isPrimaryGroupingByAmount': [false], - 'chartSlabs': this.formBuilder.array([]) + 'chartSlabs': this.formBuilder.array([], Validators.required) }); } diff --git a/src/app/settings/settings.service.ts b/src/app/settings/settings.service.ts index 413d2e481..fa4bf4089 100644 --- a/src/app/settings/settings.service.ts +++ b/src/app/settings/settings.service.ts @@ -1,5 +1,7 @@ /** Angular Imports */ import { Injectable } from '@angular/core'; +import { AlertService } from 'app/core/alert/alert.service'; +import { Dates } from 'app/core/utils/dates'; /** Environment Imports */ import { environment } from '../../environments/environment'; @@ -12,7 +14,12 @@ import { environment } from '../../environments/environment'; }) export class SettingsService { - constructor() { } + public static businessDateConfigName = 'enable_business_date'; + public static businessDateType = 'BUSINESS_DATE'; + public static cobDateType = 'COB_DATE'; + + constructor(private alertService: AlertService, + private dateUtils: Dates) { } /** * Sets date format setting throughout the app. @@ -42,7 +49,7 @@ export class SettingsService { * @param {string} url URL */ setServer(url: string) { - localStorage.setItem('mifosXServerURL', JSON.stringify(url)); + localStorage.setItem('mifosXServerURL', url); } /** @@ -81,7 +88,42 @@ export class SettingsService { * Returns server setting */ get server() { + if (localStorage.getItem('mifosXServerURL')) { + return localStorage.getItem('mifosXServerURL'); + } return environment.baseApiUrl; } + /** + * Returns server url with api path and version + */ + get serverUrl() { + return this.server + environment.apiProvider + environment.apiVersion; + } + + /** + * Validate If the enable_business_date configuration is enabled or disabled. + */ + validateBusinessDateStatus(configurations: any) { + configurations.some((config: any) => { + if (config.name === SettingsService.businessDateConfigName) { + return config.enabled; + } + }); + } + + /** + * Get the Business Date or COB Date. + */ + getBusinessDates(businessDateData: any, dateType: string): void { + businessDateData.some((data: any) => { + if (data.type === dateType) { + const dateVal = new Date(data.date); + this.alertService.alert({ type: dateType + ' Set', + message: this.dateUtils.formatDate(dateVal, this.dateFormat())}); + return; + } + }); + } + } diff --git a/src/app/shared/footer/footer.component.html b/src/app/shared/footer/footer.component.html index 29eba9b2e..57db56ffb 100644 --- a/src/app/shared/footer/footer.component.html +++ b/src/app/shared/footer/footer.component.html @@ -1,5 +1,7 @@ diff --git a/src/app/shared/footer/footer.component.ts b/src/app/shared/footer/footer.component.ts index ea6fa7501..4e70dce62 100644 --- a/src/app/shared/footer/footer.component.ts +++ b/src/app/shared/footer/footer.component.ts @@ -1,8 +1,14 @@ /** Angular Imports */ -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Alert } from 'app/core/alert/alert.model'; +import { AlertService } from 'app/core/alert/alert.service'; +import { AuthenticationService } from 'app/core/authentication/authentication.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { SystemService } from 'app/system/system.service'; /** Environment Configuration */ import { environment } from 'environments/environment'; +import { Subscription } from 'rxjs'; /** * Footer component. @@ -12,14 +18,73 @@ import { environment } from 'environments/environment'; templateUrl: './footer.component.html', styleUrls: ['./footer.component.scss'] }) -export class FooterComponent implements OnInit { +export class FooterComponent implements OnInit, OnDestroy { /** Mifos X version. */ version: string = environment.version; + /** Mifos X hash */ + hash: string = environment.hash; + /** Business Date */ + businessDate: Date = null; - constructor() { } + isBusinessDateEnabled = false; + isBusinessDateDefined = false; + /** Subscription to alerts. */ + alert$: Subscription; + timer: any; + + constructor(private systemService: SystemService, + private authenticationService: AuthenticationService, + private alertService: AlertService) { } ngOnInit() { + this.alert$ = this.alertService.alertEvent.subscribe((alertEvent: Alert) => { + const alertType = alertEvent.type; + if (alertType === SettingsService.businessDateType + ' Set Config') { + this.isBusinessDateEnabled = (alertEvent.message === 'enabled') ? true : false; + this.isBusinessDateDefined = false; + if (this.isBusinessDateEnabled) { + this.setBusinessDate(); + } + } else if (alertType === SettingsService.businessDateType + ' Set') { + if (this.isBusinessDateEnabled) { + this.setBusinessDate(); + } + } else if (alertType === 'Authentication Start') { + this.timer = setTimeout(() => { this.getConfigurations(); }, 60000); + } + }); + this.getConfigurations(); } + ngOnDestroy() { + clearTimeout(this.timer); + } + + /** + * Get the Configuration for Business Date + */ + getConfigurations(): void { + if (this.authenticationService.isAuthenticated()) { + this.systemService.getConfigurationByName(SettingsService.businessDateConfigName) + .subscribe((configurationData: any) => { + this.isBusinessDateEnabled = configurationData.enabled; + if (this.isBusinessDateEnabled) { + this.setBusinessDate(); + this.timer = setTimeout(() => { this.getConfigurations(); }, 60000); + } + }); + } + } + + /** + * Get the Business Date data + */ + setBusinessDate(): void { + this.systemService.getBusinessDate(SettingsService.businessDateType) + .subscribe((data: any) => { + this.businessDate = new Date(data.date); + this.isBusinessDateDefined = true; + }); + } } diff --git a/src/app/shared/language-selector/language-selector.component.html b/src/app/shared/language-selector/language-selector.component.html index 59e1f58b8..e895082ae 100644 --- a/src/app/shared/language-selector/language-selector.component.html +++ b/src/app/shared/language-selector/language-selector.component.html @@ -1,5 +1,5 @@ - Language + {{ 'Language' | translate }} {{ language }} diff --git a/src/app/shared/server-selector/server-selector.component.html b/src/app/shared/server-selector/server-selector.component.html index f7510dc0f..b4d1ab7f0 100644 --- a/src/app/shared/server-selector/server-selector.component.html +++ b/src/app/shared/server-selector/server-selector.component.html @@ -1,19 +1,19 @@ - - Server - + + {{ 'Server' | translate }} +
-
+
language - Add new server + {{ 'Add new server' | translate }} - Url is required + Url {{'is required' | translate }} - +
@@ -21,4 +21,4 @@ {{ server }}
-
\ No newline at end of file + diff --git a/src/app/shared/server-selector/server-selector.component.ts b/src/app/shared/server-selector/server-selector.component.ts index ce6fe82d7..2c4e1219f 100644 --- a/src/app/shared/server-selector/server-selector.component.ts +++ b/src/app/shared/server-selector/server-selector.component.ts @@ -26,34 +26,42 @@ export class ServerSelectorComponent implements OnInit { /** Server Setting */ serverSelector = new FormControl(''); + /** Server list to show */ + existMoreThanOneServer = false; + /** * @param {SettingsService} settingsService Settings Service */ - constructor(private settingsService: SettingsService, public dialog: MatDialog, private formBuilder: FormBuilder) { } + constructor(private settingsService: SettingsService, + public dialog: MatDialog, + private formBuilder: FormBuilder) { } ngOnInit(): void { this.servers = this.settingsService.servers; - this.serverSelector.patchValue(this.settingsService.server); - this.buildDependencies(); - this.form = this.formBuilder.group({ - 'url': ['', [Validators.required]], - }); + this.existMoreThanOneServer = (this.servers && this.servers.length > 1); + if (!this.existMoreThanOneServer) { + this.settingsService.setServer(this.servers[0]); + } else { + this.existMoreThanOneServer = true; + this.serverSelector.patchValue(this.settingsService.server); + this.form = this.formBuilder.group({ + 'url': ['', [Validators.required]], + }); + } } /** - * Subscribe to value changes. + * Set backend server from the list */ - buildDependencies() { - this.serverSelector.valueChanges.subscribe((url: string) => { - this.settingsService.setServer(url); - window.location.reload(); // refreshes the environment.ts. - }); + setServer(): void { + this.settingsService.setServer(this.serverSelector.value); } + /** - * Set new server. + * Add new server to the list. */ - setNewServer() { + addNewServer(): void { let servers; this.settingsService.setServer(this.form.value.url); servers = this.settingsService.servers; diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index ded7a5102..3bc9b5e06 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -25,6 +25,7 @@ import { ServerSelectorComponent } from './server-selector/server-selector.compo /** Custom Modules */ import { IconsModule } from './icons.module'; import { MaterialModule } from './material.module'; +import { TranslateModule } from '@ngx-translate/core'; /** * Shared Module @@ -36,7 +37,8 @@ import { MaterialModule } from './material.module'; CommonModule, IconsModule, MaterialModule, - ReactiveFormsModule + ReactiveFormsModule, + TranslateModule.forRoot(), ], declarations: [ FormfieldComponent, diff --git a/src/app/system/configurations/business-date-tab/business-date-tab.component.ts b/src/app/system/configurations/business-date-tab/business-date-tab.component.ts index f48e4a3be..9ad1a0518 100644 --- a/src/app/system/configurations/business-date-tab/business-date-tab.component.ts +++ b/src/app/system/configurations/business-date-tab/business-date-tab.component.ts @@ -3,8 +3,11 @@ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; +import { Alert } from 'app/core/alert/alert.model'; +import { AlertService } from 'app/core/alert/alert.service'; import { Dates } from 'app/core/utils/dates'; import { SettingsService } from 'app/settings/settings.service'; +import { Subscription } from 'rxjs'; /** Custom Services */ import { SystemService } from '../../system.service'; @@ -15,10 +18,8 @@ import { SystemService } from '../../system.service'; styleUrls: ['./business-date-tab.component.scss'] }) export class BusinessDateTabComponent implements OnInit { - - configurationName = 'enable_business_date'; - businessDateType = 'BUSINESS_DATE'; - cobDateType = 'COB_DATE'; + /** Subscription to alerts. */ + alert$: Subscription; /** Minimum date allowed. */ minDate = new Date(2000, 0, 1); @@ -30,8 +31,6 @@ export class BusinessDateTabComponent implements OnInit { cobDate = new Date(); /** business Date form. */ businessDateForm: FormGroup; - /** Configuration data. */ - configurations: any; /** Business data. */ businessDateData: any; @@ -42,17 +41,29 @@ export class BusinessDateTabComponent implements OnInit { /** * Retrieves the configurations data from `resolve`. - * @param {ActivatedRoute} route Activated Route. * @param {SystemService} systemService System Service. - * @param {Router} router Router for navigation. + * @param {SettingsService} settingsService Settings Service. + * @param {FormBuilder} formBuilder Form Builder. + * @param {Dates} dateUtils Date Utils. */ - constructor(private route: ActivatedRoute, + constructor( private systemService: SystemService, private settingsService: SettingsService, private formBuilder: FormBuilder, - private dateUtils: Dates) {} + private dateUtils: Dates, + private alertService: AlertService) {} ngOnInit(): void { + this.alert$ = this.alertService.alertEvent.subscribe((alertEvent: Alert) => { + const alertType = alertEvent.type; + if (alertType === SettingsService.businessDateType + ' Set Config') { + this.isBusinessDateEnabled = (alertEvent.message === 'enabled') ? true : false; + if (this.isBusinessDateEnabled) { + this.setBusinessDates(); + this.createBusinessDateForm(); + } + } + }); this.userDateFormat = this.settingsService.dateFormat; this.getConfigurations(); this.createBusinessDateForm(); @@ -61,31 +72,26 @@ export class BusinessDateTabComponent implements OnInit { /** * Get the Configuration and the Business Date data */ - getConfigurations(): void { - this.systemService.getConfigurations() + getConfigurations(): void { + this.systemService.getConfigurationByName(SettingsService.businessDateConfigName) .subscribe((configurationData: any) => { - this.configurations = configurationData.globalConfiguration; - this.validateBusinessDateStatus(); + this.isBusinessDateEnabled = configurationData.enabled; if (this.isBusinessDateEnabled) { - this.systemService.getBusinessDates() - .subscribe((businessDateData: any) => { - this.businessDateData = businessDateData; - this.setBusinessDates(); - }); + this.setBusinessDates(); } }); } - /** - * Set the Business Date and COB Date. - */ - setBusinessDates() { - this.businessDateData.forEach((data: any) => { - if (data.type === this.businessDateType) { - this.businessDate = new Date(data.date); - } else { - this.cobDate = new Date(data.date); - } + setBusinessDates(): void { + this.systemService.getBusinessDates() + .subscribe((businessDateData: any) => { + businessDateData.forEach((data: any) => { + if (data.type === SettingsService.businessDateType) { + this.businessDate = new Date(data.date); + } else { + this.cobDate = new Date(data.date); + } + }); }); } @@ -99,18 +105,6 @@ export class BusinessDateTabComponent implements OnInit { }); } - /** - * Validate If the enable_business_date configuration is enabled or disabled. - */ - validateBusinessDateStatus(): void { - this.isBusinessDateEnabled = false; - this.configurations.forEach((config: any) => { - if (config.name === this.configurationName) { - this.isBusinessDateEnabled = config.enabled; - } - }); - } - /** * Flag to display or not the datepicker control to set the Business Date value */ @@ -123,9 +117,9 @@ export class BusinessDateTabComponent implements OnInit { const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; const prevBusinessDate: Date = this.businessDateForm.value.businessDate; - let dateType = this.businessDateType; + let dateType = SettingsService.businessDateType; if (this.dateIndex === 1) { - dateType = this.cobDateType; + dateType = SettingsService.cobDateType; } const data = { date: this.dateUtils.formatDate(prevBusinessDate, dateFormat), diff --git a/src/app/system/configurations/configurations.component.html b/src/app/system/configurations/configurations.component.html index 8f79d8fed..abde27cfc 100644 --- a/src/app/system/configurations/configurations.component.html +++ b/src/app/system/configurations/configurations.component.html @@ -4,7 +4,7 @@ - + diff --git a/src/app/system/configurations/configurations.component.ts b/src/app/system/configurations/configurations.component.ts index 6af46138b..ee435da29 100644 --- a/src/app/system/configurations/configurations.component.ts +++ b/src/app/system/configurations/configurations.component.ts @@ -1,4 +1,9 @@ import { Component, OnInit } from '@angular/core'; +import { Alert } from 'app/core/alert/alert.model'; +import { AlertService } from 'app/core/alert/alert.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { Subscription } from 'rxjs'; +import { SystemService } from '../system.service'; @Component({ selector: 'mifosx-configurations', @@ -6,10 +11,31 @@ import { Component, OnInit } from '@angular/core'; styleUrls: ['./configurations.component.scss'] }) export class ConfigurationsComponent implements OnInit { + /** Subscription to alerts. */ + alert$: Subscription; - constructor() { } + isBusinessDateEnabled = false; + + constructor(private alertService: AlertService, + private systemService: SystemService) { } ngOnInit(): void { + this.alert$ = this.alertService.alertEvent.subscribe((alertEvent: Alert) => { + const alertType = alertEvent.type; + if (alertType === SettingsService.businessDateType + ' Set Config') { + this.isBusinessDateEnabled = (alertEvent.message === 'enabled') ? true : false; + } + }); + this.getConfigurations(); } + /** + * Get the Configuration and the Business Date data + */ + getConfigurations(): void { + this.systemService.getConfigurationByName(SettingsService.businessDateConfigName) + .subscribe((configurationData: any) => { + this.isBusinessDateEnabled = configurationData.enabled; + }); + } } diff --git a/src/app/system/configurations/global-configurations-tab/global-configurations-tab.component.ts b/src/app/system/configurations/global-configurations-tab/global-configurations-tab.component.ts index bc8c855da..2bf925cf3 100644 --- a/src/app/system/configurations/global-configurations-tab/global-configurations-tab.component.ts +++ b/src/app/system/configurations/global-configurations-tab/global-configurations-tab.component.ts @@ -3,7 +3,8 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; -import { ActivatedRoute, Router } from '@angular/router'; +import { AlertService } from 'app/core/alert/alert.service'; +import { SettingsService } from 'app/settings/settings.service'; /** Custom Services */ import { SystemService } from '../../system.service'; @@ -33,7 +34,7 @@ export class GlobalConfigurationsTabComponent implements OnInit { * @param {SystemService} systemService System Service. * @param {Router} router Router for navigation. */ - constructor(private route: ActivatedRoute, + constructor(private alertService: AlertService, private systemService: SystemService) { this.configurationData = this.systemService.getConfigurations(); } @@ -73,6 +74,10 @@ export class GlobalConfigurationsTabComponent implements OnInit { this.systemService.updateConfiguration(configuration.id, { enabled: !configuration.enabled }) .subscribe((response: any) => { configuration.enabled = response.changes.enabled; + if (configuration.name === SettingsService.businessDateConfigName) { + const msg = configuration.enabled ? 'enabled' : 'disabled'; + this.alertService.alert({type: SettingsService.businessDateType + ' Set Config', message: msg}); + } }); } } diff --git a/src/app/system/system.service.ts b/src/app/system/system.service.ts index 2c6faec84..5f76ce6d8 100644 --- a/src/app/system/system.service.ts +++ b/src/app/system/system.service.ts @@ -331,10 +331,17 @@ export class SystemService { /** * @returns {Observable} Business Date data. */ - getBusinessDates(): Observable { + getBusinessDates(): Observable { return this.http.get('/businessdate'); } + /** + * @returns {Observable} Business Date data using a type BUSINESS_DATE / COB_DATE. + */ + getBusinessDate(dateType: string): Observable { + return this.http.get(`/businessdate/${dateType}`); + } + /** * @param {any} dateData Business Date data to be updated. * @returns {Observable} @@ -358,6 +365,14 @@ export class SystemService { return this.http.get(`/configurations/${configurationId}`); } + /** + * @param {string} configurationId Configuration ID of configuration. + * @returns {Observable} Configuration. + */ + getConfigurationByName(configurationName: string): Observable { + return this.http.get(`/configurations/name/${configurationName}`); + } + /** * @param {string} configurationId Configuration ID of configuration to be updated. * @param {any} configuration Configuration to be updated. diff --git a/src/app/web-app.component.spec.ts b/src/app/web-app.component.spec.ts index 768631304..e37858f80 100644 --- a/src/app/web-app.component.spec.ts +++ b/src/app/web-app.component.spec.ts @@ -1,6 +1,5 @@ import { TestBed, async } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; -import { TranslateModule } from '@ngx-translate/core'; import { CoreModule } from './core/core.module'; import { WebAppComponent } from './web-app.component'; @@ -10,7 +9,6 @@ describe('AppComponent', () => { TestBed.configureTestingModule({ imports: [ RouterTestingModule, - TranslateModule.forRoot(), CoreModule ], declarations: [WebAppComponent], diff --git a/src/app/web-app.component.ts b/src/app/web-app.component.ts index c8caab22c..c309b3a45 100644 --- a/src/app/web-app.component.ts +++ b/src/app/web-app.component.ts @@ -49,6 +49,7 @@ export class WebAppComponent implements OnInit { * @param {ThemeStorageService} themeStorageService Theme Storage Service. * @param {MatSnackBar} snackBar Material Snackbar for notifications. * @param {AlertService} alertService Alert Service. + * @param {SettingsService} settingsService Settings Service. * @param {AuthenticationService} authenticationService Authentication service. */ constructor(private router: Router, @@ -82,7 +83,7 @@ export class WebAppComponent implements OnInit { log.debug('init'); // Setup translations - this.translateService.addLangs(environment.supportedLanguages); + this.translateService.addLangs(environment.supportedLanguages.split(',')); this.translateService.use(environment.defaultLanguage); // Change page title on navigation or language change, based on route data @@ -142,17 +143,8 @@ export class WebAppComponent implements OnInit { if (!localStorage.getItem('mifosXDateFormat')) { this.settingsService.setDateFormat('dd MMMM yyyy'); } - if (!localStorage.getItem('mifosXServers')) { - this.settingsService.setServers([ - 'https://dev.mifos.io', - 'https://demo.mifos.io', - 'https://qa.mifos.io', - 'https://staging.mifos.io', - 'https://mobile.mifos.io', - 'https://demo.fineract.dev', - 'https://localhost:8443' - ]); - } + // Set the server list from the env var FINERACT_API_URLS + this.settingsService.setServers(environment.baseApiUrls.split(',')); } logout() { diff --git a/src/assets/env.js b/src/assets/env.js new file mode 100644 index 000000000..7cff1087e --- /dev/null +++ b/src/assets/env.js @@ -0,0 +1,16 @@ +(function(window) { + window["env"] = window["env"] || {}; + + // BackEnd Environment variables + window["env"]["fineractApiUrls"] = 'https://localhost:8443'; + window["env"]["fineractApiUrl"] = 'https://localhost:8443'; + + window["env"]["apiProvider"] = '/fineract-provider/api'; + window["env"]["apiVersion"] = '/v1'; + + window["env"]["fineractPlatformTenantId"] = ''; + + // Language Environment variables + window["env"]["defaultLanguage"] = ''; + window["env"]["supportedLanguages"] = ''; +})(this); diff --git a/src/assets/env.template.js b/src/assets/env.template.js new file mode 100644 index 000000000..7ec7d9a0c --- /dev/null +++ b/src/assets/env.template.js @@ -0,0 +1,16 @@ +(function(window) { + window["env"] = window["env"] || {}; + + // BackEnd Environment variables + window["env"]["fineractApiUrls"] = '$FINERACT_API_URLS'; + window["env"]["fineractApiUrl"] = '$FINERACT_API_URL'; + + window["env"]["apiProvider"] = '$FINERACT_API_PROVIDER'; + window["env"]["apiVersion"] = '$FINERACT_API_VERSION'; + + window["env"]["fineractPlatformTenantId"] = '$FINERACT_PLATFORM_TENANT_IDENTIFIER'; + + // Language Environment variables + window["env"]["defaultLanguage"] = '$MIFOS_DEFAULT_LANGUAGE'; + window["env"]["supportedLanguages"] = '$MIFOS_SUPPORTED_LANGUAGES'; +})(this); diff --git a/src/assets/styles/_form.scss b/src/assets/styles/_form.scss index e7cfdcd43..ae4b4a6ae 100644 --- a/src/assets/styles/_form.scss +++ b/src/assets/styles/_form.scss @@ -27,3 +27,7 @@ textarea.noresize { .full-input [type="radio"] { width: auto; } + +.mat-tab-body-content { + margin-bottom: 20px; +} diff --git a/src/assets/translations/en-US.json b/src/assets/translations/en-US.json index 412335662..915d56710 100644 --- a/src/assets/translations/en-US.json +++ b/src/assets/translations/en-US.json @@ -16,5 +16,7 @@ "Reports": "Reports", "Admin": "Admin", "Self Service": "Self Service", - "Institution": "Institution" + "Institution": "Institution", + "Language": "Language", + "Server": "Server" } diff --git a/src/assets/translations/fr-FR.json b/src/assets/translations/fr-FR.json index c6bbf1699..fef1febe7 100644 --- a/src/assets/translations/fr-FR.json +++ b/src/assets/translations/fr-FR.json @@ -16,5 +16,7 @@ "Reports": "Rapports", "Admin": "Admin", "Self Service": "En libre service", - "Institution": "Institution" + "Institution": "Institution", + "Language": "Langue", + "Server": "Serveur" } diff --git a/src/environments/.env.ts b/src/environments/.env.ts index 34d9b36a8..e08c1f746 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -1,4 +1,10 @@ +// IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN! +/* tslint:disable */ export default { - 'mifos_x_version': '0.0.0', + 'mifos_x': { + 'version': '0.0.0', + 'hash': '79efe55' + }, 'allow_switching_backend_instance': true }; +/* tslint:enable */ diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index ff288db37..f94e21471 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -3,22 +3,25 @@ import env from './.env'; export const environment = { production: true, - version: env.mifos_x_version, - fineractPlatformTenantId: 'default', // For connecting to server running elsewhere update the tenant identifier - baseApiUrl: JSON.parse(localStorage.getItem('mifosXServerURL')) || 'https://demo.fineract.dev', // For connecting to server running elsewhere update the base API URL + version: env.mifos_x.version, + hash: env.mifos_x.hash, + // For connecting to server running elsewhere update the tenant identifier + fineractPlatformTenantId: window['env']['fineractPlatformTenantId'] || 'default', + // For connecting to others servers running elsewhere update the base API URL + baseApiUrls: window['env']['fineractApiUrls'] || + 'https://dev.mifos.io,https://demo.mifos.io,https://qa.mifos.io,https://staging.mifos.io,https://mobile.mifos.io,https://demo.fineract.dev,https://localhost:8443', + // For connecting to server running elsewhere set the base API URL + baseApiUrl: window['env']['baseApiUrl'] || 'https://demo.fineract.dev', allowServerSwitch: env.allow_switching_backend_instance, - apiProvider: '/fineract-provider/api', - apiVersion: '/v1', + apiProvider: window['env']['apiProvider'] || '/fineract-provider/api', + apiVersion: window['env']['apiVersion'] || '/v1', serverUrl: '', oauth: { enabled: false, // For connecting to Mifos X using OAuth2 Authentication change the value to true serverUrl: '' }, - defaultLanguage: 'en-US', - supportedLanguages: [ - 'en-US', - 'fr-FR' - ] + defaultLanguage: window['env']['defaultLanguage'] || 'en-US', + supportedLanguages: window['env']['supportedLanguages'] || 'en-US,fr-FR' }; // Server URL diff --git a/src/environments/environment.ts b/src/environments/environment.ts index b05c752d1..2b66735d5 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -8,22 +8,25 @@ import env from './.env'; export const environment = { production: false, - version: env.mifos_x_version, - fineractPlatformTenantId: 'default', // For connecting to server running elsewhere update the tenant identifier - baseApiUrl: JSON.parse(localStorage.getItem('mifosXServerURL')) || 'https://dev.mifos.io', // For connecting to server running elsewhere update the base API URL + version: env.mifos_x.version, + hash: env.mifos_x.hash, + // For connecting to server running elsewhere update the tenant identifier + fineractPlatformTenantId: window['env']['fineractPlatformTenantId'] || 'default', + // For connecting to others servers running elsewhere update the base API URL + baseApiUrls: window['env']['fineractApiUrls'] || + 'https://dev.mifos.io,https://demo.mifos.io,https://qa.mifos.io,https://staging.mifos.io,https://mobile.mifos.io,https://demo.fineract.dev,https://localhost:8443', + // For connecting to server running elsewhere set the base API URL + baseApiUrl: window['env']['baseApiUrl'] || 'https://demo.fineract.dev', allowServerSwitch: env.allow_switching_backend_instance, - apiProvider: '/fineract-provider/api', - apiVersion: '/v1', + apiProvider: window['env']['apiProvider'] || '/fineract-provider/api', + apiVersion: window['env']['apiVersion'] || '/v1', serverUrl: '', oauth: { enabled: false, // For connecting to Mifos X using OAuth2 Authentication change the value to true serverUrl: '' }, - defaultLanguage: 'en-US', - supportedLanguages: [ - 'en-US', - 'fr-FR' - ] + defaultLanguage: window['env']['defaultLanguage'] || 'en-US', + supportedLanguages: window['env']['supportedLanguages'] || 'en-US,fr-FR' }; // Server URL diff --git a/src/index.html b/src/index.html index 412caf2fd..ef5270584 100644 --- a/src/index.html +++ b/src/index.html @@ -16,6 +16,8 @@ + +