Merge branch 'spencer/transaction-list' into 'master'
User Interface Layout. See merge request grassrootseconomics/cic-staff-client!1
This commit is contained in:
commit
03be46e169
12
README.md
12
README.md
@ -1,9 +1,13 @@
|
|||||||
# CICADA
|
# CICADA
|
||||||
|
|
||||||
Angular web client for managing users and transactions in the CIC network.
|
An angular admin web client for managing users and transactions in the CIC network.
|
||||||
|
|
||||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.2.0.
|
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.2.0.
|
||||||
|
|
||||||
|
## Angular CLI
|
||||||
|
|
||||||
|
Run `npm install -g @angular/cli` to install the angular CLI.
|
||||||
|
|
||||||
## Development server
|
## Development server
|
||||||
|
|
||||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
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.
|
||||||
@ -28,6 +32,12 @@ Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.
|
|||||||
|
|
||||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
Environment variables are contained in the directory `src/environments/`
|
||||||
|
|
||||||
|
It contains environment variables for development on `environment.ts` and production on `environment.prod.ts`.
|
||||||
|
|
||||||
## Further help
|
## Further help
|
||||||
|
|
||||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
||||||
|
15
angular.json
15
angular.json
@ -7,7 +7,8 @@
|
|||||||
"projectType": "application",
|
"projectType": "application",
|
||||||
"schematics": {
|
"schematics": {
|
||||||
"@schematics/angular:component": {
|
"@schematics/angular:component": {
|
||||||
"style": "scss"
|
"style": "scss",
|
||||||
|
"changeDetection": "OnPush"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "",
|
"root": "",
|
||||||
@ -29,9 +30,16 @@
|
|||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
|
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
|
||||||
"src/styles.scss"
|
"src/styles.scss",
|
||||||
|
"node_modules/datatables.net-dt/css/jquery.dataTables.css",
|
||||||
|
"node_modules/bootstrap/dist/css/bootstrap.min.css"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": [
|
||||||
|
"node_modules/jquery/dist/jquery.js",
|
||||||
|
"node_modules/datatables.net/js/jquery.dataTables.js",
|
||||||
|
"node_modules/bootstrap/dist/js/bootstrap.js",
|
||||||
|
"node_modules/block-syncer/dist/worker_ondemand.js"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
@ -88,6 +96,7 @@
|
|||||||
"polyfills": "src/polyfills.ts",
|
"polyfills": "src/polyfills.ts",
|
||||||
"tsConfig": "tsconfig.spec.json",
|
"tsConfig": "tsconfig.spec.json",
|
||||||
"karmaConfig": "karma.conf.js",
|
"karmaConfig": "karma.conf.js",
|
||||||
|
"codeCoverage": true,
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/favicon.ico",
|
"src/favicon.ico",
|
||||||
"src/assets"
|
"src/assets"
|
||||||
|
26031
package-lock.json
generated
26031
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@ -7,7 +7,8 @@
|
|||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"test": "ng test",
|
"test": "ng test",
|
||||||
"lint": "ng lint",
|
"lint": "ng lint",
|
||||||
"e2e": "ng e2e"
|
"e2e": "ng e2e",
|
||||||
|
"postinstall": "node patch-webpack.js"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -21,17 +22,39 @@
|
|||||||
"@angular/platform-browser": "~10.2.0",
|
"@angular/platform-browser": "~10.2.0",
|
||||||
"@angular/platform-browser-dynamic": "~10.2.0",
|
"@angular/platform-browser-dynamic": "~10.2.0",
|
||||||
"@angular/router": "~10.2.0",
|
"@angular/router": "~10.2.0",
|
||||||
|
"@popperjs/core": "^2.5.4",
|
||||||
|
"angular-datatables": "^9.0.2",
|
||||||
|
"block-syncer": "^0.2.4",
|
||||||
|
"bootstrap": "^4.5.3",
|
||||||
|
"chart.js": "^2.9.4",
|
||||||
|
"cic-client": "^0.1.1",
|
||||||
|
"cic-client-meta": "0.0.7-alpha.3",
|
||||||
|
"datatables.net": "^1.10.22",
|
||||||
|
"datatables.net-dt": "^1.10.22",
|
||||||
|
"ethers": "^5.0.31",
|
||||||
|
"jquery": "^3.5.1",
|
||||||
|
"mocha": "^8.2.1",
|
||||||
|
"moolb": "^0.1.0",
|
||||||
|
"ng2-charts": "^2.4.2",
|
||||||
|
"ngx-logger": "^4.2.1",
|
||||||
"openpgp": "^4.10.10",
|
"openpgp": "^4.10.10",
|
||||||
|
"popper.js": "^1.16.1",
|
||||||
"rxjs": "~6.6.0",
|
"rxjs": "~6.6.0",
|
||||||
|
"sha3": "^2.1.4",
|
||||||
"tslib": "^2.0.0",
|
"tslib": "^2.0.0",
|
||||||
|
"vcard-parser": "^1.0.0",
|
||||||
|
"vcards-js": "^2.10.0",
|
||||||
|
"web3": "^1.3.0",
|
||||||
"zone.js": "~0.10.2"
|
"zone.js": "~0.10.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "~0.1002.0",
|
"@angular-devkit/build-angular": "~0.1002.0",
|
||||||
"@angular/cli": "~10.2.0",
|
"@angular/cli": "~10.2.0",
|
||||||
"@angular/compiler-cli": "~10.2.0",
|
"@angular/compiler-cli": "~10.2.0",
|
||||||
|
"@types/datatables.net": "^1.10.19",
|
||||||
"@types/jasmine": "~3.5.0",
|
"@types/jasmine": "~3.5.0",
|
||||||
"@types/jasminewd2": "~2.0.3",
|
"@types/jasminewd2": "~2.0.3",
|
||||||
|
"@types/jquery": "^3.5.4",
|
||||||
"@types/node": "^12.19.14",
|
"@types/node": "^12.19.14",
|
||||||
"codelyzer": "^6.0.0",
|
"codelyzer": "^6.0.0",
|
||||||
"jasmine-core": "~3.6.0",
|
"jasmine-core": "~3.6.0",
|
||||||
@ -43,6 +66,7 @@
|
|||||||
"karma-jasmine-html-reporter": "^1.5.0",
|
"karma-jasmine-html-reporter": "^1.5.0",
|
||||||
"karma-junit-reporter": "^2.0.1",
|
"karma-junit-reporter": "^2.0.1",
|
||||||
"protractor": "~7.0.0",
|
"protractor": "~7.0.0",
|
||||||
|
"secp256k1": "^4.0.2",
|
||||||
"ts-node": "~8.3.0",
|
"ts-node": "~8.3.0",
|
||||||
"tslint": "~6.1.0",
|
"tslint": "~6.1.0",
|
||||||
"typescript": "~4.0.2"
|
"typescript": "~4.0.2"
|
||||||
|
13
patch-webpack.js
Normal file
13
patch-webpack.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const f = 'node_modules/@angular-devkit/build-angular/src/angular-cli-files/models/webpack-configs/browser.js';
|
||||||
|
|
||||||
|
fs.readFile(f, 'utf8', function (err,data) {
|
||||||
|
if (err) {
|
||||||
|
return console.log(err);
|
||||||
|
}
|
||||||
|
let result = data.replace(/node: false/g, "node: {crypto: true, stream: true, fs: 'empty', net: 'empty'}");
|
||||||
|
|
||||||
|
fs.writeFile(f, result, 'utf8', function (err) {
|
||||||
|
if (err) return console.log(err);
|
||||||
|
});
|
||||||
|
});
|
53
src/app/_eth/accountIndex.ts
Normal file
53
src/app/_eth/accountIndex.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// @ts-ignore
|
||||||
|
import * as accountIndex from '@src/assets/js/block-sync/data/AccountRegistry.json';
|
||||||
|
import {environment} from '@src/environments/environment';
|
||||||
|
const Web3 = require('web3');
|
||||||
|
|
||||||
|
const web3 = new Web3(environment.web3Provider);
|
||||||
|
const abi = accountIndex.default;
|
||||||
|
|
||||||
|
export class AccountIndex {
|
||||||
|
contractAddress: string;
|
||||||
|
signerAddress: string;
|
||||||
|
contract: any;
|
||||||
|
|
||||||
|
constructor(contractAddress: string, signerAddress?: string) {
|
||||||
|
this.contractAddress = contractAddress;
|
||||||
|
this.contract = new web3.eth.Contract(abi, this.contractAddress);
|
||||||
|
if (signerAddress) {
|
||||||
|
this.signerAddress = signerAddress;
|
||||||
|
} else {
|
||||||
|
this.signerAddress = web3.eth.accounts[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async totalAccounts(): Promise<number> {
|
||||||
|
return await this.contract.methods.count().call();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async haveAccount(address: string): Promise<boolean> {
|
||||||
|
return await this.contract.methods.accountIndex(address).call() !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async last(numberOfAccounts: number): Promise<Array<string>> {
|
||||||
|
const count = await this.totalAccounts();
|
||||||
|
let lowest = count - numberOfAccounts - 1;
|
||||||
|
if (lowest < 0) {
|
||||||
|
lowest = 0;
|
||||||
|
}
|
||||||
|
let accounts = [];
|
||||||
|
for (let i = count - 1; i > lowest; i--) {
|
||||||
|
const account = await this.contract.methods.accounts(i).call();
|
||||||
|
accounts.push(account);
|
||||||
|
}
|
||||||
|
return accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async addToAccountRegistry(address: string): Promise<boolean> {
|
||||||
|
if (!await this.haveAccount(address)) {
|
||||||
|
return await this.contract.methods.add(address).send({from: this.signerAddress});
|
||||||
|
} else {
|
||||||
|
return await this.haveAccount(address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
src/app/_eth/index.ts
Normal file
3
src/app/_eth/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from '@app/_eth/accountIndex';
|
||||||
|
export * from '@app/_eth/registry';
|
||||||
|
export * from '@app/_eth/token-registry';
|
8
src/app/_eth/registry.spec.ts
Normal file
8
src/app/_eth/registry.spec.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Registry } from '@app/_eth/registry';
|
||||||
|
import {environment} from '@src/environments/environment';
|
||||||
|
|
||||||
|
describe('Registry', () => {
|
||||||
|
it('should create an instance', () => {
|
||||||
|
expect(new Registry(environment.registryAddress)).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
32
src/app/_eth/registry.ts
Normal file
32
src/app/_eth/registry.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// @ts-ignore
|
||||||
|
import * as registry from '@src/assets/js/block-sync/data/Registry.json';
|
||||||
|
import {environment} from '@src/environments/environment';
|
||||||
|
const Web3 = require('web3');
|
||||||
|
|
||||||
|
const web3 = new Web3(environment.web3Provider);
|
||||||
|
const abi = registry.default;
|
||||||
|
|
||||||
|
export class Registry {
|
||||||
|
contractAddress: string;
|
||||||
|
signerAddress: string;
|
||||||
|
contract: any;
|
||||||
|
|
||||||
|
constructor(contractAddress: string, signerAddress?: string) {
|
||||||
|
this.contractAddress = contractAddress;
|
||||||
|
this.contract = new web3.eth.Contract(abi, contractAddress);
|
||||||
|
if (signerAddress) {
|
||||||
|
this.signerAddress = signerAddress;
|
||||||
|
} else {
|
||||||
|
this.signerAddress = web3.eth.accounts[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async owner(): Promise<string> {
|
||||||
|
return await this.contract.methods.owner().call();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async addressOf(identifier: string): Promise<string> {
|
||||||
|
const id = '0x' + web3.utils.padRight(new Buffer(identifier).toString('hex'), 64);
|
||||||
|
return await this.contract.methods.addressOf(id).call();
|
||||||
|
}
|
||||||
|
}
|
8
src/app/_eth/token-registry.spec.ts
Normal file
8
src/app/_eth/token-registry.spec.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { TokenRegistry } from '@app/_eth/token-registry';
|
||||||
|
import {environment} from '@src/environments/environment';
|
||||||
|
|
||||||
|
describe('TokenRegistry', () => {
|
||||||
|
it('should create an instance', () => {
|
||||||
|
expect(new TokenRegistry(environment.registryAddress)).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
36
src/app/_eth/token-registry.ts
Normal file
36
src/app/_eth/token-registry.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// @ts-ignore
|
||||||
|
import * as registryClient from '@src/assets/js/block-sync/data/RegistryClient.json';
|
||||||
|
import Web3 from 'web3';
|
||||||
|
import {environment} from '@src/environments/environment';
|
||||||
|
|
||||||
|
const web3 = new Web3(environment.web3Provider);
|
||||||
|
const abi = registryClient.default;
|
||||||
|
|
||||||
|
export class TokenRegistry {
|
||||||
|
contractAddress: string;
|
||||||
|
signerAddress: string;
|
||||||
|
contract: any;
|
||||||
|
|
||||||
|
constructor(contractAddress: string, signerAddress?: string) {
|
||||||
|
this.contractAddress = contractAddress;
|
||||||
|
this.contract = new web3.eth.Contract(abi, this.contractAddress);
|
||||||
|
if (signerAddress) {
|
||||||
|
this.signerAddress = signerAddress;
|
||||||
|
} else {
|
||||||
|
this.signerAddress = web3.eth.accounts[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async totalTokens(): Promise<number> {
|
||||||
|
return await this.contract.methods.registryCount().call();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async entry(serial: number): Promise<string> {
|
||||||
|
return await this.contract.methods.entry(serial).call();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async addressOf(identifier: string): Promise<string> {
|
||||||
|
const id = '0x' + web3.utils.padRight(new Buffer(identifier).toString('hex'), 64);
|
||||||
|
return await this.contract.methods.addressOf(id).call();
|
||||||
|
}
|
||||||
|
}
|
@ -12,11 +12,11 @@ export class AuthGuard implements CanActivate {
|
|||||||
canActivate(
|
canActivate(
|
||||||
route: ActivatedRouteSnapshot,
|
route: ActivatedRouteSnapshot,
|
||||||
state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
||||||
if (localStorage.getItem(atob('CICADA_USER'))) {
|
if (sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.router.navigate(['/auth'], { queryParams: { returnUrl: state.url }});
|
this.router.navigate(['/auth']);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
5
src/app/_helpers/array-sum.ts
Normal file
5
src/app/_helpers/array-sum.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export class ArraySum {
|
||||||
|
static arraySum(arr: any[]): number {
|
||||||
|
return arr.reduce((accumulator, current) => accumulator + current, 0);
|
||||||
|
}
|
||||||
|
}
|
7
src/app/_helpers/global-error-handler.spec.ts
Normal file
7
src/app/_helpers/global-error-handler.spec.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { GlobalErrorHandler } from './global-error-handler';
|
||||||
|
|
||||||
|
describe('GlobalErrorHandler', () => {
|
||||||
|
it('should create an instance', () => {
|
||||||
|
// expect(new GlobalErrorHandler()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
60
src/app/_helpers/global-error-handler.ts
Normal file
60
src/app/_helpers/global-error-handler.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import {ErrorHandler, Injectable} from '@angular/core';
|
||||||
|
import {LoggingService} from '@app/_services/logging.service';
|
||||||
|
import {HttpErrorResponse} from '@angular/common/http';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GlobalErrorHandler extends ErrorHandler {
|
||||||
|
private sentencesForWarningLogging: string[] = [];
|
||||||
|
|
||||||
|
constructor(private loggingService: LoggingService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleError(error: any): void {
|
||||||
|
this.logError(error);
|
||||||
|
const message = error.message ? error.message : error.toString();
|
||||||
|
|
||||||
|
if (error.status) {
|
||||||
|
error = new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorTraceString = `Error message:\n${message}.\nStack trace: ${error.stack}`;
|
||||||
|
|
||||||
|
const isWarning = this.isWarning(errorTraceString);
|
||||||
|
if (isWarning) {
|
||||||
|
this.loggingService.sendWarnLevelMessage(errorTraceString, {error});
|
||||||
|
} else {
|
||||||
|
this.loggingService.sendErrorLevelMessage(errorTraceString, this, {error});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
logError(error: any): void {
|
||||||
|
if (error instanceof HttpErrorResponse) {
|
||||||
|
this.loggingService.sendErrorLevelMessage(
|
||||||
|
`There was an HTTP error. ${error.message}, Status code: ${(error as HttpErrorResponse).status}`, this, {error});
|
||||||
|
} else if (error instanceof TypeError) {
|
||||||
|
this.loggingService.sendErrorLevelMessage(`There was a Type error. ${error.message}`, this, {error});
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
this.loggingService.sendErrorLevelMessage(`There was a general error. ${error.message}`, this, {error});
|
||||||
|
} else {
|
||||||
|
this.loggingService.sendErrorLevelMessage('Nobody threw an error but something happened!', this, {error});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isWarning(errorTraceString: string): boolean {
|
||||||
|
let isWarning = true;
|
||||||
|
if (errorTraceString.includes('/src/app/')) {
|
||||||
|
isWarning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sentencesForWarningLogging.forEach((whiteListSentence) => {
|
||||||
|
if (errorTraceString.includes(whiteListSentence)) {
|
||||||
|
isWarning = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return isWarning;
|
||||||
|
}
|
||||||
|
}
|
18
src/app/_helpers/http-getter.ts
Normal file
18
src/app/_helpers/http-getter.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
function HttpGetter(): void {}
|
||||||
|
|
||||||
|
HttpGetter.prototype.get = filename => new Promise((whohoo, doh) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.addEventListener('load', (e) => {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
whohoo(xhr.responseText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
doh('failed with status ' + xhr.status + ': ' + xhr.statusText);
|
||||||
|
});
|
||||||
|
xhr.open('GET', filename);
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
|
||||||
|
export {
|
||||||
|
HttpGetter
|
||||||
|
};
|
@ -1,4 +1,6 @@
|
|||||||
export * from '@app/_helpers/custom.validator';
|
export * from '@app/_helpers/custom.validator';
|
||||||
export * from '@app/_helpers/error.interceptor';
|
|
||||||
export * from '@app/_helpers/custom-error-state-matcher';
|
export * from '@app/_helpers/custom-error-state-matcher';
|
||||||
export * from '@app/_helpers/pgp-key-store';
|
export * from '@app/_helpers/mock-backend';
|
||||||
|
export * from '@app/_helpers/array-sum';
|
||||||
|
export * from '@app/_helpers/http-getter';
|
||||||
|
export * from '@app/_helpers/global-error-handler';
|
||||||
|
219
src/app/_helpers/mock-backend.ts
Normal file
219
src/app/_helpers/mock-backend.ts
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
import {HTTP_INTERCEPTORS, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse} from '@angular/common/http';
|
||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {Observable, of, throwError} from 'rxjs';
|
||||||
|
import {delay, dematerialize, materialize, mergeMap} from 'rxjs/operators';
|
||||||
|
|
||||||
|
const accounts = [
|
||||||
|
{id: 1, name: 'John Doe', phone: '+25412345678', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', type: 'user', age: 43, created: '08/16/2020', balance: '12987', failedPinAttempts: 1, status: 'active', bio: 'Bodaboda', category: 'transport', gender: 'male', location: 'Bofu', locationType: 'Rural', token: 'RSV', referrer: '+25412341234'},
|
||||||
|
{id: 2, name: 'Jane Buck', phone: '+25412341234', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9866', type: 'vendor', age: 25, created: '04/02/2020', balance: '56281', failedPinAttempts: 0, status: 'active', bio: 'Groceries', category: 'food/water', gender: 'female', location: 'Lindi', locationType: 'Urban', token: 'ERN', referrer: ''},
|
||||||
|
{id: 3, name: 'Mc Donald', phone: '+25498765432', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9867', type: 'group', age: 31, created: '11/16/2020', balance: '450', failedPinAttempts: 2, status: 'blocked', bio: 'Food', category: 'food/water', gender: 'male', location: 'Miyani', locationType: 'Rural', token: 'RSV', referrer: '+25498769876'},
|
||||||
|
{id: 4, name: 'Hera Cles', phone: '+25498769876', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9868', type: 'user', age: 38, created: '05/28/2020', balance: '5621', failedPinAttempts: 3, status: 'active', bio: 'Shop', category: 'shop', gender: 'female', location: 'Kayaba', locationType: 'Urban', token: 'BRT', referrer: '+25412341234'},
|
||||||
|
{id: 5, name: 'Silver Fia', phone: '+25462518374', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9869', type: 'tokenAgent', age: 19, created: '10/10/2020', balance: '817', failedPinAttempts: 0, status: 'blocked', bio: 'Electronics', category: 'shop', gender: 'male', location: 'Mkanyeni', locationType: 'Rural', token: 'RSV', referrer: '+25412345678'},
|
||||||
|
];
|
||||||
|
|
||||||
|
const actions = [
|
||||||
|
{ id: 1, user: 'Tom', role: 'enroller', action: 'Disburse RSV 100', approval: false },
|
||||||
|
{ id: 2, user: 'Christine', role: 'admin', action: 'Change user phone number', approval: true },
|
||||||
|
{ id: 3, user: 'Will', role: 'superadmin', action: 'Reclaim RSV 1000', approval: true },
|
||||||
|
{ id: 4, user: 'Vivian', role: 'enroller', action: 'Complete user profile', approval: true },
|
||||||
|
{ id: 5, user: 'Jack', role: 'enroller', action: 'Reclaim RSV 200', approval: false },
|
||||||
|
{ id: 6, user: 'Patience', role: 'enroller', action: 'Change user information', approval: false }
|
||||||
|
];
|
||||||
|
|
||||||
|
const histories = [
|
||||||
|
{ id: 1, userId: 3, userName: 'Mc Donald', action: 'Receive RSV 100', staff: 'Tom', timestamp: Date.now() },
|
||||||
|
{ id: 2, userId: 5, userName: 'Silver Fia', action: 'Change phone number from +25412345678 to +25498765432', staff: 'Christine', timestamp: Date.now()},
|
||||||
|
{ id: 3, userId: 4, userName: 'Hera Cles', action: 'Completed user profile', staff: 'Vivian', timestamp: Date.now() },
|
||||||
|
];
|
||||||
|
|
||||||
|
const locations: any = [
|
||||||
|
{ name: 'Kwale',
|
||||||
|
districts: [
|
||||||
|
{ name: 'Kinango',
|
||||||
|
locations: [
|
||||||
|
{ name: 'Bofu', villages: ['Bofu', 'Chidzuvini', 'Mkanyeni']},
|
||||||
|
{ name: 'Mnyenzeni', villages: ['Miloeni', 'Miyani', 'Mnyenzeni', 'Vikolani', 'Vitangani']}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ name: 'Nairobi',
|
||||||
|
districts: [
|
||||||
|
{ name: 'Dagorreti',
|
||||||
|
locations: [
|
||||||
|
{ name: 'Kawangware', villages: ['Congo']},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ name: 'Ngong',
|
||||||
|
locations: [
|
||||||
|
{ name: 'Kibera', villages: ['Kibera', 'Lindi']},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ name: 'South B',
|
||||||
|
locations: [
|
||||||
|
{ name: 'Mukuru', villages: ['Kayaba']},
|
||||||
|
{ name: 'South B', villages: ['South B']},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const staffMembers = [
|
||||||
|
{ id: 1, name: 'admin@acme.org', accountType: 'Admin', created: '17/11/2020', status: 'activated'},
|
||||||
|
{ id: 2, name: 'will@grassecon.org', accountType: 'SuperAdmin', created: '17/11/2020', status: 'activated'},
|
||||||
|
{ id: 3, name: 'spence@grassecon.org', accountType: 'Enroller', created: '17/11/2020', status: 'activated'},
|
||||||
|
{ id: 4, name: 'admin@redcross.org', accountType: 'View', created: '17/11/2020', status: 'activated'}
|
||||||
|
];
|
||||||
|
|
||||||
|
const tokens = [
|
||||||
|
{name: 'Giftable Reserve', symbol: 'GRZ', address: '0xa686005CE37Dce7738436256982C3903f2E4ea8E', supply: '1000000001000000000000000000', decimals: '18', reserves: {}},
|
||||||
|
{name: 'Demo Token', symbol: 'DEMO', address: '0xc80D6aFF8194114c52AEcD84c9f15fd5c8abb187', supply: '99999999999999998976', decimals: '18', reserves: {'0xa686005CE37Dce7738436256982C3903f2E4ea8E': {weight: '1000000', balance: '99999999999999998976'}}, reserveRatio: '1000000', owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a'},
|
||||||
|
{name: 'Foo Token', symbol: 'FOO', address: '0x9ceD86089f7aBB5A97B40eb0E7521e7aa308d354', supply: '1000000000000000001014', decimals: '18', reserves: {'0xa686005CE37Dce7738436256982C3903f2E4ea8E': {weight: '1000000', balance: '1000000000000000001014'}}, reserveRatio: '1000000', owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a'},
|
||||||
|
{name: 'testb', symbol: 'tstb', address: '0xC63cFA91A3BFf41cE31Ff436f67D3ACBC977DB95', supply: '99000', decimals: '18', reserves: {'0xa686005CE37Dce7738436256982C3903f2E4ea8E': {weight: '1000000', balance: '99000'}}, reserveRatio: '1000000', owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a'},
|
||||||
|
{name: 'testa', symbol: 'tsta', address: '0x8fA4101ef19D0a078239d035659e92b278bD083C', supply: '9981', decimals: '18', reserves: {'0xa686005CE37Dce7738436256982C3903f2E4ea8E': {weight: '1000000', balance: '9981'}}, reserveRatio: '1000000', owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a'},
|
||||||
|
{name: 'testc', symbol: 'tstc', address: '0x4A6fA6bc3BfE4C9661bC692D9798425350C9e3D4', supply: '100990', decimals: '18', reserves: {'0xa686005CE37Dce7738436256982C3903f2E4ea8E': {weight: '1000000', balance: '100990'}}, reserveRatio: '1000000', owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a'}
|
||||||
|
];
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MockBackendInterceptor implements HttpInterceptor {
|
||||||
|
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
const { url, method, headers, body } = request;
|
||||||
|
|
||||||
|
// wrap in delayed observable to simulate server api call\
|
||||||
|
// call materialize and dematerialize to ensure delay even is thrown
|
||||||
|
return of(null)
|
||||||
|
.pipe(mergeMap(handleRoute))
|
||||||
|
.pipe(materialize())
|
||||||
|
.pipe(delay(500))
|
||||||
|
.pipe(dematerialize());
|
||||||
|
|
||||||
|
function handleRoute(): Observable<any> {
|
||||||
|
switch (true) {
|
||||||
|
case url.endsWith('/accounts') && method === 'GET':
|
||||||
|
return getAccounts();
|
||||||
|
case url.match(/\/accounts\/\d+$/) && method === 'GET':
|
||||||
|
return getAccountById();
|
||||||
|
case url.endsWith('/actions') && method === 'GET':
|
||||||
|
return getActions();
|
||||||
|
case url.match(/\/actions\/\d+$/) && method === 'GET':
|
||||||
|
return getActionById();
|
||||||
|
case url.match(/\/actions\/\d+$/) && method === 'POST':
|
||||||
|
return approveAction();
|
||||||
|
case url.match(/\/history\/\d+$/) && method === 'GET':
|
||||||
|
return getHistoryByUser();
|
||||||
|
case url.endsWith('/locations') && method === 'GET':
|
||||||
|
return getLocations();
|
||||||
|
case url.endsWith('/staff') && method === 'GET':
|
||||||
|
return getStaff();
|
||||||
|
case url.match(/\/staff\/\d+$/) && method === 'GET':
|
||||||
|
return getStaffById();
|
||||||
|
case url.match(/\/staff\/\d+$/) && method === 'POST' && body.status !== undefined:
|
||||||
|
return changeStaffStatus();
|
||||||
|
case url.match(/\/staff\/\d+$/) && method === 'POST' && body.accountType !== undefined:
|
||||||
|
return changeStaffType();
|
||||||
|
case url.endsWith('/tokens') && method === 'GET':
|
||||||
|
return getTokens();
|
||||||
|
case url.match(/\/tokens\/\w+$/) && method === 'GET':
|
||||||
|
return getTokenBySymbol();
|
||||||
|
default:
|
||||||
|
// pass through any requests not handled above
|
||||||
|
return next.handle(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// route functions
|
||||||
|
|
||||||
|
function getAccounts(): Observable<any> {
|
||||||
|
return ok(accounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAccountById(): Observable<any> {
|
||||||
|
const queriedAccount = accounts.find(account => account.id === idFromUrl());
|
||||||
|
return ok(queriedAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActions(): Observable<any> {
|
||||||
|
return ok(actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActionById(): Observable<any> {
|
||||||
|
const queriedAction = actions.find(action => action.id === idFromUrl());
|
||||||
|
return ok(queriedAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
function approveAction(): Observable<any> {
|
||||||
|
const queriedAction = actions.find(action => action.id === idFromUrl());
|
||||||
|
queriedAction.approval = body.approval;
|
||||||
|
const message = `Action approval status set to ${body.approval} successfully!`;
|
||||||
|
return ok(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHistoryByUser(): Observable<any> {
|
||||||
|
const queriedUserHistory = histories.filter(history => history.userId === idFromUrl());
|
||||||
|
return ok(queriedUserHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLocations(): Observable<any> {
|
||||||
|
return ok(locations);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStaff(): Observable<any> {
|
||||||
|
return ok(staffMembers);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStaffById(): Observable<any> {
|
||||||
|
const queriedStaff = staffMembers.find(staff => staff.id === idFromUrl());
|
||||||
|
return ok(queriedStaff);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeStaffStatus(): Observable<any> {
|
||||||
|
const queriedStaff = staffMembers.find(staff => staff.id === idFromUrl());
|
||||||
|
queriedStaff.status = body.status;
|
||||||
|
const message = `Staff account status changed to ${body.status} successfully!`;
|
||||||
|
return ok(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeStaffType(): Observable<any> {
|
||||||
|
const queriedStaff = staffMembers.find(staff => staff.id === idFromUrl());
|
||||||
|
queriedStaff.accountType = body.accountType;
|
||||||
|
const message = `Staff account type changed to ${body.accountType} successfully!`;
|
||||||
|
return ok(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTokens(): Observable<any> {
|
||||||
|
return ok(tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTokenBySymbol(): Observable<any> {
|
||||||
|
const queriedToken = tokens.find(token => token.symbol === stringFromUrl());
|
||||||
|
return ok(queriedToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper functions
|
||||||
|
|
||||||
|
function ok(body): Observable<any> {
|
||||||
|
return of(new HttpResponse({ status: 200, body }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(message): Observable<any> {
|
||||||
|
return throwError({ status: 400, error: { message } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function idFromUrl(): number {
|
||||||
|
const urlParts = url.split('/');
|
||||||
|
return parseInt(urlParts[urlParts.length - 1], 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringFromUrl(): string {
|
||||||
|
const urlParts = url.split('/');
|
||||||
|
return urlParts[urlParts.length - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MockBackendProvider = {
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useClass: MockBackendInterceptor,
|
||||||
|
multi: true
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { ErrorInterceptor } from '@app/_helpers/error.interceptor';
|
import { ErrorInterceptor } from '@app/_interceptors/error.interceptor';
|
||||||
|
|
||||||
describe('ErrorInterceptor', () => {
|
describe('ErrorInterceptor', () => {
|
||||||
beforeEach(() => TestBed.configureTestingModule({
|
beforeEach(() => TestBed.configureTestingModule({
|
@ -3,23 +3,27 @@ import {
|
|||||||
HttpRequest,
|
HttpRequest,
|
||||||
HttpHandler,
|
HttpHandler,
|
||||||
HttpEvent,
|
HttpEvent,
|
||||||
HttpInterceptor
|
HttpInterceptor, HttpErrorResponse
|
||||||
} from '@angular/common/http';
|
} from '@angular/common/http';
|
||||||
import {Observable, throwError} from 'rxjs';
|
import {Observable, throwError} from 'rxjs';
|
||||||
import {catchError} from 'rxjs/operators';
|
import {catchError} from 'rxjs/operators';
|
||||||
|
import {ErrorDialogService} from '@app/_services';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ErrorInterceptor implements HttpInterceptor {
|
export class ErrorInterceptor implements HttpInterceptor {
|
||||||
|
|
||||||
constructor() {}
|
constructor(private errorDialogService: ErrorDialogService) {}
|
||||||
|
|
||||||
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
|
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
|
||||||
return next.handle(request).pipe(catchError(err => {
|
return next.handle(request).pipe(catchError((err: HttpErrorResponse) => {
|
||||||
|
this.errorDialogService.openDialog({
|
||||||
|
message: err.error.message || err.statusText,
|
||||||
|
status: err.status
|
||||||
|
});
|
||||||
if ([401, 403].indexOf(err.status) !== -1) {
|
if ([401, 403].indexOf(err.status) !== -1) {
|
||||||
location.reload(true);
|
location.reload(true);
|
||||||
}
|
}
|
||||||
const error = err.error.message || err.statusText;
|
return throwError(err);
|
||||||
return throwError(error);
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
16
src/app/_interceptors/http-config.interceptor.spec.ts
Normal file
16
src/app/_interceptors/http-config.interceptor.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { HttpConfigInterceptor } from './http-config.interceptor';
|
||||||
|
|
||||||
|
describe('HttpConfigInterceptor', () => {
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
HttpConfigInterceptor
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const interceptor: HttpConfigInterceptor = TestBed.inject(HttpConfigInterceptor);
|
||||||
|
expect(interceptor).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
29
src/app/_interceptors/http-config.interceptor.ts
Normal file
29
src/app/_interceptors/http-config.interceptor.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {
|
||||||
|
HttpRequest,
|
||||||
|
HttpHandler,
|
||||||
|
HttpEvent,
|
||||||
|
HttpInterceptor
|
||||||
|
} from '@angular/common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class HttpConfigInterceptor implements HttpInterceptor {
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
|
||||||
|
const token = sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'));
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
request = request.clone({headers: request.headers.set('Authorization', 'Bearer ' + token)});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request.headers.has('Content-Type')) {
|
||||||
|
request = request.clone({headers: request.headers.set('Content-Type', 'application/json')});
|
||||||
|
}
|
||||||
|
|
||||||
|
request = request.clone({headers: request.headers.set('Accept', 'application/json')});
|
||||||
|
return next.handle(request);
|
||||||
|
}
|
||||||
|
}
|
3
src/app/_interceptors/index.ts
Normal file
3
src/app/_interceptors/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from '@app/_interceptors/error.interceptor';
|
||||||
|
export * from '@app/_interceptors/http-config.interceptor';
|
||||||
|
export * from '@app/_interceptors/logging.interceptor';
|
16
src/app/_interceptors/logging.interceptor.spec.ts
Normal file
16
src/app/_interceptors/logging.interceptor.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LoggingInterceptor } from './logging.interceptor';
|
||||||
|
|
||||||
|
describe('LoggingInterceptor', () => {
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
LoggingInterceptor
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const interceptor: LoggingInterceptor = TestBed.inject(LoggingInterceptor);
|
||||||
|
expect(interceptor).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
37
src/app/_interceptors/logging.interceptor.ts
Normal file
37
src/app/_interceptors/logging.interceptor.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {
|
||||||
|
HttpRequest,
|
||||||
|
HttpHandler,
|
||||||
|
HttpEvent,
|
||||||
|
HttpInterceptor,
|
||||||
|
HttpResponse
|
||||||
|
} from '@angular/common/http';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
import {LoggingService} from '@app/_services/logging.service';
|
||||||
|
import {finalize, tap} from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LoggingInterceptor implements HttpInterceptor {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private loggingService: LoggingService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
|
||||||
|
this.loggingService.sendInfoLevelMessage(request);
|
||||||
|
const startTime = Date.now();
|
||||||
|
let status: string;
|
||||||
|
|
||||||
|
return next.handle(request).pipe(tap(event => {
|
||||||
|
status = '';
|
||||||
|
if (event instanceof HttpResponse) {
|
||||||
|
status = 'succeeded';
|
||||||
|
}
|
||||||
|
}, error => status = 'failed'),
|
||||||
|
finalize(() => {
|
||||||
|
const elapsedTime = Date.now() - startTime;
|
||||||
|
const message = `${request.method} request for ${request.urlWithParams} ${status} in ${elapsedTime} ms`;
|
||||||
|
this.loggingService.sendInfoLevelMessage(message);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
96
src/app/_models/account.ts
Normal file
96
src/app/_models/account.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
export interface AccountDetails {
|
||||||
|
date_registered: number;
|
||||||
|
gender: string;
|
||||||
|
age?: string;
|
||||||
|
type?: string;
|
||||||
|
identities: {
|
||||||
|
evm: {
|
||||||
|
'bloxberg:8996': string[];
|
||||||
|
'oldchain:1': string[];
|
||||||
|
};
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
};
|
||||||
|
location: {
|
||||||
|
area?: string;
|
||||||
|
area_name: string;
|
||||||
|
area_type?: string;
|
||||||
|
};
|
||||||
|
products: string[];
|
||||||
|
category?: string;
|
||||||
|
vcard: {
|
||||||
|
email: [{
|
||||||
|
value: string;
|
||||||
|
}];
|
||||||
|
fn: [{
|
||||||
|
value: string;
|
||||||
|
}];
|
||||||
|
n: [{
|
||||||
|
value: string[];
|
||||||
|
}];
|
||||||
|
tel: [{
|
||||||
|
meta: {
|
||||||
|
TYP: string[];
|
||||||
|
},
|
||||||
|
value: string;
|
||||||
|
}],
|
||||||
|
version: [{
|
||||||
|
value: string;
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Signature {
|
||||||
|
algo: string;
|
||||||
|
data: string;
|
||||||
|
digest: string;
|
||||||
|
engine: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Meta {
|
||||||
|
data: AccountDetails;
|
||||||
|
id: string;
|
||||||
|
signature: Signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetaResponse {
|
||||||
|
id: string;
|
||||||
|
m: Meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultAccount: AccountDetails = {
|
||||||
|
date_registered: Date.now(),
|
||||||
|
gender: 'other',
|
||||||
|
identities: {
|
||||||
|
evm: {
|
||||||
|
'bloxberg:8996': [''],
|
||||||
|
'oldchain:1': [''],
|
||||||
|
},
|
||||||
|
latitude: 0,
|
||||||
|
longitude: 0,
|
||||||
|
},
|
||||||
|
location: {
|
||||||
|
area_name: 'Kilifi',
|
||||||
|
},
|
||||||
|
products: [],
|
||||||
|
vcard: {
|
||||||
|
email: [{
|
||||||
|
value: '',
|
||||||
|
}],
|
||||||
|
fn: [{
|
||||||
|
value: 'GE',
|
||||||
|
}],
|
||||||
|
n: [{
|
||||||
|
value: ['GE'],
|
||||||
|
}],
|
||||||
|
tel: [{
|
||||||
|
meta: {
|
||||||
|
TYP: [],
|
||||||
|
},
|
||||||
|
value: '',
|
||||||
|
}],
|
||||||
|
version: [{
|
||||||
|
value: '3.0',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
};
|
4
src/app/_models/index.ts
Normal file
4
src/app/_models/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from '@app/_models/transaction';
|
||||||
|
export * from '@app/_models/settings';
|
||||||
|
export * from '@app/_models/user';
|
||||||
|
export * from '@app/_models/account';
|
18
src/app/_models/settings.ts
Normal file
18
src/app/_models/settings.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export class Settings {
|
||||||
|
w3: W3 = {
|
||||||
|
engine: undefined,
|
||||||
|
provider: undefined,
|
||||||
|
};
|
||||||
|
scanFilter: any;
|
||||||
|
registry: any;
|
||||||
|
txHelper: any;
|
||||||
|
|
||||||
|
constructor(scanFilter: any) {
|
||||||
|
this.scanFilter = scanFilter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class W3 {
|
||||||
|
engine: any;
|
||||||
|
provider: any;
|
||||||
|
}
|
2
src/app/_models/staff.ts
Normal file
2
src/app/_models/staff.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export interface Staff {
|
||||||
|
}
|
15
src/app/_models/token.ts
Normal file
15
src/app/_models/token.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export interface Token {
|
||||||
|
name: string;
|
||||||
|
symbol: string;
|
||||||
|
address: string;
|
||||||
|
supply: string;
|
||||||
|
decimals: string;
|
||||||
|
reserves: {
|
||||||
|
'0xa686005CE37Dce7738436256982C3903f2E4ea8E'?: {
|
||||||
|
weight: string;
|
||||||
|
balance: string;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reserveRatio?: string;
|
||||||
|
owner?: string;
|
||||||
|
}
|
43
src/app/_models/transaction.ts
Normal file
43
src/app/_models/transaction.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import {User} from '@app/_models/user';
|
||||||
|
|
||||||
|
export class BlocksBloom {
|
||||||
|
low: number;
|
||||||
|
blockFilter: string;
|
||||||
|
blocktxFilter: string;
|
||||||
|
alg: string;
|
||||||
|
filterRounds: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Token {
|
||||||
|
address: string;
|
||||||
|
name: string;
|
||||||
|
symbol: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Tx {
|
||||||
|
block: number;
|
||||||
|
success: boolean;
|
||||||
|
timestamp: number;
|
||||||
|
txHash: string;
|
||||||
|
txIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Transaction {
|
||||||
|
from: string;
|
||||||
|
sender: User;
|
||||||
|
to: string;
|
||||||
|
recipient: User;
|
||||||
|
token: Token;
|
||||||
|
tx: Tx;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Conversion {
|
||||||
|
destinationToken: Token;
|
||||||
|
fromValue: number;
|
||||||
|
sourceToken: Token;
|
||||||
|
toValue: number;
|
||||||
|
trader: string;
|
||||||
|
user: User;
|
||||||
|
tx: Tx;
|
||||||
|
}
|
22
src/app/_models/user.ts
Normal file
22
src/app/_models/user.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
export class User {
|
||||||
|
dateRegistered: number;
|
||||||
|
vcard: {
|
||||||
|
fn: string;
|
||||||
|
version: string;
|
||||||
|
tel: [{
|
||||||
|
meta: {
|
||||||
|
TYP: string;
|
||||||
|
};
|
||||||
|
value: string[];
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
key: {
|
||||||
|
ethereum: string[];
|
||||||
|
};
|
||||||
|
location: {
|
||||||
|
latitude: string;
|
||||||
|
longitude: string;
|
||||||
|
external: {};
|
||||||
|
};
|
||||||
|
selling: string[];
|
||||||
|
}
|
2
src/app/_pgp/index.ts
Normal file
2
src/app/_pgp/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from '@app/_pgp/pgp-key-store';
|
||||||
|
export * from '@app/_pgp/pgp-signer';
|
@ -1,4 +1,4 @@
|
|||||||
import { MutablePgpKeyStore } from '@app/_helpers/pgp-key-store';
|
import { MutablePgpKeyStore } from '@app/_pgp/pgp-key-store';
|
||||||
|
|
||||||
describe('PgpKeyStore', () => {
|
describe('PgpKeyStore', () => {
|
||||||
it('should create an instance', () => {
|
it('should create an instance', () => {
|
@ -1,6 +1,6 @@
|
|||||||
|
import { KeyStore } from 'cic-client-meta';
|
||||||
const openpgp = require('openpgp');
|
const openpgp = require('openpgp');
|
||||||
const keyring = new openpgp.Keyring();
|
const keyring = new openpgp.Keyring();
|
||||||
import { KeyStore } from '@src/assets/js/cic-meta/auth';
|
|
||||||
|
|
||||||
interface MutableKeyStore extends KeyStore {
|
interface MutableKeyStore extends KeyStore {
|
||||||
loadKeyring(): Promise<void>;
|
loadKeyring(): Promise<void>;
|
9
src/app/_pgp/pgp-signer.spec.ts
Normal file
9
src/app/_pgp/pgp-signer.spec.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { PGPSigner } from '@app/_pgp/pgp-signer';
|
||||||
|
import {MutableKeyStore, MutablePgpKeyStore} from '@app/_pgp/pgp-key-store';
|
||||||
|
const keystore: MutableKeyStore = new MutablePgpKeyStore();
|
||||||
|
|
||||||
|
describe('PgpSigner', () => {
|
||||||
|
it('should create an instance', () => {
|
||||||
|
expect(new PGPSigner(keystore)).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
110
src/app/_pgp/pgp-signer.ts
Normal file
110
src/app/_pgp/pgp-signer.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import {MutableKeyStore} from '@app/_pgp/pgp-key-store';
|
||||||
|
import {LoggingService} from '@app/_services/logging.service';
|
||||||
|
|
||||||
|
const openpgp = require('openpgp');
|
||||||
|
|
||||||
|
interface Signable {
|
||||||
|
digest(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Signature = {
|
||||||
|
engine: string
|
||||||
|
algo: string
|
||||||
|
data: string
|
||||||
|
digest: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Signer {
|
||||||
|
onsign(signature: Signature): void;
|
||||||
|
onverify(flag: boolean): void;
|
||||||
|
fingerprint(): string;
|
||||||
|
prepare(material: Signable): boolean;
|
||||||
|
verify(digest: string, signature: Signature): void;
|
||||||
|
sign(digest: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PGPSigner implements Signer {
|
||||||
|
|
||||||
|
engine = 'pgp';
|
||||||
|
algo = 'sha256';
|
||||||
|
dgst: string;
|
||||||
|
signature: Signature;
|
||||||
|
keyStore: MutableKeyStore;
|
||||||
|
onsign: (signature: Signature) => void;
|
||||||
|
onverify: (flag: boolean) => void;
|
||||||
|
loggingService: LoggingService;
|
||||||
|
|
||||||
|
constructor(keyStore: MutableKeyStore) {
|
||||||
|
this.keyStore = keyStore;
|
||||||
|
this.onsign = (signature: Signature) => {};
|
||||||
|
this.onverify = (flag: boolean) => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
public fingerprint(): string {
|
||||||
|
return this.keyStore.getFingerprint();
|
||||||
|
}
|
||||||
|
|
||||||
|
public prepare(material: Signable): boolean {
|
||||||
|
this.dgst = material.digest();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public verify(digest: string, signature: Signature): void {
|
||||||
|
openpgp.signature.readArmored(signature.data).then((sig) => {
|
||||||
|
const opts = {
|
||||||
|
message: openpgp.cleartext.fromText(digest),
|
||||||
|
publicKeys: this.keyStore.getTrustedKeys(),
|
||||||
|
signature: sig,
|
||||||
|
};
|
||||||
|
openpgp.verify(opts).then((v) => {
|
||||||
|
let i = 0;
|
||||||
|
for (i = 0; i < v.signatures.length; i++) {
|
||||||
|
const s = v.signatures[i];
|
||||||
|
if (s.valid) {
|
||||||
|
this.onverify(s);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.loggingService.sendErrorLevelMessage(`Checked ${i} signature(s) but none valid`, this, {error: '404 Not found!'});
|
||||||
|
this.onverify(false);
|
||||||
|
});
|
||||||
|
}).catch((e) => {
|
||||||
|
this.loggingService.sendErrorLevelMessage(e.message, this, {error: e});
|
||||||
|
this.onverify(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sign(digest: string): Promise<void> {
|
||||||
|
const m = openpgp.cleartext.fromText(digest);
|
||||||
|
const pk = this.keyStore.getPrivateKey();
|
||||||
|
if (!pk.isDecrypted()) {
|
||||||
|
const password = window.prompt('password');
|
||||||
|
await pk.decrypt(password);
|
||||||
|
}
|
||||||
|
const opts = {
|
||||||
|
message: m,
|
||||||
|
privateKeys: [pk],
|
||||||
|
detached: true,
|
||||||
|
};
|
||||||
|
openpgp.sign(opts).then((s) => {
|
||||||
|
this.signature = {
|
||||||
|
engine: this.engine,
|
||||||
|
algo: this.algo,
|
||||||
|
data: s.signature,
|
||||||
|
// TODO: fix for browser later
|
||||||
|
digest,
|
||||||
|
};
|
||||||
|
this.onsign(this.signature);
|
||||||
|
}).catch((e) => {
|
||||||
|
this.loggingService.sendErrorLevelMessage(e.message, this, {error: e});
|
||||||
|
this.onsign(undefined);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Signable,
|
||||||
|
Signature,
|
||||||
|
Signer,
|
||||||
|
PGPSigner
|
||||||
|
};
|
@ -1,11 +1,12 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import {MutableKeyStore, MutablePgpKeyStore} from '@app/_helpers';
|
|
||||||
import { hobaParseChallengeHeader } from '@src/assets/js/hoba.js';
|
import { hobaParseChallengeHeader } from '@src/assets/js/hoba.js';
|
||||||
import { signChallenge } from '@src/assets/js/hoba-pgp.js';
|
import { signChallenge } from '@src/assets/js/hoba-pgp.js';
|
||||||
import {environment} from '@src/environments/environment';
|
import {environment} from '@src/environments/environment';
|
||||||
import {HttpClient} from '@angular/common/http';
|
import {LoggingService} from '@app/_services/logging.service';
|
||||||
|
import {HttpWrapperService} from '@app/_services/http-wrapper.service';
|
||||||
const origin = 'http://localhost:4444';
|
import {MutableKeyStore, MutablePgpKeyStore} from '@app/_pgp';
|
||||||
|
import {ErrorDialogService} from '@app/_services/error-dialog.service';
|
||||||
|
import {first} from 'rxjs/operators';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -17,7 +18,9 @@ export class AuthService {
|
|||||||
mutableKeyStore: MutableKeyStore = new MutablePgpKeyStore();
|
mutableKeyStore: MutableKeyStore = new MutablePgpKeyStore();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private http: HttpClient
|
private httpWrapperService: HttpWrapperService,
|
||||||
|
private loggingService: LoggingService,
|
||||||
|
private errorDialogService: ErrorDialogService
|
||||||
) {
|
) {
|
||||||
if (sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'))) {
|
if (sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'))) {
|
||||||
this.sessionToken = sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'));
|
this.sessionToken = sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'));
|
||||||
@ -28,13 +31,13 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setState(s): void {
|
setState(s): void {
|
||||||
(document.getElementById('state') as HTMLInputElement).value = s;
|
document.getElementById('state').innerHTML = s;
|
||||||
}
|
}
|
||||||
|
|
||||||
getWithToken(): void {
|
getWithToken(): void {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.responseType = 'text';
|
xhr.responseType = 'text';
|
||||||
xhr.open('GET', origin + window.location.search.substring(1));
|
xhr.open('GET', environment.cicAuthUrl + window.location.search.substring(1));
|
||||||
xhr.setRequestHeader('Authorization', 'Bearer ' + this.sessionToken);
|
xhr.setRequestHeader('Authorization', 'Bearer ' + this.sessionToken);
|
||||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||||
xhr.setRequestHeader('x-cic-automerge', 'none');
|
xhr.setRequestHeader('x-cic-automerge', 'none');
|
||||||
@ -43,8 +46,7 @@ export class AuthService {
|
|||||||
throw new Error('login rejected');
|
throw new Error('login rejected');
|
||||||
}
|
}
|
||||||
this.sessionLoginCount++;
|
this.sessionLoginCount++;
|
||||||
this.setState('click to perform login ' + this.sessionLoginCount + ' with token ' + this.sessionToken);
|
this.setState('Click button to perform login ' + this.sessionLoginCount + ' with token ' + this.sessionToken);
|
||||||
console.log('received', xhr.responseText);
|
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
xhr.send();
|
xhr.send();
|
||||||
@ -53,7 +55,7 @@ export class AuthService {
|
|||||||
sendResponse(hobaResponseEncoded): void {
|
sendResponse(hobaResponseEncoded): void {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.responseType = 'text';
|
xhr.responseType = 'text';
|
||||||
xhr.open('GET', origin + window.location.search.substring(1));
|
xhr.open('GET', environment.cicAuthUrl + window.location.search.substring(1));
|
||||||
xhr.setRequestHeader('Authorization', 'HOBA ' + hobaResponseEncoded);
|
xhr.setRequestHeader('Authorization', 'HOBA ' + hobaResponseEncoded);
|
||||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||||
xhr.setRequestHeader('x-cic-automerge', 'none');
|
xhr.setRequestHeader('x-cic-automerge', 'none');
|
||||||
@ -64,8 +66,7 @@ export class AuthService {
|
|||||||
this.sessionToken = xhr.getResponseHeader('Token');
|
this.sessionToken = xhr.getResponseHeader('Token');
|
||||||
sessionStorage.setItem(btoa('CICADA_SESSION_TOKEN'), this.sessionToken);
|
sessionStorage.setItem(btoa('CICADA_SESSION_TOKEN'), this.sessionToken);
|
||||||
this.sessionLoginCount++;
|
this.sessionLoginCount++;
|
||||||
this.setState('click to perform login ' + this.sessionLoginCount + ' with token ' + this.sessionToken);
|
this.setState('Click button to perform login ' + this.sessionLoginCount + ' with token ' + this.sessionToken);
|
||||||
console.log('received', xhr.responseText);
|
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
xhr.send();
|
xhr.send();
|
||||||
@ -74,7 +75,7 @@ export class AuthService {
|
|||||||
getChallenge(): void {
|
getChallenge(): void {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.responseType = 'arraybuffer';
|
xhr.responseType = 'arraybuffer';
|
||||||
xhr.open('GET', origin + window.location.search.substring(1));
|
xhr.open('GET', environment.cicAuthUrl + window.location.search.substring(1));
|
||||||
xhr.onload = (e) => {
|
xhr.onload = (e) => {
|
||||||
if (xhr.status === 401) {
|
if (xhr.status === 401) {
|
||||||
const authHeader = xhr.getResponseHeader('WWW-Authenticate');
|
const authHeader = xhr.getResponseHeader('WWW-Authenticate');
|
||||||
@ -92,14 +93,13 @@ export class AuthService {
|
|||||||
this.getWithToken();
|
this.getWithToken();
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('login token failed', e);
|
this.loggingService.sendErrorLevelMessage('Login token failed', this, {error: e});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const o = this.getChallenge();
|
this.getChallenge();
|
||||||
return true;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('login challenge failed', e);
|
this.loggingService.sendErrorLevelMessage('Login challenge failed', this, {error: e});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -107,23 +107,26 @@ export class AuthService {
|
|||||||
|
|
||||||
|
|
||||||
async loginResponse(o): Promise<any> {
|
async loginResponse(o): Promise<any> {
|
||||||
const r = await signChallenge(o.challenge, o.realm, origin, this.mutableKeyStore);
|
const r = await signChallenge(o.challenge, o.realm, environment.cicAuthUrl, this.mutableKeyStore);
|
||||||
this.sendResponse(r);
|
this.sendResponse(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
loginView(): void {
|
loginView(): void {
|
||||||
document.getElementById('one').style.display = 'none';
|
document.getElementById('one').style.display = 'none';
|
||||||
document.getElementById('two').style.display = 'block';
|
document.getElementById('two').style.display = 'block';
|
||||||
this.setState('click to log in with PGP key ' + this.mutableKeyStore.getPrivateKeyId());
|
this.setState('Click button to log in with PGP key ' + this.mutableKeyStore.getPrivateKeyId());
|
||||||
}
|
}
|
||||||
|
|
||||||
async setKey(privateKeyArmored): Promise<boolean> {
|
async setKey(privateKeyArmored): Promise<boolean> {
|
||||||
console.log('settings pk' + privateKeyArmored);
|
|
||||||
try {
|
try {
|
||||||
await this.mutableKeyStore.importPrivateKey(privateKeyArmored);
|
await this.mutableKeyStore.importPrivateKey(privateKeyArmored);
|
||||||
localStorage.setItem(btoa('CICADA_PRIVATE_KEY'), privateKeyArmored);
|
localStorage.setItem(btoa('CICADA_PRIVATE_KEY'), privateKeyArmored);
|
||||||
} catch (e) {
|
} catch (err) {
|
||||||
console.error('failed setting key', e);
|
this.loggingService.sendErrorLevelMessage('Failed setting key', this, {error: err});
|
||||||
|
this.errorDialogService.openDialog({
|
||||||
|
message: `Failed to set key, Enter your private key again. Reason: ${err.error.message || err.statusText}`,
|
||||||
|
status: err.status
|
||||||
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.loginView();
|
this.loginView();
|
||||||
@ -136,10 +139,10 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getPublicKeys(): Promise<void> {
|
async getPublicKeys(): Promise<void> {
|
||||||
this.http.get(`${environment.publicKeysUrl}/keys.asc`).subscribe(async res => {
|
this.httpWrapperService.get(`${environment.publicKeysUrl}`).pipe(first()).subscribe(async res => {
|
||||||
await this.mutableKeyStore.importPublicKey(res);
|
await this.mutableKeyStore.importPublicKey(res.body);
|
||||||
}, error => {
|
}, error => {
|
||||||
console.error('There was an error!', error);
|
this.loggingService.sendErrorLevelMessage('There was an error fetching public keys!', this, {error});
|
||||||
});
|
});
|
||||||
if (this.privateKey !== undefined) {
|
if (this.privateKey !== undefined) {
|
||||||
await this.mutableKeyStore.importPrivateKey(this.privateKey);
|
await this.mutableKeyStore.importPrivateKey(this.privateKey);
|
||||||
|
22
src/app/_services/block-sync.service.spec.ts
Normal file
22
src/app/_services/block-sync.service.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { BlockSyncService } from '@app/_services/block-sync.service';
|
||||||
|
import {TransactionService} from '@app/_services/transaction.service';
|
||||||
|
import {TransactionServiceStub} from '@src/testing';
|
||||||
|
|
||||||
|
describe('BlockSyncService', () => {
|
||||||
|
let service: BlockSyncService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: TransactionService, useClass: TransactionServiceStub }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
service = TestBed.inject(BlockSyncService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
116
src/app/_services/block-sync.service.ts
Normal file
116
src/app/_services/block-sync.service.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {Settings} from '@app/_models';
|
||||||
|
import Web3 from 'web3';
|
||||||
|
import {CICRegistry, TransactionHelper} from 'cic-client';
|
||||||
|
import {first} from 'rxjs/operators';
|
||||||
|
import {TransactionService} from '@app/_services/transaction.service';
|
||||||
|
import {environment} from '@src/environments/environment';
|
||||||
|
import {HttpGetter} from '@app/_helpers';
|
||||||
|
import {LoggingService} from '@app/_services/logging.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class BlockSyncService {
|
||||||
|
readyStateTarget: number = 2;
|
||||||
|
readyState: number = 0;
|
||||||
|
fileGetter = new HttpGetter();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private transactionService: TransactionService,
|
||||||
|
private loggingService: LoggingService
|
||||||
|
) { }
|
||||||
|
|
||||||
|
blockSync(address: string = null, offset: number = 0, limit: number = 100): any {
|
||||||
|
this.transactionService.resetTransactionsList();
|
||||||
|
const settings = new Settings(this.scan);
|
||||||
|
const provider = environment.web3Provider;
|
||||||
|
const readyStateElements = { network: 2 };
|
||||||
|
settings.w3.provider = provider;
|
||||||
|
settings.w3.engine = new Web3(provider);
|
||||||
|
settings.registry = new CICRegistry(settings.w3.engine, environment.registryAddress, this.fileGetter,
|
||||||
|
['../../assets/js/block-sync/data']);
|
||||||
|
settings.registry.declaratorHelper.addTrust(environment.trustedDeclaratorAddress);
|
||||||
|
settings.txHelper = new TransactionHelper(settings.w3.engine, settings.registry);
|
||||||
|
|
||||||
|
settings.txHelper.ontransfer = async (transaction: any): Promise<void> => {
|
||||||
|
window.dispatchEvent(this.newTransferEvent(transaction));
|
||||||
|
};
|
||||||
|
settings.txHelper.onconversion = async (transaction: any): Promise<any> => {
|
||||||
|
window.dispatchEvent(this.newConversionEvent(transaction));
|
||||||
|
};
|
||||||
|
settings.registry.onload = (addressReturned: number): void => {
|
||||||
|
this.loggingService.sendInfoLevelMessage(`Loaded network contracts ${addressReturned}`);
|
||||||
|
this.readyStateProcessor(settings, readyStateElements.network, address, offset, limit);
|
||||||
|
};
|
||||||
|
|
||||||
|
settings.registry.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
readyStateProcessor(settings: Settings, bit: number, address: string, offset: number, limit: number): void {
|
||||||
|
this.readyState |= bit;
|
||||||
|
if (this.readyStateTarget === this.readyState && this.readyStateTarget) {
|
||||||
|
const wHeadSync = new Worker('./../assets/js/block-sync/head.js');
|
||||||
|
wHeadSync.onmessage = (m) => {
|
||||||
|
settings.txHelper.processReceipt(m.data);
|
||||||
|
};
|
||||||
|
wHeadSync.postMessage({
|
||||||
|
w3_provider: settings.w3.provider,
|
||||||
|
});
|
||||||
|
if (address === null) {
|
||||||
|
this.transactionService.getAllTransactions(offset, limit).pipe(first()).subscribe(res => {
|
||||||
|
this.fetcher(settings, res.body);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.transactionService.getAddressTransactions(address, offset, limit).pipe(first()).subscribe(res => {
|
||||||
|
this.fetcher(settings, res.body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newTransferEvent(tx): any {
|
||||||
|
return new CustomEvent('cic_transfer', {
|
||||||
|
detail: {
|
||||||
|
tx,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
newConversionEvent(tx): any {
|
||||||
|
return new CustomEvent('cic_convert', {
|
||||||
|
detail: {
|
||||||
|
tx,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async scan(settings, lo, hi, bloomBlockBytes, bloomBlocktxBytes, bloomRounds): Promise<void> {
|
||||||
|
const w = new Worker('./../assets/js/block-sync/ondemand.js');
|
||||||
|
w.onmessage = (m) => {
|
||||||
|
settings.txHelper.processReceipt(m.data);
|
||||||
|
};
|
||||||
|
w.postMessage({
|
||||||
|
w3_provider: settings.w3.provider,
|
||||||
|
lo,
|
||||||
|
hi,
|
||||||
|
filters: [
|
||||||
|
bloomBlockBytes,
|
||||||
|
bloomBlocktxBytes,
|
||||||
|
],
|
||||||
|
filter_rounds: bloomRounds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fetcher(settings: Settings, transactionsInfo: any): void {
|
||||||
|
const blockFilterBinstr = window.atob(transactionsInfo.block_filter);
|
||||||
|
const bOne = new Uint8Array(blockFilterBinstr.length);
|
||||||
|
bOne.map((e, i, v) => v[i] = blockFilterBinstr.charCodeAt(i));
|
||||||
|
|
||||||
|
const blocktxFilterBinstr = window.atob(transactionsInfo.blocktx_filter);
|
||||||
|
const bTwo = new Uint8Array(blocktxFilterBinstr.length);
|
||||||
|
bTwo.map((e, i, v) => v[i] = blocktxFilterBinstr.charCodeAt(i));
|
||||||
|
|
||||||
|
settings.scanFilter(settings, transactionsInfo.low, transactionsInfo.high, bOne, bTwo, transactionsInfo.filter_rounds);
|
||||||
|
}
|
||||||
|
}
|
16
src/app/_services/error-dialog.service.spec.ts
Normal file
16
src/app/_services/error-dialog.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ErrorDialogService } from './error-dialog.service';
|
||||||
|
|
||||||
|
describe('ErrorDialogService', () => {
|
||||||
|
let service: ErrorDialogService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(ErrorDialogService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
33
src/app/_services/error-dialog.service.ts
Normal file
33
src/app/_services/error-dialog.service.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {MatDialog} from '@angular/material/dialog';
|
||||||
|
import {ErrorDialogComponent} from '@app/shared/error-dialog/error-dialog.component';
|
||||||
|
import {LoggingService} from '@app/_services/logging.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ErrorDialogService {
|
||||||
|
public isDialogOpen: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public dialog: MatDialog,
|
||||||
|
private loggingService: LoggingService
|
||||||
|
) { }
|
||||||
|
|
||||||
|
openDialog(data): any {
|
||||||
|
if (this.isDialogOpen) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.isDialogOpen = true;
|
||||||
|
const dialogRef = this.dialog.open(ErrorDialogComponent, {
|
||||||
|
width: '300px',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe(result => {
|
||||||
|
this.loggingService.sendInfoLevelMessage('The dialog was closed');
|
||||||
|
this.isDialogOpen = false;
|
||||||
|
const res = result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
16
src/app/_services/http-wrapper.service.spec.ts
Normal file
16
src/app/_services/http-wrapper.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { HttpWrapperService } from './http-wrapper.service';
|
||||||
|
|
||||||
|
describe('HttpWrapperService', () => {
|
||||||
|
let service: HttpWrapperService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(HttpWrapperService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
60
src/app/_services/http-wrapper.service.ts
Normal file
60
src/app/_services/http-wrapper.service.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {HttpClient, HttpRequest} from '@angular/common/http';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
import * as moment from 'moment';
|
||||||
|
import { Moment } from 'moment';
|
||||||
|
import {LoggingService} from '@app/_services/logging.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class HttpWrapperService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private http: HttpClient,
|
||||||
|
private loggingService: LoggingService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
get(url: string, options?: any): Observable<Response> {
|
||||||
|
return this.request('GET', url, null, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
post(url: string, body: any, options?: any): Observable<Response> {
|
||||||
|
return this.request('POST', url, body, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
put(url: string, body: any, options?: any): Observable<Response> {
|
||||||
|
return this.request('PUT', url, body, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(url: string, options?: any): Observable<Response> {
|
||||||
|
return this.request('DELETE', url, null, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private logTime(startMoment: Moment, url: string, method: string): void {
|
||||||
|
const requestDuration = moment().diff(startMoment, 'milliseconds');
|
||||||
|
this.loggingService.sendInfoLevelMessage(`HTTP ${method}, URL: ${url}, Duration: ${requestDuration} ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private request(method: string, url: string, body?: any, options?: any): Observable<any> {
|
||||||
|
this.loggingService.sendInfoLevelMessage(`Options: ${options}`);
|
||||||
|
return Observable.create((observer: any) => {
|
||||||
|
const requestBeginTime = moment();
|
||||||
|
this.http.request(new HttpRequest(method, url, body, options)).subscribe((response) => {
|
||||||
|
this.loggingService.sendInfoLevelMessage(response);
|
||||||
|
this.logTime(requestBeginTime, `${url}`, method);
|
||||||
|
observer.next(response);
|
||||||
|
observer.complete();
|
||||||
|
}, (error) => {
|
||||||
|
switch (error.status) {
|
||||||
|
case 403:
|
||||||
|
observer.complete();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
observer.error(error);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1,9 @@
|
|||||||
export * from '@app/_services/auth.service';
|
export * from '@app/_services/auth.service';
|
||||||
|
export * from '@app/_services/transaction.service';
|
||||||
|
export * from '@app/_services/user.service';
|
||||||
|
export * from '@app/_services/token.service';
|
||||||
|
export * from '@app/_services/block-sync.service';
|
||||||
|
export * from '@app/_services/location.service';
|
||||||
|
export * from '@app/_services/logging.service';
|
||||||
|
export * from '@app/_services/http-wrapper.service';
|
||||||
|
export * from '@app/_services/error-dialog.service';
|
||||||
|
16
src/app/_services/location.service.spec.ts
Normal file
16
src/app/_services/location.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LocationService } from '@app/_services/location.service';
|
||||||
|
|
||||||
|
describe('LocationService', () => {
|
||||||
|
let service: LocationService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(LocationService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
22
src/app/_services/location.service.ts
Normal file
22
src/app/_services/location.service.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {BehaviorSubject} from 'rxjs';
|
||||||
|
import {environment} from '@src/environments/environment';
|
||||||
|
import {first} from 'rxjs/operators';
|
||||||
|
import {HttpWrapperService} from '@app/_services/http-wrapper.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class LocationService {
|
||||||
|
locations: any = '';
|
||||||
|
private locationsList = new BehaviorSubject<any>(this.locations);
|
||||||
|
locationsSubject = this.locationsList.asObservable();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private httpWrapperService: HttpWrapperService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
getLocations(): void {
|
||||||
|
this.httpWrapperService.get(`${environment.cicCacheUrl}/locations`).pipe(first()).subscribe(res => this.locationsList.next(res.body));
|
||||||
|
}
|
||||||
|
}
|
16
src/app/_services/logging.service.spec.ts
Normal file
16
src/app/_services/logging.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LoggingService } from './logging.service';
|
||||||
|
|
||||||
|
describe('LoggingService', () => {
|
||||||
|
let service: LoggingService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(LoggingService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
48
src/app/_services/logging.service.ts
Normal file
48
src/app/_services/logging.service.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {NGXLogger} from 'ngx-logger';
|
||||||
|
import {environment} from '@src/environments/environment';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class LoggingService {
|
||||||
|
env: string;
|
||||||
|
canDebug: boolean;
|
||||||
|
|
||||||
|
constructor(private logger: NGXLogger) {
|
||||||
|
// TRACE|DEBUG|INFO|LOG|WARN|ERROR|FATAL|OFF
|
||||||
|
this.env = environment.production ? 'Production' : 'Development';
|
||||||
|
|
||||||
|
if (this.env === 'Development') {
|
||||||
|
this.sendInfoLevelMessage('Dropping into debug mode');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendTraceLevelMessage(message, source, error): void {
|
||||||
|
this.logger.trace(message, source, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendDebugLevelMessage(message, source, error): void {
|
||||||
|
this.logger.debug(message, source, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendInfoLevelMessage(message): void {
|
||||||
|
this.logger.info(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendLogLevelMessage(message, source, error): void {
|
||||||
|
this.logger.log(message, source, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendWarnLevelMessage(message, error): void {
|
||||||
|
this.logger.warn(message, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendErrorLevelMessage(message, source, error): void {
|
||||||
|
this.logger.error(message, source, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendFatalLevelMessage(message, source, error): void {
|
||||||
|
this.logger.fatal(message, source, error);
|
||||||
|
}
|
||||||
|
}
|
24
src/app/_services/token.service.spec.ts
Normal file
24
src/app/_services/token.service.spec.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { TokenService } from '@app/_services/token.service';
|
||||||
|
|
||||||
|
describe('TokenService', () => {
|
||||||
|
let service: TokenService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(TokenService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return token for available token', () => {
|
||||||
|
expect(service.getTokenBySymbol('RSV')).toEqual({ name: 'Reserve', symbol: 'RSV' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return token for unavailable token', () => {
|
||||||
|
expect(service.getTokenBySymbol('ABC')).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
41
src/app/_services/token.service.ts
Normal file
41
src/app/_services/token.service.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {environment} from '@src/environments/environment';
|
||||||
|
import {BehaviorSubject, Observable} from 'rxjs';
|
||||||
|
import {HttpGetter} from '@app/_helpers';
|
||||||
|
import {CICRegistry} from 'cic-client';
|
||||||
|
import Web3 from 'web3';
|
||||||
|
import {HttpWrapperService} from '@app/_services/http-wrapper.service';
|
||||||
|
import {Registry, TokenRegistry} from '@app/_eth';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class TokenService {
|
||||||
|
web3 = new Web3(environment.web3Provider);
|
||||||
|
fileGetter = new HttpGetter();
|
||||||
|
registry = new Registry(environment.registryAddress);
|
||||||
|
cicRegistry = new CICRegistry(this.web3, environment.registryAddress, this.fileGetter, ['../../assets/js/block-sync/data']);
|
||||||
|
tokens: any = '';
|
||||||
|
private tokensList = new BehaviorSubject<any>(this.tokens);
|
||||||
|
tokensSubject = this.tokensList.asObservable();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private httpWrapperService: HttpWrapperService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
async getTokens(): Promise<any> {
|
||||||
|
const tokenRegistryQuery = new TokenRegistry(await this.registry.addressOf('TokenRegistry'));
|
||||||
|
const count = await tokenRegistryQuery.totalTokens();
|
||||||
|
return Array.from({length: count}, async (v, i) => await tokenRegistryQuery.entry(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
getTokenBySymbol(symbol: string): Observable<any> {
|
||||||
|
return this.httpWrapperService.get(`${environment.cicCacheUrl}/tokens/${symbol}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTokenBalance(address: string): Promise<number> {
|
||||||
|
const tokenRegistryQuery = new TokenRegistry(await this.registry.addressOf('TokenRegistry'));
|
||||||
|
const sarafuToken = await this.cicRegistry.addToken(await tokenRegistryQuery.entry(0));
|
||||||
|
return await sarafuToken.methods.balanceOf(address).call();
|
||||||
|
}
|
||||||
|
}
|
24
src/app/_services/transaction.service.spec.ts
Normal file
24
src/app/_services/transaction.service.spec.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { TransactionService } from '@app/_services/transaction.service';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
|
||||||
|
|
||||||
|
describe('TransactionService', () => {
|
||||||
|
let httpClient: HttpClient;
|
||||||
|
let httpTestingController: HttpTestingController;
|
||||||
|
let service: TransactionService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientTestingModule]
|
||||||
|
});
|
||||||
|
httpClient = TestBed.inject(HttpClient);
|
||||||
|
httpTestingController = TestBed.inject(HttpTestingController);
|
||||||
|
service = TestBed.inject(TransactionService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
135
src/app/_services/transaction.service.ts
Normal file
135
src/app/_services/transaction.service.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {first} from 'rxjs/operators';
|
||||||
|
import {BehaviorSubject, Observable} from 'rxjs';
|
||||||
|
import {environment} from '@src/environments/environment';
|
||||||
|
import {Envelope, User} from 'cic-client-meta';
|
||||||
|
import {UserService} from '@app/_services/user.service';
|
||||||
|
import { Keccak } from 'sha3';
|
||||||
|
import { utils } from 'ethers';
|
||||||
|
import {add0x, fromHex, strip0x, toHex} from '@src/assets/js/ethtx/dist/hex';
|
||||||
|
import {Tx} from '@src/assets/js/ethtx/dist';
|
||||||
|
import {toValue} from '@src/assets/js/ethtx/dist/tx';
|
||||||
|
import * as secp256k1 from 'secp256k1';
|
||||||
|
import {AuthService} from '@app/_services/auth.service';
|
||||||
|
import {defaultAccount} from '@app/_models';
|
||||||
|
import {LoggingService} from '@app/_services/logging.service';
|
||||||
|
import {HttpWrapperService} from '@app/_services/http-wrapper.service';
|
||||||
|
import {Registry} from '@app/_eth';
|
||||||
|
const Web3 = require('web3');
|
||||||
|
const vCard = require('vcard-parser');
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class TransactionService {
|
||||||
|
transactions: any[] = [];
|
||||||
|
private transactionList = new BehaviorSubject<any[]>(this.transactions);
|
||||||
|
transactionsSubject = this.transactionList.asObservable();
|
||||||
|
userInfo: any;
|
||||||
|
web3 = new Web3(environment.web3Provider);
|
||||||
|
registry = new Registry(environment.registryAddress);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private httpWrapperService: HttpWrapperService,
|
||||||
|
private authService: AuthService,
|
||||||
|
private userService: UserService,
|
||||||
|
private loggingService: LoggingService
|
||||||
|
) { }
|
||||||
|
|
||||||
|
getAllTransactions(offset: number, limit: number): Observable<any> {
|
||||||
|
return this.httpWrapperService.get(`${environment.cicCacheUrl}/tx/${offset}/${limit}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAddressTransactions(address: string, offset: number, limit: number): Observable<any> {
|
||||||
|
return this.httpWrapperService.get(`${environment.cicCacheUrl}/tx/${address}/${offset}/${limit}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setTransaction(transaction, cacheSize: number): Promise<void> {
|
||||||
|
if (this.transactions.find(cachedTx => cachedTx.tx.txHash === transaction.tx.txHash)) { return; }
|
||||||
|
transaction.value = Number(transaction.value);
|
||||||
|
transaction.type = 'transaction';
|
||||||
|
try {
|
||||||
|
this.userService.getAccountDetailsFromMeta(await User.toKey(transaction.from)).pipe(first()).subscribe((res) => {
|
||||||
|
transaction.sender = this.getAccountInfo(res.body);
|
||||||
|
}, error => {
|
||||||
|
transaction.sender = defaultAccount;
|
||||||
|
});
|
||||||
|
this.userService.getAccountDetailsFromMeta(await User.toKey(transaction.to)).pipe(first()).subscribe((res) => {
|
||||||
|
transaction.recipient = this.getAccountInfo(res.body);
|
||||||
|
}, error => {
|
||||||
|
transaction.recipient = defaultAccount;
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.addTransaction(transaction, cacheSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setConversion(conversion, cacheSize): Promise<void> {
|
||||||
|
if (this.transactions.find(cachedTx => cachedTx.tx.txHash === conversion.tx.txHash)) { return; }
|
||||||
|
conversion.type = 'conversion';
|
||||||
|
conversion.fromValue = Number(conversion.fromValue);
|
||||||
|
conversion.toValue = Number(conversion.toValue);
|
||||||
|
try {
|
||||||
|
this.userService.getAccountDetailsFromMeta(await User.toKey(conversion.trader)).pipe(first()).subscribe((res) => {
|
||||||
|
conversion.sender = conversion.recipient = this.getAccountInfo(res.body);
|
||||||
|
}, error => {
|
||||||
|
conversion.sender = conversion.recipient = defaultAccount;
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.addTransaction(conversion, cacheSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addTransaction(transaction, cacheSize: number): void {
|
||||||
|
this.transactions.unshift(transaction);
|
||||||
|
if (this.transactions.length > cacheSize) {
|
||||||
|
this.transactions.length = cacheSize;
|
||||||
|
}
|
||||||
|
this.transactionList.next(this.transactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetTransactionsList(): void {
|
||||||
|
this.transactions = [];
|
||||||
|
this.transactionList.next(this.transactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAccountInfo(account: string): any {
|
||||||
|
let accountInfo = Envelope.fromJSON(JSON.stringify(account)).unwrap().m.data;
|
||||||
|
accountInfo.vcard = vCard.parse(atob(accountInfo.vcard));
|
||||||
|
return accountInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
async transferRequest(tokenAddress: string, senderAddress: string, recipientAddress: string, value: number): Promise<any> {
|
||||||
|
const transferAuthAddress = await this.registry.addressOf('TransferAuthorization');
|
||||||
|
const hashFunction = new Keccak(256);
|
||||||
|
hashFunction.update('createRequest(address,address,address,uint256)');
|
||||||
|
const hash = hashFunction.digest();
|
||||||
|
const methodSignature = hash.toString('hex').substring(0, 8);
|
||||||
|
const abiCoder = new utils.AbiCoder();
|
||||||
|
const abi = await abiCoder.encode(['address', 'address', 'address', 'uint256'], [senderAddress, recipientAddress, tokenAddress, value]);
|
||||||
|
const data = fromHex(methodSignature + strip0x(abi));
|
||||||
|
const tx = new Tx(environment.bloxbergChainId);
|
||||||
|
tx.nonce = await this.web3.eth.getTransactionCount(senderAddress);
|
||||||
|
tx.gasPrice = await this.web3.eth.getGasPrice();
|
||||||
|
tx.gasLimit = 8000000;
|
||||||
|
tx.to = fromHex(strip0x(transferAuthAddress));
|
||||||
|
tx.value = toValue(value);
|
||||||
|
tx.data = data;
|
||||||
|
const txMsg = tx.message();
|
||||||
|
const privateKey = this.authService.mutableKeyStore.getPrivateKey();
|
||||||
|
if (!privateKey.isDecrypted()) {
|
||||||
|
const password = window.prompt('password');
|
||||||
|
await privateKey.decrypt(password);
|
||||||
|
}
|
||||||
|
const signatureObject = secp256k1.ecdsaSign(txMsg, privateKey.keyPacket.privateParams.d);
|
||||||
|
const r = signatureObject.signature.slice(0, 32);
|
||||||
|
const s = signatureObject.signature.slice(32);
|
||||||
|
const v = signatureObject.recid;
|
||||||
|
tx.setSignature(r, s, v);
|
||||||
|
const txWire = add0x(toHex(tx.serializeRLP()));
|
||||||
|
const result = await this.web3.eth.sendSignedTransaction(txWire);
|
||||||
|
this.loggingService.sendInfoLevelMessage(`Result: ${result}`);
|
||||||
|
const transaction = await this.web3.eth.getTransaction(result.transactionHash);
|
||||||
|
this.loggingService.sendInfoLevelMessage(`Transaction: ${transaction}`);
|
||||||
|
}
|
||||||
|
}
|
80
src/app/_services/user.service.spec.ts
Normal file
80
src/app/_services/user.service.spec.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { UserService } from '@app/_services/user.service';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
|
||||||
|
|
||||||
|
describe('UserService', () => {
|
||||||
|
let httpClient: HttpClient;
|
||||||
|
let httpTestingController: HttpTestingController;
|
||||||
|
let service: UserService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientTestingModule]
|
||||||
|
});
|
||||||
|
httpClient = TestBed.inject(HttpClient);
|
||||||
|
httpTestingController = TestBed.inject(HttpTestingController);
|
||||||
|
service = TestBed.inject(UserService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return user for available id', () => {
|
||||||
|
expect(service.getAccountById(1)).toEqual({
|
||||||
|
id: 1,
|
||||||
|
name: 'John Doe',
|
||||||
|
phone: '+25412345678',
|
||||||
|
address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865',
|
||||||
|
type: 'user',
|
||||||
|
created: '08/16/2020',
|
||||||
|
balance: '12987',
|
||||||
|
failedPinAttempts: 1,
|
||||||
|
status: 'approved',
|
||||||
|
bio: 'Bodaboda',
|
||||||
|
gender: 'male'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return user for unavailable id', () => {
|
||||||
|
expect(service.getAccountById(9999999999)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return action for available id', () => {
|
||||||
|
expect(service.getActionById('1')).toEqual({
|
||||||
|
id: 1,
|
||||||
|
user: 'Tom',
|
||||||
|
role: 'enroller',
|
||||||
|
action: 'Disburse RSV 100',
|
||||||
|
approval: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return action for unavailable id', () => {
|
||||||
|
expect(service.getActionById('9999999999')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should switch action approval from false to true', () => {
|
||||||
|
service.approveAction('1');
|
||||||
|
expect(service.getActionById('1')).toEqual({
|
||||||
|
id: 1,
|
||||||
|
user: 'Tom',
|
||||||
|
role: 'enroller',
|
||||||
|
action: 'Disburse RSV 100',
|
||||||
|
approval: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should switch action approval from true to false', () => {
|
||||||
|
service.revokeAction('2');
|
||||||
|
expect(service.getActionById('2')).toEqual({
|
||||||
|
id: 2,
|
||||||
|
user: 'Christine',
|
||||||
|
role: 'admin',
|
||||||
|
action: 'Change user phone number',
|
||||||
|
approval: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
201
src/app/_services/user.service.ts
Normal file
201
src/app/_services/user.service.ts
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {BehaviorSubject, Observable} from 'rxjs';
|
||||||
|
import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http';
|
||||||
|
import {environment} from '@src/environments/environment';
|
||||||
|
import {first} from 'rxjs/operators';
|
||||||
|
import {ArgPair, Envelope, Syncable, User} from 'cic-client-meta';
|
||||||
|
import {MetaResponse} from '@app/_models';
|
||||||
|
import {LoggingService} from '@app/_services/logging.service';
|
||||||
|
import {HttpWrapperService} from '@app/_services/http-wrapper.service';
|
||||||
|
import {TokenService} from '@app/_services/token.service';
|
||||||
|
import {AccountIndex, Registry} from '@app/_eth';
|
||||||
|
import {MutableKeyStore, MutablePgpKeyStore, PGPSigner, Signer} from '@app/_pgp';
|
||||||
|
const vCard = require('vcard-parser');
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class UserService {
|
||||||
|
headers: HttpHeaders = new HttpHeaders({'x-cic-automerge': 'client'});
|
||||||
|
keystore: MutableKeyStore = new MutablePgpKeyStore();
|
||||||
|
signer: Signer = new PGPSigner(this.keystore);
|
||||||
|
registry = new Registry(environment.registryAddress);
|
||||||
|
|
||||||
|
accountsMeta = [];
|
||||||
|
accounts: any = [];
|
||||||
|
private accountsList = new BehaviorSubject<any>(this.accounts);
|
||||||
|
accountsSubject = this.accountsList.asObservable();
|
||||||
|
|
||||||
|
actions: any = '';
|
||||||
|
private actionsList = new BehaviorSubject<any>(this.actions);
|
||||||
|
actionsSubject = this.actionsList.asObservable();
|
||||||
|
|
||||||
|
staff: any = '';
|
||||||
|
private staffList = new BehaviorSubject<any>(this.staff);
|
||||||
|
staffSubject = this.staffList.asObservable();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private http: HttpClient,
|
||||||
|
private httpWrapperService: HttpWrapperService,
|
||||||
|
private loggingService: LoggingService,
|
||||||
|
private tokenService: TokenService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPin(phone: string): Observable<any> {
|
||||||
|
const params = new HttpParams().set('phoneNumber', phone);
|
||||||
|
return this.httpWrapperService.get(`${environment.cicUssdUrl}/pin`, {params});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAccountStatus(phone: string): any {
|
||||||
|
const params = new HttpParams().set('phoneNumber', phone);
|
||||||
|
return this.httpWrapperService.get(`${environment.cicUssdUrl}/pin`, {params});
|
||||||
|
}
|
||||||
|
|
||||||
|
getLockedAccounts(offset: number, limit: number): any {
|
||||||
|
return this.httpWrapperService.get(`${environment.cicUssdUrl}/accounts/locked/${offset}/${limit}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async changeAccountInfo(address: string, name: string, phoneNumber: string, age: string, type: string, bio: string, gender: string,
|
||||||
|
businessCategory: string, userLocation: string, location: string, locationType: string, metaAccount: MetaResponse
|
||||||
|
): Promise<any> {
|
||||||
|
let reqBody = metaAccount;
|
||||||
|
let accountInfo = reqBody.m.data;
|
||||||
|
accountInfo.vcard.fn[0].value = name;
|
||||||
|
accountInfo.vcard.n[0].value = name.split(' ');
|
||||||
|
accountInfo.vcard.tel[0].value = phoneNumber;
|
||||||
|
accountInfo.products = [bio];
|
||||||
|
accountInfo.gender = gender;
|
||||||
|
accountInfo.age = age;
|
||||||
|
accountInfo.type = type;
|
||||||
|
accountInfo.category = businessCategory;
|
||||||
|
accountInfo.location.area = location;
|
||||||
|
accountInfo.location.area_name = userLocation;
|
||||||
|
accountInfo.location.area_type = locationType;
|
||||||
|
accountInfo.vcard = vCard.generate(accountInfo.vcard);
|
||||||
|
reqBody.m.data = accountInfo;
|
||||||
|
const accountKey = await User.toKey(address);
|
||||||
|
this.httpWrapperService.get(`${environment.cicMetaUrl}/${accountKey}`, { headers: this.headers }).pipe(first()).subscribe(async res => {
|
||||||
|
const syncableAccount: Syncable = Envelope.fromJSON(JSON.stringify(res.body)).unwrap();
|
||||||
|
let update = [];
|
||||||
|
for (const prop in reqBody) {
|
||||||
|
update.push(new ArgPair(prop, reqBody[prop]));
|
||||||
|
}
|
||||||
|
syncableAccount.update(update, 'client-branch');
|
||||||
|
await this.updateMeta(syncableAccount, accountKey, this.headers);
|
||||||
|
}, async error => {
|
||||||
|
this.loggingService.sendErrorLevelMessage('Can\'t find account info in meta service', this, {error});
|
||||||
|
const syncableAccount: Syncable = new Syncable(accountKey, accountInfo);
|
||||||
|
await this.updateMeta(syncableAccount, accountKey, this.headers);
|
||||||
|
});
|
||||||
|
return accountKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateMeta(syncableAccount: Syncable, accountKey: string, headers: HttpHeaders): Promise<any> {
|
||||||
|
const envelope = await this.wrap(syncableAccount , this.signer);
|
||||||
|
const reqBody = envelope.toJSON();
|
||||||
|
this.httpWrapperService.put(`${environment.cicMetaUrl}/${accountKey}`, reqBody , { headers }).pipe(first()).subscribe(res => {
|
||||||
|
this.loggingService.sendInfoLevelMessage(`Response: ${res.body}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAccounts(): void {
|
||||||
|
this.httpWrapperService.get(`${environment.cicCacheUrl}/accounts`).pipe(first()).subscribe(res => this.accountsList.next(res.body));
|
||||||
|
}
|
||||||
|
|
||||||
|
getAccountById(id: number): Observable<any> {
|
||||||
|
return this.httpWrapperService.get(`${environment.cicCacheUrl}/accounts/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getActions(): void {
|
||||||
|
this.httpWrapperService.get(`${environment.cicCacheUrl}/actions`).pipe(first()).subscribe(res => this.actionsList.next(res.body));
|
||||||
|
}
|
||||||
|
|
||||||
|
getActionById(id: string): any {
|
||||||
|
return this.httpWrapperService.get(`${environment.cicCacheUrl}/actions/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
approveAction(id: string): Observable<any> {
|
||||||
|
return this.httpWrapperService.post(`${environment.cicCacheUrl}/actions/${id}`, { approval: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
revokeAction(id: string): Observable<any> {
|
||||||
|
return this.httpWrapperService.post(`${environment.cicCacheUrl}/actions/${id}`, { approval: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
getHistoryByUser(id: string): Observable<any> {
|
||||||
|
return this.httpWrapperService.get(`${environment.cicCacheUrl}/history/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getStaff(): void {
|
||||||
|
this.httpWrapperService.get(`${environment.cicCacheUrl}/staff`).pipe(first()).subscribe(res => this.staffList.next(res.body));
|
||||||
|
}
|
||||||
|
|
||||||
|
getStaffById(id: string): Observable<any> {
|
||||||
|
return this.httpWrapperService.get(`${environment.cicCacheUrl}/staff/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
activateStaff(id: string): Observable<any> {
|
||||||
|
return this.httpWrapperService.post(`${environment.cicCacheUrl}/staff/${id}`, {status: 'activated'});
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivateStaff(id: string): Observable<any> {
|
||||||
|
return this.httpWrapperService.post(`${environment.cicCacheUrl}/staff/${id}`, {status: 'deactivated'});
|
||||||
|
}
|
||||||
|
|
||||||
|
changeStaffType(id: string, type: string): Observable<any> {
|
||||||
|
return this.httpWrapperService.post(`${environment.cicCacheUrl}/staff/${id}`, {accountType: type});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAccountDetailsFromMeta(userKey: string): Observable<any> {
|
||||||
|
return this.http.get(`${environment.cicMetaUrl}/${userKey}`, { headers: this.headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
getUser(userKey: string): any {
|
||||||
|
return this.httpWrapperService.get(`${environment.cicMetaUrl}/${userKey}`, { headers: this.headers })
|
||||||
|
.pipe(first()).subscribe(async res => {
|
||||||
|
return Envelope.fromJSON(JSON.stringify(res.body)).unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
wrap(syncable: Syncable, signer: Signer): Promise<Envelope> {
|
||||||
|
return new Promise<Envelope>(async (whohoo, doh) => {
|
||||||
|
syncable.setSigner(signer);
|
||||||
|
syncable.onwrap = async (env) => {
|
||||||
|
if (env === undefined) {
|
||||||
|
doh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
whohoo(env);
|
||||||
|
};
|
||||||
|
await syncable.sign();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadAccounts(limit: number = 100, offset: number = 0): Promise<void> {
|
||||||
|
this.resetAccountsList();
|
||||||
|
const accountIndexAddress = await this.registry.addressOf('AccountRegistry');
|
||||||
|
const accountIndexQuery = new AccountIndex(accountIndexAddress);
|
||||||
|
const accountAddresses = await accountIndexQuery.last(await accountIndexQuery.totalAccounts());
|
||||||
|
this.loggingService.sendInfoLevelMessage(accountAddresses);
|
||||||
|
for (const accountAddress of accountAddresses.slice(offset, offset + limit)) {
|
||||||
|
this.getAccountDetailsFromMeta(await User.toKey(accountAddress)).pipe(first()).subscribe(async res => {
|
||||||
|
const account = Envelope.fromJSON(JSON.stringify(res)).unwrap();
|
||||||
|
this.accountsMeta.push(account);
|
||||||
|
const accountInfo = account.m.data;
|
||||||
|
accountInfo.balance = await this.tokenService.getTokenBalance(accountInfo.identities.evm['bloxberg:8996'][0]);
|
||||||
|
accountInfo.vcard = vCard.parse(atob(accountInfo.vcard));
|
||||||
|
this.accounts.unshift(accountInfo);
|
||||||
|
if (this.accounts.length > limit) {
|
||||||
|
this.accounts.length = limit;
|
||||||
|
}
|
||||||
|
this.accountsList.next(this.accounts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetAccountsList(): void {
|
||||||
|
this.accounts = [];
|
||||||
|
this.accountsList.next(this.accounts);
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,17 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { Routes, RouterModule } from '@angular/router';
|
import {Routes, RouterModule, PreloadAllModules} from '@angular/router';
|
||||||
|
import {AuthGuard} from '@app/_guards';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: 'auth', loadChildren: () => import('@app/auth/auth.module').then(m => m.AuthModule) },
|
{ path: 'auth', loadChildren: () => import('@app/auth/auth.module').then(m => m.AuthModule) },
|
||||||
{ path: '**', redirectTo: 'auth', pathMatch: 'full' }
|
{ path: '', loadChildren: () => import('@pages/pages.module').then(m => m.PagesModule), canActivate: [AuthGuard] },
|
||||||
|
{ path: '**', redirectTo: '', pathMatch: 'full' }
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forRoot(routes)],
|
imports: [RouterModule.forRoot(routes, {
|
||||||
|
preloadingStrategy: PreloadAllModules
|
||||||
|
})],
|
||||||
exports: [RouterModule]
|
exports: [RouterModule]
|
||||||
})
|
})
|
||||||
export class AppRoutingModule { }
|
export class AppRoutingModule { }
|
||||||
|
@ -1 +1 @@
|
|||||||
<router-outlet></router-outlet>
|
<router-outlet (activate)="onResize(mediaQuery)"></router-outlet>
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { AppComponent } from '@app/app.component';
|
import { AppComponent } from '@app/app.component';
|
||||||
|
import {TransactionService} from '@app/_services';
|
||||||
|
import {FooterStubComponent, SidebarStubComponent, TopbarStubComponent, TransactionServiceStub} from '@src/testing';
|
||||||
|
|
||||||
describe('AppComponent', () => {
|
describe('AppComponent', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@ -9,8 +11,14 @@ describe('AppComponent', () => {
|
|||||||
RouterTestingModule
|
RouterTestingModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent
|
AppComponent,
|
||||||
|
FooterStubComponent,
|
||||||
|
SidebarStubComponent,
|
||||||
|
TopbarStubComponent
|
||||||
],
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: TransactionService, useClass: TransactionServiceStub }
|
||||||
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -25,11 +33,4 @@ describe('AppComponent', () => {
|
|||||||
const app = fixture.componentInstance;
|
const app = fixture.componentInstance;
|
||||||
expect(app.title).toEqual('cic-staff-client');
|
expect(app.title).toEqual('cic-staff-client');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render title', () => {
|
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
|
||||||
fixture.detectChanges();
|
|
||||||
const compiled = fixture.nativeElement;
|
|
||||||
expect(compiled.querySelector('.content span').textContent).toContain('cic-staff-client app is running!');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -1,15 +1,66 @@
|
|||||||
import { Component } from '@angular/core';
|
import {ChangeDetectionStrategy, Component, HostListener} from '@angular/core';
|
||||||
import {AuthService} from '@app/_services';
|
import {AuthService, LoggingService, TokenService, TransactionService} from '@app/_services';
|
||||||
|
import {NGXLogger} from 'ngx-logger';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrls: ['./app.component.scss']
|
styleUrls: ['./app.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
title = 'cic-staff-client';
|
title = 'CICADA';
|
||||||
|
readyStateTarget: number = 3;
|
||||||
|
readyState: number = 0;
|
||||||
|
mediaQuery = window.matchMedia('(max-width: 768px)');
|
||||||
|
|
||||||
constructor(private authService: AuthService) {
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private tokenService: TokenService,
|
||||||
|
private transactionService: TransactionService,
|
||||||
|
private logger: NGXLogger,
|
||||||
|
private loggingService: LoggingService,
|
||||||
|
) {
|
||||||
this.authService.mutableKeyStore.loadKeyring().then(r => this.authService.getPublicKeys().then());
|
this.authService.mutableKeyStore.loadKeyring().then(r => this.authService.getPublicKeys().then());
|
||||||
|
this.tokenService.getTokens().then(async r => loggingService.sendInfoLevelMessage(await r));
|
||||||
|
this.mediaQuery.addListener(this.onResize);
|
||||||
|
this.onResize(this.mediaQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load resize
|
||||||
|
onResize(e): void {
|
||||||
|
const sidebar = document.getElementById('sidebar');
|
||||||
|
const content = document.getElementById('content');
|
||||||
|
const sidebarCollapse = document.getElementById('sidebarCollapse');
|
||||||
|
if (sidebarCollapse?.classList.contains('active')) {
|
||||||
|
sidebarCollapse?.classList.remove('active');
|
||||||
|
}
|
||||||
|
if (e.matches) {
|
||||||
|
if (!sidebar?.classList.contains('active')) {
|
||||||
|
sidebar?.classList.add('active');
|
||||||
|
}
|
||||||
|
if (!content?.classList.contains('active')) {
|
||||||
|
content?.classList.add('active');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (sidebar?.classList.contains('active')) {
|
||||||
|
sidebar?.classList.remove('active');
|
||||||
|
}
|
||||||
|
if (content?.classList.contains('active')) {
|
||||||
|
content?.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('window:cic_transfer', ['$event'])
|
||||||
|
cicTransfer(event: CustomEvent): void {
|
||||||
|
const transaction = event.detail.tx;
|
||||||
|
this.transactionService.setTransaction(transaction, 100).then();
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('window:cic_convert', ['$event'])
|
||||||
|
cicConvert(event: CustomEvent): void {
|
||||||
|
const conversion = event.detail.tx;
|
||||||
|
this.transactionService.setConversion(conversion, 100).then();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,52 @@
|
|||||||
import { BrowserModule } from '@angular/platform-browser';
|
import {BrowserModule} from '@angular/platform-browser';
|
||||||
import { NgModule } from '@angular/core';
|
import {ErrorHandler, NgModule} from '@angular/core';
|
||||||
|
|
||||||
import { AppRoutingModule } from '@app/app-routing.module';
|
import {AppRoutingModule} from '@app/app-routing.module';
|
||||||
import { AppComponent } from '@app/app.component';
|
import {AppComponent} from '@app/app.component';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||||
import {HttpClientModule} from '@angular/common/http';
|
import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
|
||||||
import {MutablePgpKeyStore} from '@app/_helpers';
|
import {
|
||||||
|
GlobalErrorHandler,
|
||||||
|
MockBackendProvider,
|
||||||
|
} from '@app/_helpers';
|
||||||
|
import {DataTablesModule} from 'angular-datatables';
|
||||||
|
import {SharedModule} from '@app/shared/shared.module';
|
||||||
|
import {MatTableModule} from '@angular/material/table';
|
||||||
|
import {AuthGuard} from '@app/_guards';
|
||||||
|
import {LoggerModule} from 'ngx-logger';
|
||||||
|
import {environment} from '@src/environments/environment';
|
||||||
|
import {ErrorInterceptor, HttpConfigInterceptor, LoggingInterceptor} from '@app/_interceptors';
|
||||||
|
import {MutablePgpKeyStore} from '@app/_pgp';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
HttpClientModule
|
HttpClientModule,
|
||||||
|
DataTablesModule,
|
||||||
|
SharedModule,
|
||||||
|
MatTableModule,
|
||||||
|
LoggerModule.forRoot({
|
||||||
|
level: environment.level,
|
||||||
|
serverLogLevel: environment.serverLogLevel,
|
||||||
|
serverLoggingUrl: `${environment.loggingUrl}/api/logs/`,
|
||||||
|
disableConsoleLogging: false
|
||||||
|
})
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AuthGuard,
|
||||||
|
MutablePgpKeyStore,
|
||||||
|
MockBackendProvider,
|
||||||
|
GlobalErrorHandler,
|
||||||
|
{ provide: ErrorHandler, useClass: GlobalErrorHandler },
|
||||||
|
{ provide: HTTP_INTERCEPTORS, useClass: HttpConfigInterceptor, multi: true },
|
||||||
|
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
|
||||||
|
{ provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true },
|
||||||
],
|
],
|
||||||
providers: [MutablePgpKeyStore],
|
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
export class AppModule { }
|
||||||
|
@ -17,8 +17,8 @@
|
|||||||
|
|
||||||
<mat-form-field appearance="outline" class="full-width">
|
<mat-form-field appearance="outline" class="full-width">
|
||||||
<mat-label>Private Key</mat-label>
|
<mat-label>Private Key</mat-label>
|
||||||
<textarea matInput style="height: 30rem" name="privateKeyAsc" id="privateKeyAsc" formControlName="key"
|
<textarea matInput style="height: 30rem" formControlName="key" placeholder="Enter your private key..."
|
||||||
placeholder="Enter your private key..." [errorStateMatcher]="matcher"></textarea>
|
[errorStateMatcher]="matcher"></textarea>
|
||||||
<div *ngIf="submitted && keyFormStub.key.errors" class="invalid-feedback">
|
<div *ngIf="submitted && keyFormStub.key.errors" class="invalid-feedback">
|
||||||
<mat-error *ngIf="keyFormStub.key.errors.required">Private Key is required.</mat-error>
|
<mat-error *ngIf="keyFormStub.key.errors.required">Private Key is required.</mat-error>
|
||||||
</div>
|
</div>
|
||||||
@ -34,28 +34,10 @@
|
|||||||
<div id="two" style="display: none" class="card-body p-4 align-items-center">
|
<div id="two" style="display: none" class="card-body p-4 align-items-center">
|
||||||
|
|
||||||
<div class="text-center w-75 m-auto">
|
<div class="text-center w-75 m-auto">
|
||||||
<h4 class="text-dark-50 text-center font-weight-bold">Enter Passphrase</h4>
|
<h4 id="state" class="text-dark-50 text-center font-weight-bold"></h4>
|
||||||
|
<button mat-raised-button matRipple color="primary" type="submit" (click)="login()"> Login </button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form [formGroup]="passphraseForm" (ngSubmit)="login()">
|
|
||||||
|
|
||||||
<mat-form-field appearance="outline" class="full-width">
|
|
||||||
<mat-label>Passphrase</mat-label>
|
|
||||||
<input matInput name="state" id="state" formControlName="passphrase"
|
|
||||||
placeholder="Enter your passphrase..." [errorStateMatcher]="matcher">
|
|
||||||
<div *ngIf="submitted && passphraseFormStub.passphrase.errors" class="invalid-feedback">
|
|
||||||
<mat-error *ngIf="passphraseFormStub.passphrase.errors.required">Passphrase is required.</mat-error>
|
|
||||||
</div>
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<div class="form-group mb-0 text-center">
|
|
||||||
<button mat-raised-button matRipple color="primary" type="submit">
|
|
||||||
<!-- <span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span>-->
|
|
||||||
Login
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
<div class="col-12 text-center">
|
<div class="col-12 text-center">
|
||||||
<p class="text-muted">Change private key? <a (click)="switchWindows()" class="text-muted ml-1"><b>Enter private key</b></a></p>
|
<p class="text-muted">Change private key? <a (click)="switchWindows()" class="text-muted ml-1"><b>Enter private key</b></a></p>
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
import {AfterViewInit, Component, OnInit} from '@angular/core';
|
import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core';
|
||||||
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
|
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
|
||||||
import {CustomErrorStateMatcher} from '@app/_helpers';
|
import {CustomErrorStateMatcher} from '@app/_helpers';
|
||||||
import {AuthService} from '@app/_services';
|
import {AuthService} from '@app/_services';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-auth',
|
selector: 'app-auth',
|
||||||
templateUrl: './auth.component.html',
|
templateUrl: './auth.component.html',
|
||||||
styleUrls: ['./auth.component.scss']
|
styleUrls: ['./auth.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class AuthComponent implements OnInit, AfterViewInit {
|
export class AuthComponent implements OnInit {
|
||||||
keyForm: FormGroup;
|
keyForm: FormGroup;
|
||||||
passphraseForm: FormGroup;
|
|
||||||
submitted: boolean = false;
|
submitted: boolean = false;
|
||||||
loading: boolean = false;
|
loading: boolean = false;
|
||||||
matcher = new CustomErrorStateMatcher();
|
matcher = new CustomErrorStateMatcher();
|
||||||
@ -18,42 +19,24 @@ export class AuthComponent implements OnInit, AfterViewInit {
|
|||||||
constructor(
|
constructor(
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
|
private router: Router
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.keyForm = this.formBuilder.group({
|
this.keyForm = this.formBuilder.group({
|
||||||
key: ['', Validators.required],
|
key: ['', Validators.required],
|
||||||
});
|
});
|
||||||
this.passphraseForm = this.formBuilder.group({
|
|
||||||
passphrase: ['', Validators.required],
|
|
||||||
});
|
|
||||||
if (this.authService.privateKey !== undefined ) {
|
if (this.authService.privateKey !== undefined ) {
|
||||||
this.authService.setKey(this.authService.privateKey).then(r => {
|
this.authService.setKey(this.authService.privateKey).then(r => {
|
||||||
if (this.authService.sessionToken !== undefined) {
|
if (this.authService.sessionToken !== undefined) {
|
||||||
this.authService.setState(
|
this.authService.setState(
|
||||||
'click to perform login ' + this.authService.sessionLoginCount + ' with token ' + this.authService.sessionToken);
|
'Click button to perform login ' + this.authService.sessionLoginCount + ' with token ' + this.authService.sessionToken);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
|
||||||
console.log(window.location);
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.responseType = 'text';
|
|
||||||
xhr.open('GET', window.location.origin + '/privatekey.asc');
|
|
||||||
xhr.onload = (e) => {
|
|
||||||
if (xhr.status !== 200) {
|
|
||||||
console.warn('failed to autoload private key ciphertext');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
(document.getElementById('privateKeyAsc') as HTMLInputElement).value = xhr.responseText;
|
|
||||||
};
|
|
||||||
xhr.send();
|
|
||||||
}
|
|
||||||
|
|
||||||
get keyFormStub(): any { return this.keyForm.controls; }
|
get keyFormStub(): any { return this.keyForm.controls; }
|
||||||
get passphraseFormStub(): any { return this.passphraseForm.controls; }
|
|
||||||
|
|
||||||
onSubmit(): void {
|
onSubmit(): void {
|
||||||
this.submitted = true;
|
this.submitted = true;
|
||||||
@ -66,8 +49,11 @@ export class AuthComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
login(): void {
|
login(): void {
|
||||||
// if (this.passphraseForm.invalid) { return; }
|
const loginStatus = this.authService.login();
|
||||||
this.authService.login();
|
console.log(loginStatus);
|
||||||
|
if (loginStatus) {
|
||||||
|
this.router.navigate(['/home']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switchWindows(): void {
|
switchWindows(): void {
|
||||||
|
@ -0,0 +1,432 @@
|
|||||||
|
<!-- Begin page -->
|
||||||
|
<div class="wrapper">
|
||||||
|
<app-sidebar></app-sidebar>
|
||||||
|
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- Start Page Content here -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
<app-topbar></app-topbar>
|
||||||
|
<!-- Start Content-->
|
||||||
|
<div class="container-fluid" appMenuSelection>
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a routerLink="/home">Home</a></li>
|
||||||
|
<li class="breadcrumb-item"><a routerLink="/accounts">Accounts</a></li>
|
||||||
|
<!-- <li *ngIf="account" class="breadcrumb-item active" aria-current="page">{{account?.vcard?.fn[0].value}}</li>-->
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<div *ngIf="!account" class="text-center">
|
||||||
|
<div class="spinner-grow text-primary m-1" role="status" style="width: 3rem; height: 3rem;">
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<div class="spinner-grow text-primary m-1" role="status" style="width: 3rem; height: 3rem;">
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<div class="spinner-grow text-primary m-1" role="status" style="width: 3rem; height: 3rem;">
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="account" class="card mb-3">
|
||||||
|
<div class="row card-body">
|
||||||
|
<h3>
|
||||||
|
<strong> {{account?.vcard?.fn[0].value}} </strong>
|
||||||
|
</h3>
|
||||||
|
<span class="ml-auto"><strong>Balance:</strong> {{account?.balance}} SRF</span>
|
||||||
|
<span class="ml-2"><strong>Created:</strong> {{account?.date_registered | date}}</span>
|
||||||
|
<span class="ml-2"><strong>Address:</strong><a href="{{bloxbergLink}}" target="_blank"> {{account?.identities.evm['bloxberg:8996']}} </a></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-disbursement *ngIf="isDisbursing" (cancelDisbursmentEvent)="addTransfer()" [account]="account">
|
||||||
|
</app-disbursement>
|
||||||
|
<div *ngIf="account" class="card mt-3 mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<form [formGroup]="accountInfoForm" (ngSubmit)="saveInfo()">
|
||||||
|
<div class="row form-inline">
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Name(s): *</mat-label>
|
||||||
|
<input matInput type="text" id="givenNames" placeholder="{{account?.vcard?.fn[0].value}}"
|
||||||
|
value="{{account?.vcard?.fn[0].value}}" formControlName="name" [errorStateMatcher]="matcher">
|
||||||
|
<mat-error *ngIf="submitted && accountInfoFormStub.name.errors">Name is required.</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Phone Number: </mat-label>
|
||||||
|
<input matInput type="text" id="phoneNumber" placeholder="{{account?.vcard?.tel[0].value}}"
|
||||||
|
value="{{account?.vcard?.tel[0].value}}" formControlName="phoneNumber" [errorStateMatcher]="matcher">
|
||||||
|
<mat-error *ngIf="submitted && accountInfoFormStub.phoneNumber.errors">Phone Number is required.</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Age: </mat-label>
|
||||||
|
<input matInput type="text" id="age" placeholder="{{account?.age}}"
|
||||||
|
value="{{account?.age}}" formControlName="age" [errorStateMatcher]="matcher">
|
||||||
|
<mat-error *ngIf="submitted && accountInfoFormStub.age.errors">Age is required.</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label> ACCOUNT TYPE: </mat-label>
|
||||||
|
<mat-select id="accountType" [(value)]="account.type" formControlName="type"
|
||||||
|
[errorStateMatcher]="matcher">
|
||||||
|
<mat-option value="user"> USER </mat-option>
|
||||||
|
<mat-option value="cashier"> CASHIER </mat-option>
|
||||||
|
<mat-option value="vendor"> VENDOR </mat-option>
|
||||||
|
<mat-option value="tokenAgent"> TOKENAGENT </mat-option>
|
||||||
|
<mat-option value="group"> GROUPACCOUNT </mat-option>
|
||||||
|
</mat-select>
|
||||||
|
<mat-error *ngIf="submitted && accountInfoFormStub.type.errors">Type is required.</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Bio: </mat-label>
|
||||||
|
<input matInput type="text" id="bio" placeholder="{{account?.products}}" value="{{account?.products}}"
|
||||||
|
formControlName="bio" [errorStateMatcher]="matcher">
|
||||||
|
<mat-error *ngIf="submitted && accountInfoFormStub.bio.errors">Bio is required.</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label> GENDER: </mat-label>
|
||||||
|
<mat-select id="gender" [(value)]="account.gender" formControlName="gender"
|
||||||
|
[errorStateMatcher]="matcher">
|
||||||
|
<mat-option value="male"> MALE </mat-option>
|
||||||
|
<mat-option value="female"> FEMALE </mat-option>
|
||||||
|
<mat-option value="other"> OTHER </mat-option>
|
||||||
|
</mat-select>
|
||||||
|
<mat-error *ngIf="submitted && accountInfoFormStub.gender.errors">Gender is required.</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label> BUSINESS CATEGORY: </mat-label>
|
||||||
|
<mat-select id="businessCategory" [(value)]="account.category" formControlName="businessCategory"
|
||||||
|
[errorStateMatcher]="matcher">
|
||||||
|
<mat-option value="food/water">Food/Water</mat-option>
|
||||||
|
<mat-option value="fuel/energy">Fuel/Energy</mat-option>
|
||||||
|
<mat-option value="education">Education</mat-option>
|
||||||
|
<mat-option value="health">Health</mat-option>
|
||||||
|
<mat-option value="shop">Shop</mat-option>
|
||||||
|
<mat-option value="environment">Environment</mat-option>
|
||||||
|
<mat-option value="transport">Transport</mat-option>
|
||||||
|
<mat-option value="farming/labour">Farming/Labour</mat-option>
|
||||||
|
<mat-option value="savings">Savings Group</mat-option>
|
||||||
|
<mat-option value="other">Savings Group</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
<mat-error *ngIf="submitted && accountInfoFormStub.businessCategory.errors">
|
||||||
|
Category is required.
|
||||||
|
</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>User Location: </mat-label>
|
||||||
|
<input matInput type="text" id="userLocation" placeholder="{{account?.location.area_name}}"
|
||||||
|
value="{{account?.location.area_name}}" formControlName="userLocation"
|
||||||
|
[errorStateMatcher]="matcher">
|
||||||
|
<mat-error *ngIf="submitted && accountInfoFormStub.userLocation.errors">
|
||||||
|
User Location is required.
|
||||||
|
</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label> LOCATION: </mat-label>
|
||||||
|
<mat-select id="location" [(value)]="account.location.area" formControlName="location"
|
||||||
|
[errorStateMatcher]="matcher">
|
||||||
|
<div *ngFor="let county of locations; trackBy: trackByName">
|
||||||
|
<div *ngFor="let district of county.districts; trackBy: trackByName">
|
||||||
|
<mat-optgroup *ngFor="let location of district.locations; trackBy: trackByName" [label]="county.name + ' / ' +
|
||||||
|
district.name + ' / ' + location.name">
|
||||||
|
<mat-option *ngFor="let village of location.villages; trackBy: trackByName" [value]="village">
|
||||||
|
{{village}}
|
||||||
|
</mat-option>
|
||||||
|
</mat-optgroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-select>
|
||||||
|
<mat-error *ngIf="submitted && accountInfoFormStub.location.errors">Location is required.</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label> LOCATION TYPE: </mat-label>
|
||||||
|
<mat-select id="locationType" [(value)]="account.location.area_type" formControlName="locationType"
|
||||||
|
[errorStateMatcher]="matcher">
|
||||||
|
<mat-option value="Urban"> URBAN </mat-option>
|
||||||
|
<mat-option value="Periurban"> PERIURBAN </mat-option>
|
||||||
|
<mat-option value="Rural"> RURAL </mat-option>
|
||||||
|
<mat-option value="Other"> OTHER </mat-option>
|
||||||
|
</mat-select>
|
||||||
|
<mat-error *ngIf="submitted && accountInfoFormStub.locationType.errors">Location Type is required.</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<button mat-raised-button color="primary" type="button" class="btn btn btn-outline-primary mb-3">
|
||||||
|
Add User KYC
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<button mat-raised-button color="primary" type="button" class="btn btn btn-outline-success mb-3"
|
||||||
|
(click)="resetPin()">
|
||||||
|
Reset Pin
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<button mat-raised-button color="warn" type="button" class="btn btn-outline-danger mb-3">
|
||||||
|
Delete User
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<button mat-raised-button color="primary" type="submit" class="btn btn-outline-primary">
|
||||||
|
SAVE DETAILS
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<mat-card-title class="card-header">
|
||||||
|
USER
|
||||||
|
</mat-card-title>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-bordered table-hover">
|
||||||
|
<caption> 1 user </caption>
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">NAME</th>
|
||||||
|
<th scope="col">ACCOUNT TYPE</th>
|
||||||
|
<th scope="col">CREATED</th>
|
||||||
|
<th scope="col">STATUS</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>{{account?.name}}</td>
|
||||||
|
<td>{{account?.type}}</td>
|
||||||
|
<td>{{account?.created}}</td>
|
||||||
|
<td>
|
||||||
|
<span *ngIf="account?.status === 'active'" class="badge badge-success badge-pill">
|
||||||
|
{{account?.status}}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="account?.status === 'blocked'" class="badge badge-danger badge-pill">
|
||||||
|
{{account?.status}}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<mat-card-title class="card-header">
|
||||||
|
History
|
||||||
|
</mat-card-title>
|
||||||
|
<div class="card-body">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label> Filter </mat-label>
|
||||||
|
<input matInput type="text" (keyup)="doHistoryFilter($event.target.value)" placeholder="Filter">
|
||||||
|
<mat-icon matSuffix>search</mat-icon>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-table class="mat-elevation-z10" [dataSource]="historyDataSource" matSort #HistoryTableSort="matSort"
|
||||||
|
matSortActive="sender" matSortDirection="asc" matSortDisableClear>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="user">
|
||||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> User </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let history"> {{history.userName}} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="action">
|
||||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> Action </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let history"> {{history.action}} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="staff">
|
||||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> Staff </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let history"> {{history.staff}} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="timestamp">
|
||||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> Timestamp </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let history"> {{history.timestamp | date}} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<mat-header-row *matHeaderRowDef="historyDisplayedColumns"></mat-header-row>
|
||||||
|
<mat-row *matRowDef="let history; columns: historyDisplayedColumns" matRipple></mat-row>
|
||||||
|
</mat-table>
|
||||||
|
|
||||||
|
<mat-paginator #HistoryTablePaginator="matPaginator" [pageSize]="10"
|
||||||
|
[pageSizeOptions]="[10, 20, 50, 100]" showFirstLastButtons></mat-paginator>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-tab-group dynamicHeight mat-align-tabs="start">
|
||||||
|
<mat-tab label="Transactions">
|
||||||
|
<app-transaction-details [transaction]="transaction"></app-transaction-details>
|
||||||
|
<div class="card mt-1">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="row">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label> TRANSACTION TYPE </mat-label>
|
||||||
|
<mat-select id="transferSelect" [(value)]="transactionsType" (selectionChange)="filterTransactions()">
|
||||||
|
<mat-option value="all">ALL TRANSFERS</mat-option>
|
||||||
|
<mat-option value="transaction">PAYMENTS</mat-option>
|
||||||
|
<mat-option value="conversion">CONVERSION</mat-option>
|
||||||
|
<mat-option value="disbursements">DISBURSEMENTS</mat-option>
|
||||||
|
<mat-option value="rewards">REWARDS</mat-option>
|
||||||
|
<mat-option value="reclamation">RECLAMATION</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label> Filter </mat-label>
|
||||||
|
<input matInput type="text" (keyup)="doTransactionFilter($event.target.value)" placeholder="Filter">
|
||||||
|
<mat-icon matSuffix>search</mat-icon>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-table class="mat-elevation-z10" [dataSource]="transactionsDataSource" matSort matSortActive="created"
|
||||||
|
#TransactionTableSort="matSort" matSortDirection="asc" matSortDisableClear>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="sender">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header> Sender </th>
|
||||||
|
<td mat-cell *matCellDef="let transaction"> {{transaction?.sender?.vcard.fn[0].value}} </td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="recipient">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header> Recipient </th>
|
||||||
|
<td mat-cell *matCellDef="let transaction"> {{transaction?.recipient?.vcard.fn[0].value}} </td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="value">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header> Value </th>
|
||||||
|
<td mat-cell *matCellDef="let transaction">
|
||||||
|
<span *ngIf="transaction.type == 'transaction'">{{transaction?.value | tokenRatio}}</span>
|
||||||
|
<span *ngIf="transaction.type == 'conversion'">{{transaction?.toValue | tokenRatio}}</span>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="created">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header> Created </th>
|
||||||
|
<td mat-cell *matCellDef="let transaction"> {{transaction?.tx.timestamp | date}} </td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="type">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header> TYPE </th>
|
||||||
|
<td mat-cell *matCellDef="let transaction">
|
||||||
|
<span class="badge badge-success badge-pill"> {{transaction?.type}} </span>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<mat-header-row *matHeaderRowDef="transactionsDisplayedColumns"></mat-header-row>
|
||||||
|
<mat-row *matRowDef="let transaction; columns: transactionsDisplayedColumns" matRipple
|
||||||
|
(click)="viewTransaction(transaction)"></mat-row>
|
||||||
|
</mat-table>
|
||||||
|
|
||||||
|
<mat-paginator #TransactionTablePaginator="matPaginator" [pageSize]="transactionsDefaultPageSize"
|
||||||
|
[pageSizeOptions]="transactionsPageSizeOptions" showFirstLastButtons></mat-paginator>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-tab>
|
||||||
|
|
||||||
|
<mat-tab label="Users">
|
||||||
|
<div class="card mt-1">
|
||||||
|
<mat-card-title class="card-header">
|
||||||
|
Accounts
|
||||||
|
</mat-card-title>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row card-header">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label> ACCOUNT TYPE </mat-label>
|
||||||
|
<mat-select id="typeSelect" [(value)]="accountsType" (selectionChange)="filterAccounts()">
|
||||||
|
<mat-option value="all">ALL</mat-option>
|
||||||
|
<mat-option value="user">USER</mat-option>
|
||||||
|
<mat-option value="cashier">CASHIER</mat-option>
|
||||||
|
<mat-option value="vendor">VENDOR</mat-option>
|
||||||
|
<mat-option value="tokenAgent">TOKENAGENT</mat-option>
|
||||||
|
<mat-option value="group">GROUPACCOUNT</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label> Filter </mat-label>
|
||||||
|
<input matInput type="text" (keyup)="doUserFilter($event.target.value)" placeholder="Filter">
|
||||||
|
<mat-icon matSuffix>search</mat-icon>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-table class="mat-elevation-z10" [dataSource]="userDataSource" matSort #UserTableSort="matSort"
|
||||||
|
matSortActive="created" matSortDirection="desc" matSortDisableClear>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="name">
|
||||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> NAME </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let user"> {{user?.vcard.fn[0].value}} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="phone">
|
||||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> PHONE NUMBER </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let user"> {{user?.vcard.tel[0].value}} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="created">
|
||||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> CREATED </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let user"> {{user?.date_registered | date}} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="balance">
|
||||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> BALANCE </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let user"> {{user?.balance}} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="location">
|
||||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> LOCATION </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let user"> {{user?.location.area_name}} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<mat-header-row *matHeaderRowDef=userDisplayedColumns></mat-header-row>
|
||||||
|
<mat-row *matRowDef="let account; columns: userDisplayedColumns" (click)="viewAccount(account)"
|
||||||
|
matRipple></mat-row>
|
||||||
|
</mat-table>
|
||||||
|
|
||||||
|
<mat-paginator #UserTablePaginator="matPaginator" [pageSize]="usersDefaultPageSize"
|
||||||
|
[pageSizeOptions]="usersPageSizeOptions" showFirstLastButtons></mat-paginator>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-tab>
|
||||||
|
</mat-tab-group>
|
||||||
|
</div>
|
||||||
|
<app-footer appMenuSelection></app-footer>
|
||||||
|
</div>
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- End Page content -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
</div>
|
@ -0,0 +1,61 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AccountDetailsComponent } from '@pages/accounts/account-details/account-details.component';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
|
||||||
|
import {ActivatedRoute} from '@angular/router';
|
||||||
|
import {AccountsModule} from '@pages/accounts/accounts.module';
|
||||||
|
import {UserService} from '@app/_services';
|
||||||
|
import {AppModule} from '@app/app.module';
|
||||||
|
import {ActivatedRouteStub, FooterStubComponent, SidebarStubComponent, TopbarStubComponent, UserServiceStub} from '@src/testing';
|
||||||
|
|
||||||
|
describe('AccountDetailsComponent', () => {
|
||||||
|
let component: AccountDetailsComponent;
|
||||||
|
let fixture: ComponentFixture<AccountDetailsComponent>;
|
||||||
|
let httpClient: HttpClient;
|
||||||
|
let httpTestingController: HttpTestingController;
|
||||||
|
let route: ActivatedRouteStub;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
route = new ActivatedRouteStub();
|
||||||
|
route.setParamMap({ id: 'test' });
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [
|
||||||
|
AccountDetailsComponent,
|
||||||
|
FooterStubComponent,
|
||||||
|
SidebarStubComponent,
|
||||||
|
TopbarStubComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
AccountsModule,
|
||||||
|
AppModule,
|
||||||
|
HttpClientTestingModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: route },
|
||||||
|
{ provide: UserService, useClass: UserServiceStub }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
httpClient = TestBed.inject(HttpClient);
|
||||||
|
httpTestingController = TestBed.inject(HttpTestingController);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AccountDetailsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('#addTransfer() should toggle #isDisbursing', () => {
|
||||||
|
expect(component.isDisbursing).toBe(false, 'off at first');
|
||||||
|
component.addTransfer();
|
||||||
|
expect(component.isDisbursing).toBe(true, 'on after click');
|
||||||
|
component.addTransfer();
|
||||||
|
expect(component.isDisbursing).toBe(false, 'off after second click');
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,213 @@
|
|||||||
|
import {ChangeDetectionStrategy, Component, OnInit, ViewChild} from '@angular/core';
|
||||||
|
import {MatTableDataSource} from '@angular/material/table';
|
||||||
|
import {SelectionModel} from '@angular/cdk/collections';
|
||||||
|
import {MatPaginator} from '@angular/material/paginator';
|
||||||
|
import {MatSort} from '@angular/material/sort';
|
||||||
|
import {BlockSyncService, LocationService, LoggingService, TokenService, TransactionService, UserService} from '@app/_services';
|
||||||
|
import {ActivatedRoute, Params, Router} from '@angular/router';
|
||||||
|
import {first} from 'rxjs/operators';
|
||||||
|
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
|
||||||
|
import {CustomErrorStateMatcher} from '@app/_helpers';
|
||||||
|
import {Envelope, User} from 'cic-client-meta';
|
||||||
|
const vCard = require('vcard-parser');
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-account-details',
|
||||||
|
templateUrl: './account-details.component.html',
|
||||||
|
styleUrls: ['./account-details.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class AccountDetailsComponent implements OnInit {
|
||||||
|
transactionsDataSource: MatTableDataSource<any>;
|
||||||
|
transactionsDisplayedColumns = ['sender', 'recipient', 'value', 'created', 'type'];
|
||||||
|
transactionsDefaultPageSize = 10;
|
||||||
|
transactionsPageSizeOptions = [10, 20, 50, 100];
|
||||||
|
@ViewChild('TransactionTablePaginator', {static: true}) transactionTablePaginator: MatPaginator;
|
||||||
|
@ViewChild('TransactionTableSort', {static: true}) transactionTableSort: MatSort;
|
||||||
|
|
||||||
|
userDataSource: MatTableDataSource<any>;
|
||||||
|
userDisplayedColumns = ['name', 'phone', 'created', 'balance', 'location'];
|
||||||
|
usersDefaultPageSize = 10;
|
||||||
|
usersPageSizeOptions = [10, 20, 50, 100];
|
||||||
|
@ViewChild('UserTablePaginator', {static: true}) userTablePaginator: MatPaginator;
|
||||||
|
@ViewChild('UserTableSort', {static: true}) userTableSort: MatSort;
|
||||||
|
|
||||||
|
historyDataSource: MatTableDataSource<any>;
|
||||||
|
historyDisplayedColumns = ['user', 'action', 'staff', 'timestamp'];
|
||||||
|
@ViewChild('HistoryTablePaginator', {static: true}) historyTablePaginator: MatPaginator;
|
||||||
|
@ViewChild('HistoryTableSort', {static: true}) historyTableSort: MatSort;
|
||||||
|
|
||||||
|
accountInfoForm: FormGroup;
|
||||||
|
account: any;
|
||||||
|
accountAddress: string;
|
||||||
|
accountBalance: number;
|
||||||
|
metaAccount: any;
|
||||||
|
accounts: any[] = [];
|
||||||
|
accountsType = 'all';
|
||||||
|
date: string;
|
||||||
|
time: number;
|
||||||
|
isDisbursing = false;
|
||||||
|
locations: any;
|
||||||
|
transaction: any;
|
||||||
|
transactions: any[];
|
||||||
|
transactionsType = 'all';
|
||||||
|
matcher = new CustomErrorStateMatcher();
|
||||||
|
submitted: boolean = false;
|
||||||
|
bloxbergLink: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private locationService: LocationService,
|
||||||
|
private transactionService: TransactionService,
|
||||||
|
private userService: UserService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
private tokenService: TokenService,
|
||||||
|
private loggingService: LoggingService,
|
||||||
|
private blockSyncService: BlockSyncService
|
||||||
|
) {
|
||||||
|
this.accountInfoForm = this.formBuilder.group({
|
||||||
|
name: ['', Validators.required],
|
||||||
|
phoneNumber: ['', Validators.required],
|
||||||
|
age: ['', Validators.required],
|
||||||
|
type: ['', Validators.required],
|
||||||
|
bio: ['', Validators.required],
|
||||||
|
gender: ['', Validators.required],
|
||||||
|
businessCategory: ['', Validators.required],
|
||||||
|
userLocation: ['', Validators.required],
|
||||||
|
location: ['', Validators.required],
|
||||||
|
locationType: ['', Validators.required],
|
||||||
|
});
|
||||||
|
this.route.paramMap.subscribe(async (params: Params) => {
|
||||||
|
this.accountAddress = params.get('id');
|
||||||
|
this.bloxbergLink = 'https://blockexplorer.bloxberg.org/address/' + this.accountAddress + '/transactions';
|
||||||
|
this.userService.getAccountDetailsFromMeta(await User.toKey(this.accountAddress)).pipe(first()).subscribe(res => {
|
||||||
|
this.metaAccount = Envelope.fromJSON(JSON.stringify(res.body)).unwrap();
|
||||||
|
this.account = this.metaAccount.m.data;
|
||||||
|
this.loggingService.sendInfoLevelMessage(this.account);
|
||||||
|
this.tokenService.getTokenBalance(this.accountAddress).then(r => this.accountBalance = r);
|
||||||
|
this.account.vcard = vCard.parse(atob(this.account.vcard));
|
||||||
|
this.accountInfoForm.patchValue({
|
||||||
|
name: this.account.vcard?.fn[0].value,
|
||||||
|
phoneNumber: this.account.vcard?.tel[0].value,
|
||||||
|
age: this.account.age,
|
||||||
|
type: this.account.type,
|
||||||
|
bio: this.account.products,
|
||||||
|
gender: this.account.gender,
|
||||||
|
businessCategory: this.account.category,
|
||||||
|
userLocation: this.account.location.area_name,
|
||||||
|
location: this.account.location.area,
|
||||||
|
locationType: this.account.location.area_type,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.userService.getHistoryByUser(this.accountAddress).pipe(first()).subscribe(response => {
|
||||||
|
this.historyDataSource = new MatTableDataSource<any>(response.body);
|
||||||
|
this.historyDataSource.paginator = this.historyTablePaginator;
|
||||||
|
this.historyDataSource.sort = this.historyTableSort;
|
||||||
|
});
|
||||||
|
this.blockSyncService.blockSync(this.accountAddress);
|
||||||
|
});
|
||||||
|
this.userService.getAccounts();
|
||||||
|
this.locationService.getLocations();
|
||||||
|
this.locationService.locationsSubject.subscribe(locations => {
|
||||||
|
this.locations = locations;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.userService.accountsSubject.subscribe(accounts => {
|
||||||
|
this.userDataSource = new MatTableDataSource<any>(accounts);
|
||||||
|
this.userDataSource.paginator = this.userTablePaginator;
|
||||||
|
this.userDataSource.sort = this.userTableSort;
|
||||||
|
this.accounts = accounts;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.transactionService.transactionsSubject.subscribe(transactions => {
|
||||||
|
this.transactionsDataSource = new MatTableDataSource<any>(transactions);
|
||||||
|
this.transactionsDataSource.paginator = this.transactionTablePaginator;
|
||||||
|
this.transactionsDataSource.sort = this.transactionTableSort;
|
||||||
|
this.transactions = transactions;
|
||||||
|
});
|
||||||
|
|
||||||
|
const d = new Date();
|
||||||
|
this.date = `${d.getDate()}/${d.getMonth()}/${d.getFullYear()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTransfer(): void {
|
||||||
|
this.isDisbursing = !this.isDisbursing;
|
||||||
|
}
|
||||||
|
|
||||||
|
doTransactionFilter(value: string): void {
|
||||||
|
this.transactionsDataSource.filter = value.trim().toLocaleLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
doUserFilter(value: string): void {
|
||||||
|
this.userDataSource.filter = value.trim().toLocaleLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
doHistoryFilter(value: string): void {
|
||||||
|
this.historyDataSource.filter = value.trim().toLocaleLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
viewTransaction(transaction): void {
|
||||||
|
this.transaction = transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
viewAccount(account): void {
|
||||||
|
this.router.navigateByUrl(`/accounts/${account.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
get accountInfoFormStub(): any { return this.accountInfoForm.controls; }
|
||||||
|
|
||||||
|
saveInfo(): void {
|
||||||
|
this.submitted = true;
|
||||||
|
if (this.accountInfoForm.invalid) { return; }
|
||||||
|
this.userService.changeAccountInfo(
|
||||||
|
this.account.address,
|
||||||
|
this.accountInfoFormStub.name.value,
|
||||||
|
this.accountInfoFormStub.phoneNumber.value,
|
||||||
|
this.accountInfoFormStub.age.value,
|
||||||
|
this.accountInfoFormStub.type.value,
|
||||||
|
this.accountInfoFormStub.bio.value,
|
||||||
|
this.accountInfoFormStub.gender.value,
|
||||||
|
this.accountInfoFormStub.businessCategory.value,
|
||||||
|
this.accountInfoFormStub.userLocation.value,
|
||||||
|
this.accountInfoFormStub.location.value,
|
||||||
|
this.accountInfoFormStub.locationType.value,
|
||||||
|
this.metaAccount
|
||||||
|
).then(res => this.loggingService.sendInfoLevelMessage(`Response: ${res}`));
|
||||||
|
this.submitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
filterAccounts(): void {
|
||||||
|
if (this.accountsType === 'all') {
|
||||||
|
this.userService.accountsSubject.subscribe(accounts => {
|
||||||
|
this.userDataSource.data = accounts;
|
||||||
|
this.accounts = accounts;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.userDataSource.data = this.accounts.filter(account => account.type === this.accountsType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filterTransactions(): void {
|
||||||
|
if (this.transactionsType === 'all') {
|
||||||
|
this.transactionService.transactionsSubject.subscribe(transactions => {
|
||||||
|
this.transactionsDataSource.data = transactions;
|
||||||
|
this.transactions = transactions;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.transactionsDataSource.data = this.transactions.filter(transaction => transaction.type === this.transactionsType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPin(): void {
|
||||||
|
this.userService.resetPin(this.account.phone).pipe(first()).subscribe(res => {
|
||||||
|
this.loggingService.sendInfoLevelMessage(`Response: ${res.body}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public trackByName(index, item): string {
|
||||||
|
return item.name;
|
||||||
|
}
|
||||||
|
}
|
21
src/app/pages/accounts/accounts-routing.module.ts
Normal file
21
src/app/pages/accounts/accounts-routing.module.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { AccountsComponent } from '@pages/accounts/accounts.component';
|
||||||
|
import {CreateAccountComponent} from '@pages/accounts/create-account/create-account.component';
|
||||||
|
import {ExportAccountsComponent} from '@pages/accounts/export-accounts/export-accounts.component';
|
||||||
|
import {AccountDetailsComponent} from '@pages/accounts/account-details/account-details.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{ path: '', component: AccountsComponent },
|
||||||
|
// { path: 'create', component: CreateAccountComponent },
|
||||||
|
{ path: 'export', component: ExportAccountsComponent },
|
||||||
|
{ path: ':id', component: AccountDetailsComponent },
|
||||||
|
{ path: '**', redirectTo: '', pathMatch: 'full' }
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class AccountsRoutingModule { }
|
86
src/app/pages/accounts/accounts.component.html
Normal file
86
src/app/pages/accounts/accounts.component.html
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<!-- Begin page -->
|
||||||
|
<div class="wrapper">
|
||||||
|
<app-sidebar></app-sidebar>
|
||||||
|
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- Start Page Content here -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
<app-topbar></app-topbar>
|
||||||
|
<!-- Start Content-->
|
||||||
|
<div class="container-fluid" appMenuSelection>
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a routerLink="/home">Home</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Accounts</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<div class="card">
|
||||||
|
<mat-card-title class="card-header">
|
||||||
|
Accounts
|
||||||
|
</mat-card-title>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row card-header">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label> ACCOUNT TYPE </mat-label>
|
||||||
|
<mat-select id="typeSelect" [(value)]="accountsType" (selectionChange)="filterAccounts()">
|
||||||
|
<mat-option value="all">ALL</mat-option>
|
||||||
|
<mat-option value="user">USER</mat-option>
|
||||||
|
<mat-option value="cashier">CASHIER</mat-option>
|
||||||
|
<mat-option value="vendor">VENDOR</mat-option>
|
||||||
|
<mat-option value="tokenAgent">TOKENAGENT</mat-option>
|
||||||
|
<mat-option value="group">GROUPACCOUNT</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<button mat-raised-button color="primary" routerLink="/accounts/export" type="button" class="btn btn-outline-primary ml-auto"> EXPORT </button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label> Filter </mat-label>
|
||||||
|
<input matInput type="text" (keyup)="doFilter($event.target.value)" placeholder="Filter">
|
||||||
|
<mat-icon matSuffix>search</mat-icon>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-table class="mat-elevation-z10" [dataSource]="dataSource" matSort matSortActive="created"
|
||||||
|
matSortDirection="desc" matSortDisableClear>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="name">
|
||||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> NAME </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let user"> {{user?.vcard.fn[0].value}} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="phone">
|
||||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> PHONE NUMBER </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let user"> {{user?.vcard.tel[0].value}} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="created">
|
||||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> CREATED </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let user"> {{user?.date_registered | date}} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="balance">
|
||||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> BALANCE </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let user"> {{user?.balance | tokenRatio}} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="location">
|
||||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> LOCATION </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let user"> {{user?.location.area_name}} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||||
|
<mat-row *matRowDef="let account; columns: displayedColumns" (click)="viewAccount(account)" matRipple></mat-row>
|
||||||
|
</mat-table>
|
||||||
|
|
||||||
|
<mat-paginator [pageSize]="defaultPageSize" [pageSizeOptions]="pageSizeOptions" showFirstLastButtons></mat-paginator>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-footer appMenuSelection></app-footer>
|
||||||
|
</div>
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- End Page content -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
</div>
|
0
src/app/pages/accounts/accounts.component.scss
Normal file
0
src/app/pages/accounts/accounts.component.scss
Normal file
48
src/app/pages/accounts/accounts.component.spec.ts
Normal file
48
src/app/pages/accounts/accounts.component.spec.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AccountsComponent } from './accounts.component';
|
||||||
|
import {FooterStubComponent, SidebarStubComponent, TopbarStubComponent, UserServiceStub} from '@src/testing';
|
||||||
|
import {AccountsModule} from '@pages/accounts/accounts.module';
|
||||||
|
import {AppModule} from '@app/app.module';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
|
||||||
|
import {UserService} from '@app/_services';
|
||||||
|
|
||||||
|
describe('AccountsComponent', () => {
|
||||||
|
let component: AccountsComponent;
|
||||||
|
let fixture: ComponentFixture<AccountsComponent>;
|
||||||
|
let httpClient: HttpClient;
|
||||||
|
let httpTestingController: HttpTestingController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [
|
||||||
|
AccountsComponent,
|
||||||
|
FooterStubComponent,
|
||||||
|
SidebarStubComponent,
|
||||||
|
TopbarStubComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
AccountsModule,
|
||||||
|
AppModule,
|
||||||
|
HttpClientTestingModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: UserService, useClass: UserServiceStub }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
httpClient = TestBed.inject(HttpClient);
|
||||||
|
httpTestingController = TestBed.inject(HttpTestingController);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AccountsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
59
src/app/pages/accounts/accounts.component.ts
Normal file
59
src/app/pages/accounts/accounts.component.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import {ChangeDetectionStrategy, Component, OnInit, ViewChild} from '@angular/core';
|
||||||
|
import {MatTableDataSource} from '@angular/material/table';
|
||||||
|
import {MatPaginator} from '@angular/material/paginator';
|
||||||
|
import {MatSort} from '@angular/material/sort';
|
||||||
|
import {UserService} from '@app/_services';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-accounts',
|
||||||
|
templateUrl: './accounts.component.html',
|
||||||
|
styleUrls: ['./accounts.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class AccountsComponent implements OnInit {
|
||||||
|
dataSource: MatTableDataSource<any>;
|
||||||
|
accounts: any[] = [];
|
||||||
|
displayedColumns = ['name', 'phone', 'created', 'balance', 'location'];
|
||||||
|
defaultPageSize = 10;
|
||||||
|
pageSizeOptions = [10, 20, 50, 100];
|
||||||
|
accountsType = 'all';
|
||||||
|
|
||||||
|
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||||
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private userService: UserService,
|
||||||
|
private router: Router
|
||||||
|
) {
|
||||||
|
this.userService.loadAccounts(100).then();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.userService.accountsSubject.subscribe(accounts => {
|
||||||
|
this.dataSource = new MatTableDataSource<any>(accounts);
|
||||||
|
this.dataSource.paginator = this.paginator;
|
||||||
|
this.dataSource.sort = this.sort;
|
||||||
|
this.accounts = accounts;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
doFilter(value: string): void {
|
||||||
|
this.dataSource.filter = value.trim().toLocaleLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
viewAccount(account): void {
|
||||||
|
this.router.navigateByUrl(`/accounts/${account.identities.evm['bloxberg:8996']}`).then();
|
||||||
|
}
|
||||||
|
|
||||||
|
filterAccounts(): void {
|
||||||
|
if (this.accountsType === 'all') {
|
||||||
|
this.userService.accountsSubject.subscribe(accounts => {
|
||||||
|
this.dataSource.data = accounts;
|
||||||
|
this.accounts = accounts;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.dataSource.data = this.accounts.filter(account => account.type === this.accountsType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
53
src/app/pages/accounts/accounts.module.ts
Normal file
53
src/app/pages/accounts/accounts.module.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
import { AccountsRoutingModule } from '@pages/accounts/accounts-routing.module';
|
||||||
|
import { AccountsComponent } from '@pages/accounts/accounts.component';
|
||||||
|
import {SharedModule} from '@app/shared/shared.module';
|
||||||
|
import { AccountDetailsComponent } from '@pages/accounts/account-details/account-details.component';
|
||||||
|
import {DataTablesModule} from 'angular-datatables';
|
||||||
|
import { CreateAccountComponent } from '@pages/accounts/create-account/create-account.component';
|
||||||
|
import { DisbursementComponent } from '@pages/accounts/disbursement/disbursement.component';
|
||||||
|
import { ExportAccountsComponent } from '@pages/accounts/export-accounts/export-accounts.component';
|
||||||
|
import {MatTableModule} from '@angular/material/table';
|
||||||
|
import {MatSortModule} from '@angular/material/sort';
|
||||||
|
import {MatCheckboxModule} from '@angular/material/checkbox';
|
||||||
|
import {MatPaginatorModule} from '@angular/material/paginator';
|
||||||
|
import {MatInputModule} from '@angular/material/input';
|
||||||
|
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||||
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
|
import {MatCardModule} from '@angular/material/card';
|
||||||
|
import {MatIconModule} from '@angular/material/icon';
|
||||||
|
import {MatSelectModule} from '@angular/material/select';
|
||||||
|
import {TransactionsModule} from '@pages/transactions/transactions.module';
|
||||||
|
import {MatTabsModule} from '@angular/material/tabs';
|
||||||
|
import {MatRippleModule} from '@angular/material/core';
|
||||||
|
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
|
||||||
|
import {ReactiveFormsModule} from '@angular/forms';
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AccountsComponent, AccountDetailsComponent, CreateAccountComponent, DisbursementComponent, ExportAccountsComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
AccountsRoutingModule,
|
||||||
|
SharedModule,
|
||||||
|
DataTablesModule,
|
||||||
|
MatTableModule,
|
||||||
|
MatSortModule,
|
||||||
|
MatCheckboxModule,
|
||||||
|
MatPaginatorModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatSelectModule,
|
||||||
|
TransactionsModule,
|
||||||
|
MatTabsModule,
|
||||||
|
MatRippleModule,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
ReactiveFormsModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AccountsModule { }
|
@ -0,0 +1,147 @@
|
|||||||
|
<!-- Begin page -->
|
||||||
|
<div class="wrapper">
|
||||||
|
<app-sidebar></app-sidebar>
|
||||||
|
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- Start Page Content here -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
<app-topbar></app-topbar>
|
||||||
|
<!-- Start Content-->
|
||||||
|
<div class="container-fluid" appMenuSelection>
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a routerLink="/home">Home</a></li>
|
||||||
|
<li class="breadcrumb-item"><a routerLink="/accounts">Accounts</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Create Account</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<div class="card">
|
||||||
|
<mat-card-title class="card-header text-center">
|
||||||
|
CREATE A USER ACCOUNT
|
||||||
|
</mat-card-title>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="row form-inline" [formGroup]="createForm" (ngSubmit)="onSubmit()">
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Account Type: </mat-label>
|
||||||
|
<mat-select id="accountType" formControlName="accountType" [errorStateMatcher]="matcher">
|
||||||
|
<mat-option value="user">USER</mat-option>
|
||||||
|
<mat-option value="cashier">CASHIER</mat-option>
|
||||||
|
<mat-option value="vendor">VENDOR</mat-option>
|
||||||
|
<mat-option value="tokenAgent">TOKENAGENT</mat-option>
|
||||||
|
<mat-option value="group">GROUPACCOUNT</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
<mat-error *ngIf="submitted && createFormStub.accountType.errors">Account type is required.</mat-error>
|
||||||
|
</mat-form-field><br>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>ID Number: </mat-label>
|
||||||
|
<input matInput type="text" id="idNumber" placeholder="ID Number" formControlName="idNumber" [errorStateMatcher]="matcher">
|
||||||
|
<mat-error *ngIf="submitted && createFormStub.idNumber.errors">ID Number is required.</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Phone Number: </mat-label>
|
||||||
|
<input matInput type="text" id="phoneNumber" placeholder="Phone Number" formControlName="phoneNumber" [errorStateMatcher]="matcher">
|
||||||
|
<mat-error *ngIf="submitted && createFormStub.phoneNumber.errors">Phone Number is required.</mat-error>
|
||||||
|
</mat-form-field><br>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Given Name(s):* </mat-label>
|
||||||
|
<input matInput type="text" id="givenNames" placeholder="Given Names" formControlName="givenName" [errorStateMatcher]="matcher">
|
||||||
|
<mat-error *ngIf="submitted && createFormStub.givenName.errors">Given Names are required.</mat-error>
|
||||||
|
</mat-form-field><br>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Family/Surname: </mat-label>
|
||||||
|
<input matInput type="text" id="surname" placeholder="Surname" formControlName="surname" [errorStateMatcher]="matcher">
|
||||||
|
<mat-error *ngIf="submitted && createFormStub.surname.errors">Surname is required.</mat-error>
|
||||||
|
</mat-form-field><br>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Directory Entry: </mat-label>
|
||||||
|
<input matInput type="text" id="directoryEntry" placeholder="Directory Entry" formControlName="directoryEntry" [errorStateMatcher]="matcher">
|
||||||
|
<mat-error *ngIf="submitted && createFormStub.directoryEntry.errors">Directory Entry is required.</mat-error>
|
||||||
|
</mat-form-field><br>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Location: </mat-label>
|
||||||
|
<mat-select id="location" formControlName="location" [errorStateMatcher]="matcher">
|
||||||
|
<div *ngFor="let county of locations; trackBy: trackByName">
|
||||||
|
<div *ngFor="let district of county.districts; trackBy: trackByName">
|
||||||
|
<mat-optgroup *ngFor="let location of district.locations; trackBy: trackByName" [label]="county.name + ' / ' + district.name + ' / ' + location.name">
|
||||||
|
<mat-option *ngFor="let village of location.villages; trackBy: trackByName" [value]="village">
|
||||||
|
{{village}}
|
||||||
|
</mat-option>
|
||||||
|
</mat-optgroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-select>
|
||||||
|
<mat-error *ngIf="submitted && createFormStub.location.errors">Location is required.</mat-error>
|
||||||
|
</mat-form-field><br>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Gender: </mat-label>
|
||||||
|
<mat-select id="gender" formControlName="gender" [errorStateMatcher]="matcher">
|
||||||
|
<mat-option value="female">FEMALE</mat-option>
|
||||||
|
<mat-option value="male">MALE</mat-option>
|
||||||
|
<mat-option value="other">OTHER</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
<mat-error *ngIf="submitted && createFormStub.gender.errors">Gender is required.</mat-error>
|
||||||
|
</mat-form-field><br>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Referrer Phone Number: </mat-label>
|
||||||
|
<input matInput type="text" id="referredBy" placeholder="Reffered By" formControlName="referrer" [errorStateMatcher]="matcher">
|
||||||
|
<mat-error *ngIf="submitted && createFormStub.referrer.errors">Referrer is required.</mat-error>
|
||||||
|
</mat-form-field><br>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Business Category: </mat-label>
|
||||||
|
<mat-select id="businessCategory" formControlName="businessCategory" [errorStateMatcher]="matcher">
|
||||||
|
<mat-option value="food/water">Food/Water</mat-option>
|
||||||
|
<mat-option value="fuel/energy">Fuel/Energy</mat-option>
|
||||||
|
<mat-option value="education">Education</mat-option>
|
||||||
|
<mat-option value="health">Health</mat-option>
|
||||||
|
<mat-option value="shop">Shop</mat-option>
|
||||||
|
<mat-option value="environment">Environment</mat-option>
|
||||||
|
<mat-option value="transport">Transport</mat-option>
|
||||||
|
<mat-option value="farming/labour">Farming/Labour</mat-option>
|
||||||
|
<mat-option value="savings">Savings Group</mat-option>
|
||||||
|
<mat-option value="other">Other</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
<mat-error *ngIf="submitted && createFormStub.businessCategory.errors">Business Category is required.</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button mat-raised-button color="primary" type="submit" class="btn btn-outline-primary ml-3" (click)="onSubmit()">Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-footer appMenuSelection></app-footer>
|
||||||
|
</div>
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- End Page content -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
</div>
|
@ -0,0 +1,38 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CreateAccountComponent } from '@pages/accounts/create-account/create-account.component';
|
||||||
|
import {AccountsModule} from '@pages/accounts/accounts.module';
|
||||||
|
import {AppModule} from '@app/app.module';
|
||||||
|
import {FooterStubComponent, SidebarStubComponent, TopbarStubComponent} from '@src/testing';
|
||||||
|
|
||||||
|
|
||||||
|
describe('CreateAccountComponent', () => {
|
||||||
|
let component: CreateAccountComponent;
|
||||||
|
let fixture: ComponentFixture<CreateAccountComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [
|
||||||
|
CreateAccountComponent,
|
||||||
|
FooterStubComponent,
|
||||||
|
SidebarStubComponent,
|
||||||
|
TopbarStubComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
AccountsModule,
|
||||||
|
AppModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CreateAccountComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,71 @@
|
|||||||
|
import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
|
||||||
|
import {LocationService, UserService} from '@app/_services';
|
||||||
|
import {CustomErrorStateMatcher} from '@app/_helpers';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-create-account',
|
||||||
|
templateUrl: './create-account.component.html',
|
||||||
|
styleUrls: ['./create-account.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class CreateAccountComponent implements OnInit {
|
||||||
|
createForm: FormGroup;
|
||||||
|
matcher = new CustomErrorStateMatcher();
|
||||||
|
submitted: boolean = false;
|
||||||
|
locations: any;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private router: Router,
|
||||||
|
private userService: UserService,
|
||||||
|
private locationService: LocationService
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.createForm = this.formBuilder.group({
|
||||||
|
accountType: ['', Validators.required],
|
||||||
|
idNumber: ['', Validators.required],
|
||||||
|
phoneNumber: ['', Validators.required],
|
||||||
|
givenName: ['', Validators.required],
|
||||||
|
surname: ['', Validators.required],
|
||||||
|
directoryEntry: ['', Validators.required],
|
||||||
|
location: ['', Validators.required],
|
||||||
|
gender: ['', Validators.required],
|
||||||
|
referrer: ['', Validators.required],
|
||||||
|
businessCategory: ['', Validators.required]
|
||||||
|
});
|
||||||
|
this.locationService.getLocations();
|
||||||
|
this.locationService.locationsSubject.subscribe(locations => {
|
||||||
|
this.locations = locations;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get createFormStub(): any { return this.createForm.controls; }
|
||||||
|
|
||||||
|
onSubmit(): void {
|
||||||
|
this.submitted = true;
|
||||||
|
if (this.createForm.invalid) { return; }
|
||||||
|
// this.userService.createAccount(
|
||||||
|
// this.createFormStub.accountType.value,
|
||||||
|
// this.createFormStub.idNumber.value,
|
||||||
|
// this.createFormStub.phoneNumber.value,
|
||||||
|
// this.createFormStub.givenName.value,
|
||||||
|
// this.createFormStub.surname.value,
|
||||||
|
// this.createFormStub.directoryEntry.value,
|
||||||
|
// this.createFormStub.location.value,
|
||||||
|
// this.createFormStub.gender.value,
|
||||||
|
// this.createFormStub.referrer.value,
|
||||||
|
// this.createFormStub.businessCategory.value,
|
||||||
|
// ).pipe(first()).subscribe(res => {
|
||||||
|
// console.log(res);
|
||||||
|
// });
|
||||||
|
// this.router.navigateByUrl(`/accounts`);
|
||||||
|
this.submitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public trackByName(index, item): string {
|
||||||
|
return item.name;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
<div class="card">
|
||||||
|
<mat-card-title class="card-header">
|
||||||
|
<div class="row">
|
||||||
|
NEW TRANSFER
|
||||||
|
<button mat-raised-button color="warn" type="button" class="btn btn-outline-danger ml-auto mr-2" (click)="cancel()"> CANCEL </button>
|
||||||
|
</div>
|
||||||
|
</mat-card-title>
|
||||||
|
<div class="card-body">
|
||||||
|
<form [formGroup]="disbursementForm" (ngSubmit)="createTransfer()">
|
||||||
|
<div class="row form-inline">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label> TRANSACTION TYPE </mat-label>
|
||||||
|
<mat-select id="transactionType" formControlName="transactionType" [errorStateMatcher]="matcher">
|
||||||
|
<mat-option value="disbursement">DISBURSEMENT</mat-option>
|
||||||
|
<mat-option value="transfer">TRANSFER</mat-option>
|
||||||
|
<mat-option value="deposit">DEPOSIT</mat-option>
|
||||||
|
<mat-option value="reclamation">RECLAMATION</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
<mat-error *ngIf="submitted && disbursementFormStub.transactionType.errors">Transaction type is required.</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field *ngIf="disbursementFormStub.transactionType.value === 'transfer'" appearance="outline" class="ml-3">
|
||||||
|
<mat-label>Enter Recipient: </mat-label>
|
||||||
|
<input matInput type="text" id="recipient" placeholder="Recipient" formControlName="recipient">
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="ml-3">
|
||||||
|
<mat-label>Enter Amount: </mat-label>
|
||||||
|
<input matInput type="text" id="amount" placeholder="Amount" formControlName="amount" [errorStateMatcher]="matcher">
|
||||||
|
<mat-error *ngIf="submitted && disbursementFormStub.amount.errors">Amount is required.</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
<button mat-raised-button color="primary" type="submit" class="btn btn-outline-primary ml-3" style="margin-bottom: 1.2rem;">
|
||||||
|
CREATE TRANSFER
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,37 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DisbursementComponent } from '@pages/accounts/disbursement/disbursement.component';
|
||||||
|
import {AccountsModule} from '@pages/accounts/accounts.module';
|
||||||
|
import {AppModule} from '@app/app.module';
|
||||||
|
import {FooterStubComponent, SidebarStubComponent, TopbarStubComponent} from '@src/testing';
|
||||||
|
|
||||||
|
describe('DisbursementComponent', () => {
|
||||||
|
let component: DisbursementComponent;
|
||||||
|
let fixture: ComponentFixture<DisbursementComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [
|
||||||
|
DisbursementComponent,
|
||||||
|
FooterStubComponent,
|
||||||
|
SidebarStubComponent,
|
||||||
|
TopbarStubComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
AccountsModule,
|
||||||
|
AppModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DisbursementComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,51 @@
|
|||||||
|
import {Component, OnInit, EventEmitter, Output, Input, ChangeDetectionStrategy} from '@angular/core';
|
||||||
|
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
|
||||||
|
import {CustomErrorStateMatcher} from '@app/_helpers';
|
||||||
|
import {TransactionService} from '@app/_services';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-disbursement',
|
||||||
|
templateUrl: './disbursement.component.html',
|
||||||
|
styleUrls: ['./disbursement.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class DisbursementComponent implements OnInit {
|
||||||
|
@Input() account;
|
||||||
|
@Output() cancelDisbursmentEvent = new EventEmitter();
|
||||||
|
disbursementForm: FormGroup;
|
||||||
|
matcher = new CustomErrorStateMatcher();
|
||||||
|
submitted: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private transactionService: TransactionService
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.disbursementForm = this.formBuilder.group({
|
||||||
|
transactionType: ['', Validators.required],
|
||||||
|
recipient: '',
|
||||||
|
amount: ['', Validators.required]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get disbursementFormStub(): any { return this.disbursementForm.controls; }
|
||||||
|
|
||||||
|
async createTransfer(): Promise<void> {
|
||||||
|
this.submitted = true;
|
||||||
|
if (this.disbursementForm.invalid) { return; }
|
||||||
|
if (this.disbursementFormStub.transactionType.value === 'transfer') {
|
||||||
|
await this.transactionService.transferRequest(
|
||||||
|
this.account.token,
|
||||||
|
this.account.address,
|
||||||
|
this.disbursementFormStub.recipient.value,
|
||||||
|
this.disbursementFormStub.amount.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.submitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(): void {
|
||||||
|
this.cancelDisbursmentEvent.emit();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
<!-- Begin page -->
|
||||||
|
<div class="wrapper">
|
||||||
|
<app-sidebar></app-sidebar>
|
||||||
|
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- Start Page Content here -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
<app-topbar></app-topbar>
|
||||||
|
<!-- Start Content-->
|
||||||
|
<div class="container-fluid" appMenuSelection>
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a routerLink="/home">Home</a></li>
|
||||||
|
<li class="breadcrumb-item"><a routerLink="/accounts">Accounts</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Export Accounts</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<div class="card">
|
||||||
|
<mat-card-title class="card-header">
|
||||||
|
EXPORT ACCOUNTS
|
||||||
|
</mat-card-title>
|
||||||
|
<div class="card-body">
|
||||||
|
<form [formGroup]="exportForm" (ngSubmit)="export()">
|
||||||
|
<div class="form-inline mb-2">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Export : </mat-label>
|
||||||
|
<mat-select id="accountType" formControlName="accountType" [errorStateMatcher]="matcher">
|
||||||
|
<mat-option value="vendors">VENDORS</mat-option>
|
||||||
|
<mat-option value="partners">PARTNERS</mat-option>
|
||||||
|
<mat-option value="selected">SELECTED</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
<mat-error *ngIf="submitted && exportFormStub.accountType.errors">Account Type is required.</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="form-inline mb-3">
|
||||||
|
<div class="form-group form-check">
|
||||||
|
<label class="form-check-label mr-2" for="transfers">Include transfers?</label>
|
||||||
|
<mat-checkbox id="transfers" formControlName="transfers"></mat-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button mat-raised-button color="primary" type="submit" class="btn btn-outline-primary"> EXPORT </button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-footer appMenuSelection></app-footer>
|
||||||
|
</div>
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- End Page content -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
</div>
|
@ -0,0 +1,37 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ExportAccountsComponent } from '@pages/accounts/export-accounts/export-accounts.component';
|
||||||
|
import {AccountsModule} from '@pages/accounts/accounts.module';
|
||||||
|
import {AppModule} from '@app/app.module';
|
||||||
|
import {FooterStubComponent, SidebarStubComponent, TopbarStubComponent} from '@src/testing';
|
||||||
|
|
||||||
|
describe('ExportAccountsComponent', () => {
|
||||||
|
let component: ExportAccountsComponent;
|
||||||
|
let fixture: ComponentFixture<ExportAccountsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [
|
||||||
|
ExportAccountsComponent,
|
||||||
|
FooterStubComponent,
|
||||||
|
SidebarStubComponent,
|
||||||
|
TopbarStubComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
AccountsModule,
|
||||||
|
AppModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ExportAccountsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,34 @@
|
|||||||
|
import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core';
|
||||||
|
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
|
||||||
|
import {CustomErrorStateMatcher} from '@app/_helpers';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-export-accounts',
|
||||||
|
templateUrl: './export-accounts.component.html',
|
||||||
|
styleUrls: ['./export-accounts.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class ExportAccountsComponent implements OnInit {
|
||||||
|
exportForm: FormGroup;
|
||||||
|
matcher = new CustomErrorStateMatcher();
|
||||||
|
submitted: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private formBuilder: FormBuilder
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.exportForm = this.formBuilder.group({
|
||||||
|
accountType: ['', Validators.required],
|
||||||
|
transfers: ['']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get exportFormStub(): any { return this.exportForm.controls; }
|
||||||
|
|
||||||
|
export(): void {
|
||||||
|
this.submitted = true;
|
||||||
|
if (this.exportForm.invalid) { return; }
|
||||||
|
this.submitted = false;
|
||||||
|
}
|
||||||
|
}
|
12
src/app/pages/admin/admin-routing.module.ts
Normal file
12
src/app/pages/admin/admin-routing.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { AdminComponent } from '@pages/admin/admin.component';
|
||||||
|
|
||||||
|
const routes: Routes = [{ path: '', component: AdminComponent }];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class AdminRoutingModule { }
|
102
src/app/pages/admin/admin.component.html
Normal file
102
src/app/pages/admin/admin.component.html
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<!-- Begin page -->
|
||||||
|
<div class="wrapper">
|
||||||
|
<app-sidebar></app-sidebar>
|
||||||
|
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- Start Page Content here -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
<app-topbar></app-topbar>
|
||||||
|
<!-- Start Content-->
|
||||||
|
<div class="container-fluid" appMenuSelection>
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a routerLink="/home">Home</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Admin</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<div class="card">
|
||||||
|
<mat-card-title class="card-header">
|
||||||
|
Actions
|
||||||
|
</mat-card-title>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label> Filter </mat-label>
|
||||||
|
<input matInput type="text" (keyup)="doFilter($event.target.value)" placeholder="Filter">
|
||||||
|
<mat-icon matSuffix>search</mat-icon>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-table class="mat-elevation-z10" [dataSource]="dataSource" multiTemplateDataRows>
|
||||||
|
|
||||||
|
<!-- Expand Column -->
|
||||||
|
<ng-container matColumnDef="expand">
|
||||||
|
<mat-header-cell *matHeaderCellDef> Expand </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let element" (click)="expandCollapse(element)">
|
||||||
|
<span *ngIf="!element.isExpanded" class="signs"> + </span>
|
||||||
|
<span *ngIf="element.isExpanded" class="signs"> - </span>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="user">
|
||||||
|
<mat-header-cell *matHeaderCellDef> NAME </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let action"> {{action.user}} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="role">
|
||||||
|
<mat-header-cell *matHeaderCellDef> ROLE </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let action"> {{action.role}} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="action">
|
||||||
|
<mat-header-cell *matHeaderCellDef> ACTION </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let action"> {{action.action}} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="status">
|
||||||
|
<mat-header-cell *matHeaderCellDef> STATUS </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let action">
|
||||||
|
<span *ngIf="action.approval == true" class="badge badge-success badge-pill"> {{approvalStatus(action.approval)}} </span>
|
||||||
|
<span *ngIf="action.approval == false" class="badge badge-danger badge-pill"> {{approvalStatus(action.approval)}} </span>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="approve">
|
||||||
|
<mat-header-cell *matHeaderCellDef> APPROVE </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let action">
|
||||||
|
<button mat-raised-button color="primary" *ngIf="!action.approval" class="btn btn-outline-success" (click)="approveAction(action)"> Approve </button>
|
||||||
|
<button mat-raised-button color="warn" *ngIf="action.approval" class="btn btn-outline-danger" (click)="revertAction(action)"> Revert </button>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Expanded Content Column - The detail row is made up of this one column -->
|
||||||
|
<ng-container matColumnDef="expandedDetail">
|
||||||
|
<mat-cell *matCellDef="let action">
|
||||||
|
<div>
|
||||||
|
<span><strong>Staff Name:</strong> {{action.user}}</span><br>
|
||||||
|
<span><strong>Role:</strong> {{action.role}}</span><br>
|
||||||
|
<span><strong>Action Details:</strong> {{action.action}}</span><br>
|
||||||
|
<span><strong>Approval Status:</strong> {{action.approval}}</span><br>
|
||||||
|
</div>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||||
|
<mat-row *matRowDef="let row; columns: displayedColumns;" matRipple class="element-row"
|
||||||
|
[class.expanded]="row.isExpanded"></mat-row>
|
||||||
|
<mat-row *matRowDef="let row; columns: ['expandedDetail'];"
|
||||||
|
[@detailExpand]="row.isExpanded == true ? 'expanded': 'collapsed'" style="overflow: hidden"></mat-row>
|
||||||
|
</mat-table>
|
||||||
|
|
||||||
|
<mat-paginator [pageSize]="10" [pageSizeOptions]="[10, 20, 50, 100]" showFirstLastButtons></mat-paginator>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-footer appMenuSelection></app-footer>
|
||||||
|
</div>
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- End Page content -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
</div>
|
||||||
|
|
3
src/app/pages/admin/admin.component.scss
Normal file
3
src/app/pages/admin/admin.component.scss
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
button {
|
||||||
|
width: 6rem;
|
||||||
|
}
|
61
src/app/pages/admin/admin.component.spec.ts
Normal file
61
src/app/pages/admin/admin.component.spec.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AdminComponent } from '@pages/admin/admin.component';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
|
||||||
|
import {AdminModule} from '@pages/admin/admin.module';
|
||||||
|
import {FooterStubComponent, SidebarStubComponent, TopbarStubComponent, UserServiceStub} from '@src/testing';
|
||||||
|
import {AppModule} from '@app/app.module';
|
||||||
|
import {UserService} from '@app/_services';
|
||||||
|
|
||||||
|
describe('AdminComponent', () => {
|
||||||
|
let component: AdminComponent;
|
||||||
|
let fixture: ComponentFixture<AdminComponent>;
|
||||||
|
let httpClient: HttpClient;
|
||||||
|
let httpTestingController: HttpTestingController;
|
||||||
|
let userService: UserServiceStub;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [
|
||||||
|
AdminComponent,
|
||||||
|
FooterStubComponent,
|
||||||
|
SidebarStubComponent,
|
||||||
|
TopbarStubComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
AdminModule,
|
||||||
|
AppModule,
|
||||||
|
HttpClientTestingModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: UserService, useClass: UserServiceStub }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
httpClient = TestBed.inject(HttpClient);
|
||||||
|
httpTestingController = TestBed.inject(HttpTestingController);
|
||||||
|
userService = new UserServiceStub();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AdminComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('#approveAction should toggle approval status', () => {
|
||||||
|
const action = userService.getActionById('1');
|
||||||
|
expect(action).toBe({
|
||||||
|
id: 1,
|
||||||
|
user: 'Tom',
|
||||||
|
role: 'enroller',
|
||||||
|
action: 'Disburse RSV 100',
|
||||||
|
approval: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
65
src/app/pages/admin/admin.component.ts
Normal file
65
src/app/pages/admin/admin.component.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import {ChangeDetectionStrategy, Component, OnInit, ViewChild} from '@angular/core';
|
||||||
|
import {MatTableDataSource} from '@angular/material/table';
|
||||||
|
import {MatPaginator} from '@angular/material/paginator';
|
||||||
|
import {MatSort} from '@angular/material/sort';
|
||||||
|
import {UserService} from '@app/_services';
|
||||||
|
import {animate, state, style, transition, trigger} from '@angular/animations';
|
||||||
|
import {first} from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-admin',
|
||||||
|
templateUrl: './admin.component.html',
|
||||||
|
styleUrls: ['./admin.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
animations: [
|
||||||
|
trigger('detailExpand', [
|
||||||
|
state('collapsed', style({height: '0px', minHeight: 0, visibility: 'hidden'})),
|
||||||
|
state('expanded', style({height: '*', visibility: 'visible'})),
|
||||||
|
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
|
||||||
|
])
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AdminComponent implements OnInit {
|
||||||
|
dataSource: MatTableDataSource<any>;
|
||||||
|
displayedColumns = ['expand', 'user', 'role', 'action', 'status', 'approve'];
|
||||||
|
action: any;
|
||||||
|
|
||||||
|
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||||
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private userService: UserService
|
||||||
|
) {
|
||||||
|
this.userService.getActions();
|
||||||
|
this.userService.actionsSubject.subscribe(actions => {
|
||||||
|
this.dataSource = new MatTableDataSource<any>(actions);
|
||||||
|
this.dataSource.paginator = this.paginator;
|
||||||
|
this.dataSource.sort = this.sort;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
doFilter(value: string): void {
|
||||||
|
this.dataSource.filter = value.trim().toLocaleLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalStatus(status: boolean): string {
|
||||||
|
return status ? 'Approved' : 'Unapproved';
|
||||||
|
}
|
||||||
|
|
||||||
|
approveAction(action: any): void {
|
||||||
|
this.userService.approveAction(action.id).pipe(first()).subscribe(res => console.log(res.body));
|
||||||
|
this.userService.getActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
revertAction(action: any): void {
|
||||||
|
this.userService.revokeAction(action.id).pipe(first()).subscribe(res => console.log(res.body));
|
||||||
|
this.userService.getActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
expandCollapse(row): void {
|
||||||
|
row.isExpanded = !row.isExpanded;
|
||||||
|
}
|
||||||
|
}
|
35
src/app/pages/admin/admin.module.ts
Normal file
35
src/app/pages/admin/admin.module.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
import { AdminRoutingModule } from '@pages/admin/admin-routing.module';
|
||||||
|
import { AdminComponent } from '@pages/admin/admin.component';
|
||||||
|
import {SharedModule} from '@app/shared/shared.module';
|
||||||
|
import {MatCardModule} from '@angular/material/card';
|
||||||
|
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||||
|
import {MatInputModule} from '@angular/material/input';
|
||||||
|
import {MatIconModule} from '@angular/material/icon';
|
||||||
|
import {MatTableModule} from '@angular/material/table';
|
||||||
|
import {MatSortModule} from '@angular/material/sort';
|
||||||
|
import {MatPaginatorModule} from '@angular/material/paginator';
|
||||||
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
|
import {MatRippleModule} from '@angular/material/core';
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AdminComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
AdminRoutingModule,
|
||||||
|
SharedModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatTableModule,
|
||||||
|
MatSortModule,
|
||||||
|
MatPaginatorModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatRippleModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AdminModule { }
|
20
src/app/pages/pages-routing.module.ts
Normal file
20
src/app/pages/pages-routing.module.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { PagesComponent } from './pages.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{ path: 'home', component: PagesComponent },
|
||||||
|
{ path: 'tx', loadChildren: () => import('@pages/transactions/transactions.module').then(m => m.TransactionsModule) },
|
||||||
|
{ path: 'settings', loadChildren: () => import('@pages/settings/settings.module').then(m => m.SettingsModule) },
|
||||||
|
{ path: 'accounts', loadChildren: () => import('@pages/accounts/accounts.module').then(m => m.AccountsModule) },
|
||||||
|
{ path: 'tokens', loadChildren: () => import('@pages/tokens/tokens.module').then(m => m.TokensModule) },
|
||||||
|
{ path: 'admin', loadChildren: () => import('@pages/admin/admin.module').then(m => m.AdminModule) },
|
||||||
|
{ path: '**', redirectTo: 'home', pathMatch: 'full'}
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class PagesRoutingModule { }
|
116
src/app/pages/pages.component.html
Normal file
116
src/app/pages/pages.component.html
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<!-- Begin page -->
|
||||||
|
<div class="wrapper">
|
||||||
|
<app-sidebar></app-sidebar>
|
||||||
|
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- Start Page Content here -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
<app-topbar></app-topbar>
|
||||||
|
<!-- Start Content-->
|
||||||
|
<div class="container-fluid" appMenuSelection>
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Home</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<div class="card">
|
||||||
|
<mat-card-title class="card-header">
|
||||||
|
CICADA DASHBOARD
|
||||||
|
</mat-card-title>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card-body">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Filter by location : </mat-label>
|
||||||
|
<mat-select class="ml-2" id="filterUser">
|
||||||
|
<div *ngFor="let county of locations; trackBy: trackByName">
|
||||||
|
<div *ngFor="let district of county.districts; trackBy: trackByName">
|
||||||
|
<mat-optgroup *ngFor="let location of district.locations; trackBy: trackByName" [label]="county.name + ' / ' + district.name + ' / ' + location.name">
|
||||||
|
<mat-option *ngFor="let village of location.villages; trackBy: trackByName" [value]="village">
|
||||||
|
{{village}}
|
||||||
|
</mat-option>
|
||||||
|
</mat-optgroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="card col-md-8">
|
||||||
|
<div class="card-body">
|
||||||
|
<div style="display: block">
|
||||||
|
<canvas baseChart
|
||||||
|
[datasets]="lineChartData"
|
||||||
|
[labels]="lineChartLabels"
|
||||||
|
[options]="lineChartOptions"
|
||||||
|
[colors]="lineChartColors"
|
||||||
|
[legend]="lineChartLegend"
|
||||||
|
[chartType]="lineChartType"
|
||||||
|
(chartHover)="chartHovered($event)"
|
||||||
|
(chartClick)="chartClicked($event)">
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="card-body row">
|
||||||
|
<div class="col-md-3 text-center">
|
||||||
|
<h4>MASTER WALLET BALANCE</h4>
|
||||||
|
<p>{{10000000 | number}} RCU</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 text-center">
|
||||||
|
<h4>TOTAL DISTRIBUTED</h4>
|
||||||
|
<p>{{disbursements * 1000 | number}} RCU</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 text-center">
|
||||||
|
<h4>TOTAL SPENT</h4>
|
||||||
|
<p>{{transactions * 100000 | number}} RCU</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 text-center">
|
||||||
|
<h4>TOTAL USERS</h4>
|
||||||
|
<p>{{users * 10 | number}} users</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="card-body">
|
||||||
|
<div style="display: block">
|
||||||
|
<canvas baseChart
|
||||||
|
[datasets]="barChartData"
|
||||||
|
[labels]="barChartLabels"
|
||||||
|
[options]="barChartOptions"
|
||||||
|
[legend]="barChartLegend"
|
||||||
|
[chartType]="barChartType">
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card col-md-4">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h4>TRANSFER USAGES</h4>
|
||||||
|
<div style="display: block">
|
||||||
|
<canvas baseChart
|
||||||
|
[data]="transferUsagesChartData"
|
||||||
|
[labels]="transferUsagesChartLabels"
|
||||||
|
[options]="transferUsagesChartOptions"
|
||||||
|
[colors]="transferUsagesChartColors"
|
||||||
|
[legend]="transferUsagesChartLegend"
|
||||||
|
[chartType]="transferUsagesChartType">
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="text-center">PARTNER LIVE FEED</h4>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-footer appMenuSelection></app-footer>
|
||||||
|
</div>
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- End Page content -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
</div>
|
0
src/app/pages/pages.component.scss
Normal file
0
src/app/pages/pages.component.scss
Normal file
25
src/app/pages/pages.component.spec.ts
Normal file
25
src/app/pages/pages.component.spec.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { PagesComponent } from '@pages/pages.component';
|
||||||
|
|
||||||
|
describe('PagesComponent', () => {
|
||||||
|
let component: PagesComponent;
|
||||||
|
let fixture: ComponentFixture<PagesComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ PagesComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(PagesComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
190
src/app/pages/pages.component.ts
Normal file
190
src/app/pages/pages.component.ts
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core';
|
||||||
|
import {Color, Label} from 'ng2-charts';
|
||||||
|
import {ChartDataSets, ChartOptions, ChartType} from 'chart.js';
|
||||||
|
import {LocationService, UserService} from '@app/_services';
|
||||||
|
import {ArraySum} from '@app/_helpers';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-pages',
|
||||||
|
templateUrl: './pages.component.html',
|
||||||
|
styleUrls: ['./pages.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class PagesComponent implements OnInit {
|
||||||
|
disbursements: number = 0;
|
||||||
|
users: any;
|
||||||
|
locations: any;
|
||||||
|
transactions: number = 0;
|
||||||
|
public lineChartData: ChartDataSets[] = [
|
||||||
|
{ data: [65, 59, 80, 81, 56, 55, 40], label: 'User Registration'},
|
||||||
|
{ data: [28, 48, 40, 19, 86, 27, 90], label: 'Transaction Volumes'},
|
||||||
|
{ data: [180, 480, 770, 90, 1000, 270, 400], label: 'Token Disbursements', yAxisID: 'y-axis-1'}
|
||||||
|
];
|
||||||
|
|
||||||
|
public lineChartLabels: Label[] = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
|
||||||
|
|
||||||
|
public lineChartOptions: (ChartOptions & { annotation: any }) = {
|
||||||
|
responsive: true,
|
||||||
|
scales: {
|
||||||
|
// We use this empty structure as a placeholder for dynamic theming.
|
||||||
|
xAxes: [{}],
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
id: 'y-axis-0',
|
||||||
|
position: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'y-axis-1',
|
||||||
|
position: 'right',
|
||||||
|
gridLines: {
|
||||||
|
color: 'rgba(255,0,0,0.3)',
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
fontColor: 'red',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
annotation: {
|
||||||
|
annotations: [
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
mode: 'vertical',
|
||||||
|
scaleID: 'x-axis-0',
|
||||||
|
value: 'March',
|
||||||
|
borderColor: 'orange',
|
||||||
|
borderWidth: 2,
|
||||||
|
label: {
|
||||||
|
enabled: true,
|
||||||
|
fontColor: 'orange',
|
||||||
|
content: 'LineAnno'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
public lineChartColors: Color[] = [
|
||||||
|
{ // grey
|
||||||
|
backgroundColor: 'rgba(148,159,177,0.2)',
|
||||||
|
borderColor: 'rgba(148,159,177,1)',
|
||||||
|
pointBackgroundColor: 'rgba(148,159,177,1)',
|
||||||
|
pointBorderColor: '#fff',
|
||||||
|
pointHoverBackgroundColor: '#fff',
|
||||||
|
pointHoverBorderColor: 'rgba(148,159,177,0.8)'
|
||||||
|
},
|
||||||
|
{ // dark grey
|
||||||
|
backgroundColor: 'rgba(77,83,96,0.2)',
|
||||||
|
borderColor: 'rgba(77,83,96,1)',
|
||||||
|
pointBackgroundColor: 'rgba(77,83,96,1)',
|
||||||
|
pointBorderColor: '#fff',
|
||||||
|
pointHoverBackgroundColor: '#fff',
|
||||||
|
pointHoverBorderColor: 'rgba(77,83,96,1)'
|
||||||
|
},
|
||||||
|
{ // red
|
||||||
|
backgroundColor: 'rgba(255,0,0,0.3)',
|
||||||
|
borderColor: 'red',
|
||||||
|
pointBackgroundColor: 'rgba(148,159,177,1)',
|
||||||
|
pointBorderColor: '#fff',
|
||||||
|
pointHoverBackgroundColor: '#fff',
|
||||||
|
pointHoverBorderColor: 'rgba(148,159,177,0.8)'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
public lineChartLegend = true;
|
||||||
|
public lineChartType = 'line';
|
||||||
|
|
||||||
|
public barChartOptions: ChartOptions = {
|
||||||
|
responsive: true,
|
||||||
|
// We use these empty structures as placeholders for dynamic theming.
|
||||||
|
scales: { xAxes: [{}], yAxes: [{}] },
|
||||||
|
plugins: {
|
||||||
|
datalabels: {
|
||||||
|
anchor: 'end',
|
||||||
|
align: 'end',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
public barChartLabels: Label[] = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
|
||||||
|
public barChartType: ChartType = 'horizontalBar';
|
||||||
|
public barChartLegend = true;
|
||||||
|
public barChartData: ChartDataSets[] = [
|
||||||
|
{ data: [65, 59, 80, 81, 56, 55, 40], label: 'New Users'},
|
||||||
|
{ data: [28, 48, 40, 19, 86, 27, 90], label: 'Recurrent Users'}
|
||||||
|
];
|
||||||
|
|
||||||
|
public transferUsagesChartLabels: Label[] = ['Food/Water', 'Fuel/Energy', 'Education', 'Health', 'Shop', 'Environment', 'Transport',
|
||||||
|
'Farming/Labour', 'Savings Group', 'Savings Group'];
|
||||||
|
public transferUsagesChartData: number[] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||||
|
public transferUsagesChartType: ChartType = 'pie';
|
||||||
|
public transferUsagesChartOptions: ChartOptions = {
|
||||||
|
responsive: true,
|
||||||
|
legend: {
|
||||||
|
position: 'top',
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
datalabels: {
|
||||||
|
formatter: (value, ctx) => {
|
||||||
|
const label = ctx.chart.data.labels[ctx.dataIndex];
|
||||||
|
return label;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
public transferUsagesChartLegend = true;
|
||||||
|
public transferUsagesChartColors = [
|
||||||
|
{
|
||||||
|
backgroundColor: [
|
||||||
|
'rgba(0,0,255,0.3)',
|
||||||
|
'rgba(255,0,0,0.3)',
|
||||||
|
'rgba(0,255,0,0.3)',
|
||||||
|
'rgba(255,0,255,0.3)',
|
||||||
|
'rgba(255,255,0,0.3)',
|
||||||
|
'rgba(0,255,255,0.3)',
|
||||||
|
'rgba(255,255,255,0.3)',
|
||||||
|
'rgba(255,100,0,0.3)',
|
||||||
|
'rgba(0,255,100,0.3)',
|
||||||
|
'rgba(100,0,255,0.3)'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private userService: UserService,
|
||||||
|
private locationService: LocationService
|
||||||
|
) {
|
||||||
|
this.locationService.getLocations();
|
||||||
|
this.locationService.locationsSubject.subscribe(locations => {
|
||||||
|
this.locations = locations;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.newDataPoint([50, 80], 'August');
|
||||||
|
this.transferUsagesChartData = [18, 12, 10, 8, 6, 5, 5, 3, 2, 1];
|
||||||
|
this.disbursements = ArraySum.arraySum(this.lineChartData.find(data => data.label === 'Token Disbursements').data);
|
||||||
|
this.users = ArraySum.arraySum(this.barChartData.find(data => data.label === 'New Users').data);
|
||||||
|
this.transactions = ArraySum.arraySum(this.lineChartData.find(data => data.label === 'Transaction Volumes').data);
|
||||||
|
}
|
||||||
|
|
||||||
|
newDataPoint(dataArr: any[], label: string): void {
|
||||||
|
this.barChartData.forEach((dataset, index) => {
|
||||||
|
this.barChartData[index] = Object.assign({}, this.barChartData[index], {
|
||||||
|
data: [...this.barChartData[index].data, dataArr[index]]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.barChartLabels = [...this.barChartLabels, label];
|
||||||
|
}
|
||||||
|
|
||||||
|
public chartClicked({ event, active}: { event: MouseEvent, active: {}[] }): void {
|
||||||
|
console.log(event, active);
|
||||||
|
}
|
||||||
|
|
||||||
|
public chartHovered({ event, active }: { event: MouseEvent, active: {}[] }): void {
|
||||||
|
console.log(event, active);
|
||||||
|
}
|
||||||
|
|
||||||
|
public trackByName(index, item): string {
|
||||||
|
return item.name;
|
||||||
|
}
|
||||||
|
}
|
29
src/app/pages/pages.module.ts
Normal file
29
src/app/pages/pages.module.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
import { PagesRoutingModule } from '@pages/pages-routing.module';
|
||||||
|
import { PagesComponent } from '@pages/pages.component';
|
||||||
|
import {SharedModule} from '@app/shared/shared.module';
|
||||||
|
import {ChartsModule} from 'ng2-charts';
|
||||||
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
|
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||||
|
import {MatSelectModule} from '@angular/material/select';
|
||||||
|
import {MatInputModule} from '@angular/material/input';
|
||||||
|
import {MatCardModule} from '@angular/material/card';
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [PagesComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
PagesRoutingModule,
|
||||||
|
SharedModule,
|
||||||
|
ChartsModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatCardModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class PagesModule { }
|
51
src/app/pages/settings/invite/invite.component.html
Normal file
51
src/app/pages/settings/invite/invite.component.html
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<!-- Begin page -->
|
||||||
|
<div class="wrapper">
|
||||||
|
<app-sidebar></app-sidebar>
|
||||||
|
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- Start Page Content here -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
<app-topbar></app-topbar>
|
||||||
|
<!-- Start Content-->
|
||||||
|
<div class="container-fluid" appMenuSelection>
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a routerLink="/home">Home</a></li>
|
||||||
|
<li class="breadcrumb-item"><a routerLink="/settings">Settings</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Invite User</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<mat-card-title class="card-header text-center">
|
||||||
|
INVITE NEW USERS
|
||||||
|
</mat-card-title>
|
||||||
|
<div class="card-body">
|
||||||
|
<form [formGroup]="inviteForm" (ngSubmit)="invite()">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Email Address: </mat-label>
|
||||||
|
<input matInput type="email" id="email" placeholder="Email" formControlName="email"
|
||||||
|
[errorStateMatcher]="matcher">
|
||||||
|
<mat-error *ngIf="submitted && inviteFormStub.email.errors">Email is required.</mat-error>
|
||||||
|
</mat-form-field><br>
|
||||||
|
<mat-radio-group aria-label="Select a role" formControlName="role">
|
||||||
|
<mat-radio-button value="Superadmin">Superadmin</mat-radio-button><br>
|
||||||
|
<mat-radio-button value="Admin">Admin</mat-radio-button><br>
|
||||||
|
<mat-radio-button value="Subadmin">Subadmin</mat-radio-button><br>
|
||||||
|
<mat-radio-button value="View">View</mat-radio-button><br>
|
||||||
|
</mat-radio-group>
|
||||||
|
<mat-error *ngIf="submitted && inviteFormStub.role.errors">Role is required.</mat-error>
|
||||||
|
<button mat-raised-button color="primary" type="submit" class="btn btn-outline-primary">INVITE</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-footer appMenuSelection></app-footer>
|
||||||
|
</div>
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- End Page content -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
</div>
|
0
src/app/pages/settings/invite/invite.component.scss
Normal file
0
src/app/pages/settings/invite/invite.component.scss
Normal file
37
src/app/pages/settings/invite/invite.component.spec.ts
Normal file
37
src/app/pages/settings/invite/invite.component.spec.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { InviteComponent } from '@pages/settings/invite/invite.component';
|
||||||
|
import {FooterStubComponent, SidebarStubComponent, TopbarStubComponent} from '@src/testing';
|
||||||
|
import {SettingsModule} from '@pages/settings/settings.module';
|
||||||
|
import {AppModule} from '@app/app.module';
|
||||||
|
|
||||||
|
describe('InviteComponent', () => {
|
||||||
|
let component: InviteComponent;
|
||||||
|
let fixture: ComponentFixture<InviteComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [
|
||||||
|
InviteComponent,
|
||||||
|
FooterStubComponent,
|
||||||
|
SidebarStubComponent,
|
||||||
|
TopbarStubComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
AppModule,
|
||||||
|
SettingsModule,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(InviteComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
34
src/app/pages/settings/invite/invite.component.ts
Normal file
34
src/app/pages/settings/invite/invite.component.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core';
|
||||||
|
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
|
||||||
|
import {CustomErrorStateMatcher} from '@app/_helpers';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-invite',
|
||||||
|
templateUrl: './invite.component.html',
|
||||||
|
styleUrls: ['./invite.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class InviteComponent implements OnInit {
|
||||||
|
inviteForm: FormGroup;
|
||||||
|
submitted: boolean = false;
|
||||||
|
matcher = new CustomErrorStateMatcher();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private formBuilder: FormBuilder
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.inviteForm = this.formBuilder.group({
|
||||||
|
email: ['', Validators.required],
|
||||||
|
role: ['', Validators.required]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get inviteFormStub(): any { return this.inviteForm.controls; }
|
||||||
|
|
||||||
|
invite(): void {
|
||||||
|
this.submitted = true;
|
||||||
|
if (this.inviteForm.invalid) { return; }
|
||||||
|
this.submitted = false;
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user