Merge branch 'master' into spencer/pwa
# Conflicts: # package-lock.json # src/app/shared/shared.module.ts # src/styles.scss
This commit is contained in:
commit
d4d2837d2e
@ -27,7 +27,7 @@ ng_build:
|
||||
IMAGE_TAG_BASE: $CI_REGISTRY_IMAGE:$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA
|
||||
LATEST_TAG: $CI_REGISTRY_IMAGE:latest
|
||||
script:
|
||||
- export IMAGE_TAG="$IMAGE_TAG_BASE-$(date +%F.%H%M%S)"
|
||||
- export IMAGE_TAG="$IMAGE_TAG_BASE-$(date +%s)"
|
||||
- mkdir -p /kaniko/.docker
|
||||
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > "/kaniko/.docker/config.json"
|
||||
- /kaniko/executor --context . $KANIKO_CACHE_ARGS --destination $IMAGE_TAG --destination $CI_REGISTRY_IMAGE:latest
|
||||
|
@ -22,6 +22,9 @@ Run `ng generate module module-name --route module-name --module app.module` to
|
||||
|
||||
## Build
|
||||
|
||||
set you environment variables - set these via environment variables as found in set-env.ts
|
||||
// TODO create a .env file so people don't have to set these one-by-one
|
||||
|
||||
Run `npm run build:dev` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `build:prod` script for a production build.
|
||||
|
||||
## Running unit tests
|
||||
|
21220
package-lock.json
generated
21220
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -32,8 +32,9 @@
|
||||
"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",
|
||||
"cic-client": "0.1.4",
|
||||
"cic-client-meta": "0.0.7-alpha.6",
|
||||
"cic-schemas-data-validator": "^1.0.0-alpha.3",
|
||||
"datatables.net": "^1.10.22",
|
||||
"datatables.net-dt": "^1.10.22",
|
||||
"ethers": "^5.0.31",
|
||||
|
@ -1,10 +1,8 @@
|
||||
// @ts-ignore
|
||||
import * as accountIndex from '@src/assets/js/block-sync/data/AccountRegistry.json';
|
||||
import {environment} from '@src/environments/environment';
|
||||
const Web3 = require('web3');
|
||||
import Web3 from 'web3';
|
||||
|
||||
const web3 = new Web3(environment.web3Provider);
|
||||
const abi = accountIndex.default;
|
||||
const abi: Array<any> = require('@src/assets/js/block-sync/data/AccountRegistry.json');
|
||||
const web3: Web3 = new Web3(environment.web3Provider);
|
||||
|
||||
export class AccountIndex {
|
||||
contractAddress: string;
|
||||
@ -30,14 +28,14 @@ export class AccountIndex {
|
||||
}
|
||||
|
||||
public async last(numberOfAccounts: number): Promise<Array<string>> {
|
||||
const count = await this.totalAccounts();
|
||||
let lowest = count - numberOfAccounts - 1;
|
||||
const count: number = await this.totalAccounts();
|
||||
let lowest: number = count - numberOfAccounts - 1;
|
||||
if (lowest < 0) {
|
||||
lowest = 0;
|
||||
}
|
||||
let accounts = [];
|
||||
const accounts: Array<string> = [];
|
||||
for (let i = count - 1; i > lowest; i--) {
|
||||
const account = await this.contract.methods.accounts(i).call();
|
||||
const account: string = await this.contract.methods.accounts(i).call();
|
||||
accounts.push(account);
|
||||
}
|
||||
return accounts;
|
||||
@ -46,8 +44,7 @@ export class AccountIndex {
|
||||
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);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,2 @@
|
||||
export * from '@app/_eth/accountIndex';
|
||||
export * from '@app/_eth/registry';
|
||||
export * from '@app/_eth/token-registry';
|
||||
|
@ -1,8 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
@ -1,32 +0,0 @@
|
||||
// @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();
|
||||
}
|
||||
}
|
@ -1,10 +1,8 @@
|
||||
// @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;
|
||||
const abi: Array<any> = require('@src/assets/js/block-sync/data/TokenUniqueSymbolIndex.json');
|
||||
const web3: Web3 = new Web3(environment.web3Provider);
|
||||
|
||||
export class TokenRegistry {
|
||||
contractAddress: string;
|
||||
@ -22,7 +20,7 @@ export class TokenRegistry {
|
||||
}
|
||||
|
||||
public async totalTokens(): Promise<number> {
|
||||
return await this.contract.methods.registryCount().call();
|
||||
return await this.contract.methods.entryCount().call();
|
||||
}
|
||||
|
||||
public async entry(serial: number): Promise<string> {
|
||||
@ -30,7 +28,7 @@ export class TokenRegistry {
|
||||
}
|
||||
|
||||
public async addressOf(identifier: string): Promise<string> {
|
||||
const id = '0x' + web3.utils.padRight(new Buffer(identifier).toString('hex'), 64);
|
||||
const id: string = web3.eth.abi.encodeParameter('bytes32', web3.utils.toHex(identifier));
|
||||
return await this.contract.methods.addressOf(id).call();
|
||||
}
|
||||
}
|
||||
|
@ -12,10 +12,9 @@ export class AuthGuard implements CanActivate {
|
||||
canActivate(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
||||
if (sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'))) {
|
||||
if (localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.router.navigate(['/auth']);
|
||||
return false;
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
export class ArraySum {
|
||||
static arraySum(arr: any[]): number {
|
||||
function arraySum(arr: Array<number>): number {
|
||||
return arr.reduce((accumulator, current) => accumulator + current, 0);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
arraySum
|
||||
};
|
||||
|
53
src/app/_helpers/clipboard-copy.ts
Normal file
53
src/app/_helpers/clipboard-copy.ts
Normal file
@ -0,0 +1,53 @@
|
||||
function copyToClipboard(text: any): boolean {
|
||||
// create our hidden div element
|
||||
const hiddenCopy: HTMLDivElement = document.createElement('div');
|
||||
// set the innerHTML of the div
|
||||
hiddenCopy.innerHTML = text;
|
||||
// set the position to be absolute and off the screen
|
||||
hiddenCopy.classList.add('clipboard');
|
||||
|
||||
// check and see if the user had a text selection range
|
||||
let currentRange: Range | boolean;
|
||||
if (document.getSelection().rangeCount > 0) {
|
||||
// the user has a text selection range, store it
|
||||
currentRange = document.getSelection().getRangeAt(0);
|
||||
// remove the current selection
|
||||
window.getSelection().removeRange(currentRange);
|
||||
} else {
|
||||
// they didn't have anything selected
|
||||
currentRange = false;
|
||||
}
|
||||
|
||||
// append the div to the body
|
||||
document.body.appendChild(hiddenCopy);
|
||||
// create a selection range
|
||||
const copyRange: Range = document.createRange();
|
||||
// set the copy range to be the hidden div
|
||||
copyRange.selectNode(hiddenCopy);
|
||||
// add the copy range
|
||||
window.getSelection().addRange(copyRange);
|
||||
|
||||
// since not all browsers support this, use a try block
|
||||
try {
|
||||
// copy the text
|
||||
document.execCommand('copy');
|
||||
} catch (err) {
|
||||
window.alert('Your Browser Doesn\'t support this! Error : ' + err);
|
||||
return false;
|
||||
}
|
||||
// remove the selection range (Chrome throws a warning if we don't.)
|
||||
window.getSelection().removeRange(copyRange);
|
||||
// remove the hidden div
|
||||
document.body.removeChild(hiddenCopy);
|
||||
|
||||
// return the old selection range
|
||||
if (currentRange) {
|
||||
window.getSelection().addRange(currentRange);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export {
|
||||
copyToClipboard
|
||||
};
|
@ -3,7 +3,7 @@ import {FormControl, FormGroupDirective, NgForm} from '@angular/forms';
|
||||
|
||||
export class CustomErrorStateMatcher implements ErrorStateMatcher{
|
||||
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
|
||||
const isSubmitted = form && form.submitted;
|
||||
const isSubmitted: boolean = form && form.submitted;
|
||||
return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ export class CustomValidator {
|
||||
return null;
|
||||
}
|
||||
|
||||
const valid = regex.test(control.value);
|
||||
const valid: boolean = regex.test(control.value);
|
||||
return valid ? null : error;
|
||||
};
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
function exportCsv(arrayData: any[], filename: string, delimiter = ','): void {
|
||||
if (arrayData === undefined) { return; }
|
||||
const header = Object.keys(arrayData[0]).join(delimiter) + '\n';
|
||||
let csv = header;
|
||||
function exportCsv(arrayData: Array<any>, filename: string, delimiter: string = ','): void {
|
||||
if (arrayData === undefined || arrayData.length === 0) {
|
||||
alert('No data to be exported!');
|
||||
return;
|
||||
}
|
||||
let csv: string = Object.keys(arrayData[0]).join(delimiter) + '\n';
|
||||
arrayData.forEach(obj => {
|
||||
let row = [];
|
||||
const row: Array<any> = [];
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
row.push(obj[key]);
|
||||
@ -12,11 +14,10 @@ function exportCsv(arrayData: any[], filename: string, delimiter = ','): void {
|
||||
csv += row.join(delimiter) + '\n';
|
||||
});
|
||||
|
||||
const csvData = new Blob([csv], {type: 'text/csv'});
|
||||
const csvUrl = URL.createObjectURL(csvData);
|
||||
// csvUrl = 'data:text/csv;charset=utf-8,' + encodeURI(csv);
|
||||
const csvData: Blob = new Blob([csv], {type: 'text/csv'});
|
||||
const csvUrl: string = URL.createObjectURL(csvData);
|
||||
|
||||
let downloadLink = document.createElement('a');
|
||||
const downloadLink: HTMLAnchorElement = document.createElement('a');
|
||||
downloadLink.href = csvUrl;
|
||||
downloadLink.target = '_blank';
|
||||
downloadLink.download = filename + '.csv';
|
||||
|
@ -3,9 +3,19 @@ import {LoggingService} from '@app/_services/logging.service';
|
||||
import {HttpErrorResponse} from '@angular/common/http';
|
||||
import {Router} from '@angular/router';
|
||||
|
||||
// A generalized http response error
|
||||
export class HttpError extends Error {
|
||||
public status: number;
|
||||
constructor(message: string, status: number) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.name = 'HttpError';
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class GlobalErrorHandler extends ErrorHandler {
|
||||
private sentencesForWarningLogging: string[] = [];
|
||||
private sentencesForWarningLogging: Array<string> = [];
|
||||
|
||||
constructor(
|
||||
private loggingService: LoggingService,
|
||||
@ -14,17 +24,17 @@ export class GlobalErrorHandler extends ErrorHandler {
|
||||
super();
|
||||
}
|
||||
|
||||
handleError(error: any): void {
|
||||
handleError(error: Error): void {
|
||||
this.logError(error);
|
||||
const message = error.message ? error.message : error.toString();
|
||||
const message: string = error.message ? error.message : error.toString();
|
||||
|
||||
if (error.status) {
|
||||
error = new Error(message);
|
||||
}
|
||||
// if (error.status) {
|
||||
// error = new Error(message);
|
||||
// }
|
||||
|
||||
const errorTraceString = `Error message:\n${message}.\nStack trace: ${error.stack}`;
|
||||
const errorTraceString: string = `Error message:\n${message}.\nStack trace: ${error.stack}`;
|
||||
|
||||
const isWarning = this.isWarning(errorTraceString);
|
||||
const isWarning: boolean = this.isWarning(errorTraceString);
|
||||
if (isWarning) {
|
||||
this.loggingService.sendWarnLevelMessage(errorTraceString, {error});
|
||||
} else {
|
||||
@ -35,7 +45,7 @@ export class GlobalErrorHandler extends ErrorHandler {
|
||||
}
|
||||
|
||||
logError(error: any): void {
|
||||
const route = this.router.url;
|
||||
const route: string = this.router.url;
|
||||
if (error instanceof HttpErrorResponse) {
|
||||
this.loggingService.sendErrorLevelMessage(
|
||||
`There was an HTTP error on route ${route}.\n${error.message}.\nStatus code: ${(error as HttpErrorResponse).status}`,
|
||||
@ -50,12 +60,12 @@ export class GlobalErrorHandler extends ErrorHandler {
|
||||
}
|
||||
|
||||
private isWarning(errorTraceString: string): boolean {
|
||||
let isWarning = true;
|
||||
let isWarning: boolean = true;
|
||||
if (errorTraceString.includes('/src/app/')) {
|
||||
isWarning = false;
|
||||
}
|
||||
|
||||
this.sentencesForWarningLogging.forEach((whiteListSentence) => {
|
||||
this.sentencesForWarningLogging.forEach((whiteListSentence: string) => {
|
||||
if (errorTraceString.includes(whiteListSentence)) {
|
||||
isWarning = true;
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
function HttpGetter(): void {}
|
||||
|
||||
HttpGetter.prototype.get = filename => new Promise((whohoo, doh) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
HttpGetter.prototype.get = filename => new Promise((resolve, reject) => {
|
||||
const xhr: XMLHttpRequest = new XMLHttpRequest();
|
||||
xhr.addEventListener('load', (e) => {
|
||||
if (xhr.status === 200) {
|
||||
whohoo(xhr.responseText);
|
||||
resolve(xhr.responseText);
|
||||
return;
|
||||
}
|
||||
doh('failed with status ' + xhr.status + ': ' + xhr.statusText);
|
||||
reject('failed with status ' + xhr.status + ': ' + xhr.statusText);
|
||||
});
|
||||
xhr.open('GET', filename);
|
||||
xhr.send();
|
||||
|
@ -6,3 +6,5 @@ export * from '@app/_helpers/http-getter';
|
||||
export * from '@app/_helpers/global-error-handler';
|
||||
export * from '@app/_helpers/export-csv';
|
||||
export * from '@app/_helpers/read-csv';
|
||||
export * from '@app/_helpers/clipboard-copy';
|
||||
export * from '@app/_helpers/schema-validation';
|
||||
|
@ -2,16 +2,9 @@ import {HTTP_INTERCEPTORS, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest,
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Observable, of, throwError} from 'rxjs';
|
||||
import {delay, dematerialize, materialize, mergeMap} from 'rxjs/operators';
|
||||
import {Action, AreaName, AreaType, Category, Token} from '@app/_models';
|
||||
|
||||
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 = [
|
||||
const actions: Array<Action> = [
|
||||
{ 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 },
|
||||
@ -20,61 +13,228 @@ const actions = [
|
||||
{ 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']}
|
||||
]
|
||||
}
|
||||
]
|
||||
const tokens: Array<Token> = [
|
||||
{
|
||||
name: 'Giftable Reserve', symbol: 'GRZ', address: '0xa686005CE37Dce7738436256982C3903f2E4ea8E', supply: '1000000001000000000000000000',
|
||||
decimals: '18', reserves: {}
|
||||
},
|
||||
{ name: 'Nairobi',
|
||||
districts: [
|
||||
{ name: 'Dagorreti',
|
||||
locations: [
|
||||
{ name: 'Kawangware', villages: ['Congo']},
|
||||
]
|
||||
{
|
||||
name: 'Demo Token', symbol: 'DEMO', address: '0xc80D6aFF8194114c52AEcD84c9f15fd5c8abb187', supply: '99999999999999998976',
|
||||
decimals: '18', reserves: {'0xa686005CE37Dce7738436256982C3903f2E4ea8E': {weight: '1000000', balance: '99999999999999998976'}},
|
||||
reserveRatio: '1000000', owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a'
|
||||
},
|
||||
{ name: 'Ngong',
|
||||
locations: [
|
||||
{ name: 'Kibera', villages: ['Kibera', 'Lindi']},
|
||||
]
|
||||
{
|
||||
name: 'Foo Token', symbol: 'FOO', address: '0x9ceD86089f7aBB5A97B40eb0E7521e7aa308d354', supply: '1000000000000000001014',
|
||||
decimals: '18', reserves: {'0xa686005CE37Dce7738436256982C3903f2E4ea8E': {weight: '1000000', balance: '1000000000000000001014'}},
|
||||
reserveRatio: '1000000', owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a'
|
||||
},
|
||||
{ name: 'South B',
|
||||
locations: [
|
||||
{ name: 'Mukuru', villages: ['Kayaba']},
|
||||
{ name: 'South B', villages: ['South B']},
|
||||
]
|
||||
{
|
||||
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'
|
||||
}
|
||||
];
|
||||
|
||||
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 categories: Array<Category> = [
|
||||
{
|
||||
name: 'system',
|
||||
products: ['system', 'office main', 'office main phone']
|
||||
},
|
||||
{
|
||||
name: 'education',
|
||||
products: ['book', 'coach', 'teacher', 'sch', 'school', 'pry', 'education', 'student', 'mwalimu', 'maalim', 'consultant', 'consult',
|
||||
'college', 'university', 'lecturer', 'primary', 'secondary', 'daycare', 'babycare', 'baby care', 'elim', 'eimu', 'nursery',
|
||||
'red cross', 'volunteer', 'instructor', 'journalist', 'lesson', 'academy', 'headmistress', 'headteacher', 'cyber', 'researcher',
|
||||
'professor', 'demo', 'expert', 'tution', 'tuition', 'children', 'headmaster', 'educator', 'Marital counsellor', 'counsellor',
|
||||
'trainer', 'vijana', 'youth', 'intern', 'redcross', 'KRCS', 'danish', 'science', 'data', 'facilitator', 'vitabu', 'kitabu']
|
||||
},
|
||||
{
|
||||
name: 'faith',
|
||||
products: ['pastor', 'imam', 'madrasa', 'religous', 'religious', 'ustadh', 'ustadhi', 'Marital counsellor', 'counsellor', 'church',
|
||||
'kanisa', 'mksiti', 'donor']
|
||||
},
|
||||
{
|
||||
name: 'government',
|
||||
products: ['elder', 'chief', 'police', 'government', 'country', 'county', 'soldier', 'village admin', 'ward', 'leader', 'kra',
|
||||
'mailman', 'immagration', 'immigration']
|
||||
},
|
||||
{
|
||||
name: 'environment',
|
||||
products: ['conservation', 'toilet', 'choo', 'garbage', 'fagio', 'waste', 'tree', 'taka', 'scrap', 'cleaning', 'gardener', 'rubbish',
|
||||
'usafi', 'mazingira', 'miti', 'trash', 'cleaner', 'plastic', 'collection', 'seedling', 'seedlings', 'recycling']
|
||||
},
|
||||
{
|
||||
name: 'farming',
|
||||
products: ['farm', 'farmer', 'farming', 'mkulima', 'kulima', 'ukulima', 'wakulima', 'jembe', 'shamba']
|
||||
},
|
||||
{
|
||||
name: 'labour',
|
||||
products: ['artist', 'agent', 'guard', 'askari', 'accountant', 'baker', 'beadwork', 'beauty', 'business', 'barber', 'casual',
|
||||
'electrian', 'caretaker', 'car wash', 'capenter', 'construction', 'chef', 'catering', 'cobler', 'cobbler', 'carwash', 'dhobi',
|
||||
'landlord', 'design', 'carpenter', 'fundi', 'hawking', 'hawker', 'househelp', 'hsehelp', 'house help', 'help', 'housegirl', 'kushona',
|
||||
'juakali', 'jualikali', 'juacali', 'jua kali', 'shepherd', 'makuti', 'kujenga', 'kinyozi', 'kazi', 'knitting', 'kufua', 'fua',
|
||||
'hustler', 'biashara', 'labour', 'labor', 'laundry', 'repair', 'hair', 'posho', 'mill', 'mtambo', 'uvuvi', 'engineer', 'manager',
|
||||
'tailor', 'nguo', 'mason', 'mtumba', 'garage', 'mechanic', 'mjenzi', 'mfugaji', 'painter', 'receptionist', 'printing', 'programming',
|
||||
'plumb', 'charging', 'salon', 'mpishi', 'msusi', 'mgema', 'footballer', 'photocopy', 'peddler', 'staff', 'sales', 'service', 'saloon',
|
||||
'seremala', 'security', 'insurance', 'secretary', 'shoe', 'shepard', 'shephard', 'tout', 'tv', 'mvuvi', 'mawe', 'majani', 'maembe',
|
||||
'freelance', 'mjengo', 'electronics', 'photographer', 'programmer', 'electrician', 'washing', 'bricks', 'welder', 'welding',
|
||||
'working', 'worker', 'watchman', 'waiter', 'waitress', 'viatu', 'yoga', 'guitarist', 'house', 'artisan', 'musician', 'trade',
|
||||
'makonge', 'ujenzi', 'vendor', 'watchlady', 'marketing', 'beautician', 'photo', 'metal work', 'supplier', 'law firm', 'brewer']
|
||||
},
|
||||
{
|
||||
name: 'food',
|
||||
products: ['avocado', 'bhajia', 'bajia', 'mbonga', 'bofu', 'beans', 'biscuits', 'biringanya', 'banana', 'bananas', 'crisps', 'chakula',
|
||||
'coconut', 'chapati', 'cereal', 'chipo', 'chapo', 'chai', 'chips', 'cassava', 'cake', 'cereals', 'cook', 'corn', 'coffee', 'chicken',
|
||||
'dagaa', 'donut', 'dough', 'groundnuts', 'hotel', 'holel', 'hoteli', 'butcher', 'butchery', 'fruit', 'food', 'fruits', 'fish',
|
||||
'githeri', 'grocery', 'grocer', 'pojo', 'papa', 'goats', 'mabenda', 'mbenda', 'poultry', 'soda', 'peanuts', 'potatoes', 'samosa',
|
||||
'soko', 'samaki', 'tomato', 'tomatoes', 'mchele', 'matunda', 'mango', 'melon', 'mellon', 'nyanya', 'nyama', 'omena', 'umena', 'ndizi',
|
||||
'njugu', 'kamba kamba', 'khaimati', 'kaimati', 'kunde', 'kuku', 'kahawa', 'keki', 'muguka', 'miraa', 'milk', 'choma', 'maziwa',
|
||||
'mboga', 'mbog', 'busaa', 'chumvi', 'cabbages', 'mabuyu', 'machungwa', 'mbuzi', 'mnazi', 'mchicha', 'ngombe', 'ngano', 'nazi',
|
||||
'oranges', 'peanuts', 'mkate', 'bread', 'mikate', 'vitungu', 'sausages', 'maize', 'mbata', 'mchuzi', 'mchuuzi', 'mandazi', 'mbaazi',
|
||||
'mahindi', 'maandazi', 'mogoka', 'meat', 'mhogo', 'mihogo', 'muhogo', 'maharagwe', 'miwa', 'mahamri', 'mitumba', 'simsim', 'porridge',
|
||||
'pilau', 'vegetable', 'egg', 'mayai', 'mifugo', 'unga', 'good', 'sima', 'sweet', 'sweats', 'sambusa', 'snacks', 'sugar', 'suger',
|
||||
'ugoro', 'sukari', 'soup', 'spinach', 'smokie', 'smokies', 'sukuma', 'tea', 'uji', 'ugali', 'uchuzi', 'uchuuzi', 'viazi', 'yoghurt',
|
||||
'yogurt', 'wine', 'marondo', 'maandzi', 'matoke', 'omeno', 'onions', 'nzugu', 'korosho', 'barafu', 'juice']
|
||||
},
|
||||
{
|
||||
name: 'water',
|
||||
products: ['maji', 'water']
|
||||
},
|
||||
{
|
||||
name: 'health',
|
||||
products: ['agrovet', 'dispensary', 'barakoa', 'chemist', 'Chemicals', 'chv', 'doctor', 'daktari', 'dawa', 'hospital', 'herbalist',
|
||||
'mganga', 'sabuni', 'soap', 'nurse', 'heath', 'community health worker', 'clinic', 'clinical', 'mask', 'medicine', 'lab technician',
|
||||
'pharmacy', 'cosmetics', 'veterinary', 'vet', 'sickly', 'emergency response', 'emergency']
|
||||
},
|
||||
{
|
||||
name: 'savings',
|
||||
products: ['chama', 'group', 'savings', 'loan', 'silc', 'vsla', 'credit', 'finance']
|
||||
},
|
||||
{
|
||||
name: 'shop',
|
||||
products: ['bag', 'bead', 'belt', 'bedding', 'jik', 'bed', 'cement', 'botique', 'boutique', 'lines', 'kibanda', 'kiosk', 'spareparts',
|
||||
'candy', 'cloth', 'electricals', 'mutumba', 'cafe', 'leso', 'lesso', 'duka', 'spare parts', 'socks', 'malimali', 'mitungi',
|
||||
'mali mali', 'hardware', 'detergent', 'detergents', 'dera', 'retail', 'kamba', 'pombe', 'pampers', 'pool', 'phone', 'simu', 'mangwe',
|
||||
'mikeka', 'movie', 'shop', 'acces', 'mchanga', 'uto', 'airtime', 'matress', 'mattress', 'mattresses', 'mpsea', 'mpesa', 'shirt',
|
||||
'wholesaler', 'perfume', 'playstation', 'tissue', 'vikapu', 'uniform', 'flowers', 'vitenge', 'utencils', 'utensils', 'station',
|
||||
'jewel', 'pool table', 'club', 'pub', 'bar', 'furniture', 'm-pesa', 'vyombo']
|
||||
},
|
||||
{
|
||||
name: 'transport',
|
||||
products: ['kebeba', 'beba', 'bebabeba', 'bike', 'bicycle', 'matatu', 'boda', 'bodaboda', 'cart', 'carrier', 'tour', 'travel', 'driver',
|
||||
'dereva', 'tout', 'conductor', 'kubeba', 'tuktuk', 'taxi', 'piki', 'pikipiki', 'manamba', 'trasportion', 'mkokoteni', 'mover',
|
||||
'motorist', 'motorbike', 'transport', 'transpoter', 'gari', 'magari', 'makanga', 'car']
|
||||
},
|
||||
{
|
||||
name: 'fuel/energy',
|
||||
products: ['timber', 'timberyard', 'biogas', 'charcol', 'charcoal', 'kuni', 'mbao', 'fuel', 'makaa', 'mafuta', 'moto', 'solar', 'stima',
|
||||
'fire', 'firewood', 'wood', 'oil', 'taa', 'gas', 'paraffin', 'parrafin', 'parafin', 'petrol', 'petro', 'kerosine', 'kerosene',
|
||||
'diesel']
|
||||
},
|
||||
{
|
||||
name: 'other',
|
||||
products: ['other', 'none', 'unknown', 'none']
|
||||
}
|
||||
];
|
||||
|
||||
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'}
|
||||
const areaNames: Array<AreaName> = [
|
||||
{
|
||||
name: 'Mukuru Nairobi',
|
||||
locations: ['kayaba', 'kayba', 'kambi', 'mukuru', 'masai', 'hazina', 'south', 'tetra', 'tetrapak', 'ruben', 'rueben', 'kingston',
|
||||
'korokocho', 'kingstone', 'kamongo', 'lungalunga', 'sinai', 'sigei', 'lungu', 'lunga lunga', 'owino road', 'seigei']
|
||||
},
|
||||
{
|
||||
name: 'Kinango Kwale',
|
||||
locations: ['amani', 'bofu', 'chibuga', 'chikomani', 'chilongoni', 'chigojoni', 'chinguluni', 'chigato', 'chigale', 'chikole',
|
||||
'chilongoni', 'chilumani', 'chigojoni', 'chikomani', 'chizini', 'chikomeni', 'chidzuvini', 'chidzivuni', 'chikuyu', 'chizingo',
|
||||
'doti', 'dzugwe', 'dzivani', 'dzovuni', 'hanje', 'kasemeni', 'katundani', 'kibandaogo', 'kibandaongo', 'kwale', 'kinango',
|
||||
'kidzuvini', 'kalalani', 'kafuduni', 'kaloleni', 'kilibole', 'lutsangani', 'peku', 'gona', 'guro', 'gandini', 'mkanyeni', 'myenzeni',
|
||||
'miyenzeni', 'miatsiani', 'mienzeni', 'mnyenzeni', 'minyenzeni', 'miyani', 'mioleni', 'makuluni', 'mariakani', 'makobeni', 'madewani',
|
||||
'mwangaraba', 'mwashanga', 'miloeni', 'mabesheni', 'mazeras', 'mazera', 'mlola', 'muugano', 'mulunguni', 'mabesheni', 'miatsani',
|
||||
'miatsiani', 'mwache', 'mwangani', 'mwehavikonje', 'miguneni', 'nzora', 'nzovuni', 'vikinduni', 'vikolani', 'vitangani', 'viogato',
|
||||
'vyogato', 'vistangani', 'yapha', 'yava', 'yowani', 'ziwani', 'majengo', 'matuga', 'vigungani', 'vidziweni', 'vinyunduni', 'ukunda',
|
||||
'kokotoni', 'mikindani']
|
||||
},
|
||||
{
|
||||
name: 'Misc Nairobi',
|
||||
locations: ['nairobi', 'west', 'lindi', 'kibera', 'kibira', 'kibra', 'makina', 'soweto', 'olympic', 'kangemi', 'ruiru', 'congo',
|
||||
'kawangware', 'kwangware', 'donholm', 'dagoreti', 'dandora', 'kabete', 'sinai', 'donhom', 'donholm', 'huruma', 'kitengela',
|
||||
'makadara', ',mlolongo', 'kenyatta', 'mlolongo', 'tassia', 'tasia', 'gatina', '56', 'industrial', 'kariobangi', 'kasarani', 'kayole',
|
||||
'mathare', 'pipe', 'juja', 'uchumi', 'jogoo', 'umoja', 'thika', 'kikuyu', 'stadium', 'buru buru', 'ngong', 'starehe', 'mwiki',
|
||||
'fuata', 'kware', 'kabiro', 'embakassi', 'embakasi', 'kmoja', 'east', 'githurai', 'landi', 'langata', 'limuru', 'mathere',
|
||||
'dagoretti', 'kirembe', 'muugano', 'mwiki', 'toi market']
|
||||
},
|
||||
{
|
||||
name: 'Misc Mombasa',
|
||||
locations: ['mombasa', 'likoni', 'bangla', 'bangladesh', 'kizingo', 'old town', 'makupa', 'mvita', 'ngombeni', 'ngómbeni', 'ombeni',
|
||||
'magongo', 'miritini', 'changamwe', 'jomvu', 'ohuru', 'tudor', 'diani']
|
||||
},
|
||||
{
|
||||
name: 'Kisauni',
|
||||
locations: ['bamburi', 'kisauni', 'mworoni', 'nyali', 'shanzu', 'bombolulu', 'mtopanga', 'mjambere', 'majaoni', 'manyani', 'magogoni',
|
||||
'junda', 'mwakirunge', 'mshomoroni']
|
||||
},
|
||||
{
|
||||
name: 'Kilifi',
|
||||
locations: ['kilfi', 'kilifi', 'mtwapa', 'takaungu', 'makongeni', 'mnarani', 'mnarani', 'office', 'g.e', 'ge', 'raibai', 'ribe']
|
||||
},
|
||||
{
|
||||
name: 'Kakuma',
|
||||
locations: ['kakuma']
|
||||
},
|
||||
{
|
||||
name: 'Kitui',
|
||||
locations: ['kitui', 'mwingi']
|
||||
},
|
||||
{
|
||||
name: 'Nyanza',
|
||||
locations: ['busia', 'nyalgunga', 'mbita', 'siaya', 'kisumu', 'nyalenda', 'hawinga', 'rangala', 'uyoma', 'mumias', 'homabay', 'homaboy',
|
||||
'migori', 'kusumu']
|
||||
},
|
||||
{
|
||||
name: 'Misc Rural Counties',
|
||||
locations: ['makueni', 'meru', 'kisii', 'bomet', 'machakos', 'bungoma', 'eldoret', 'kakamega', 'kericho', 'kajiado', 'nandi', 'nyeri',
|
||||
'wote', 'kiambu', 'mwea', 'nakuru', 'narok']
|
||||
},
|
||||
{
|
||||
name: 'other',
|
||||
locations: ['other', 'none', 'unknown']
|
||||
}
|
||||
];
|
||||
|
||||
const areaTypes: Array<AreaType> = [
|
||||
{
|
||||
name: 'urban',
|
||||
area: ['urban', 'nairobi', 'mombasa']
|
||||
},
|
||||
{
|
||||
name: 'rural',
|
||||
area: ['rural', 'kakuma', 'kwale', 'kinango', 'kitui', 'nyanza']
|
||||
},
|
||||
{
|
||||
name: 'periurban',
|
||||
area: ['kilifi', 'periurban']
|
||||
},
|
||||
{
|
||||
name: 'other',
|
||||
area: ['other']
|
||||
}
|
||||
];
|
||||
|
||||
const accountTypes: Array<string> = ['user', 'cashier', 'vendor', 'tokenagent', 'group'];
|
||||
const transactionTypes: Array<string> = ['transactions', 'conversions', 'disbursements', 'rewards', 'reclamation'];
|
||||
const genders: Array<string> = ['male', 'female', 'other'];
|
||||
|
||||
@Injectable()
|
||||
export class MockBackendInterceptor implements HttpInterceptor {
|
||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
@ -90,32 +250,34 @@ export class MockBackendInterceptor implements HttpInterceptor {
|
||||
|
||||
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();
|
||||
case url.endsWith('/categories') && method === 'GET':
|
||||
return getCategories();
|
||||
case url.match(/\/categories\/\w+$/) && method === 'GET':
|
||||
return getCategoryByProduct();
|
||||
case url.endsWith('/areanames') && method === 'GET':
|
||||
return getAreaNames();
|
||||
case url.match(/\/areanames\/\w+$/) && method === 'GET':
|
||||
return getAreaNameByLocation();
|
||||
case url.endsWith('/areatypes') && method === 'GET':
|
||||
return getAreaTypes();
|
||||
case url.match(/\/areatypes\/\w+$/) && method === 'GET':
|
||||
return getAreaTypeByArea();
|
||||
case url.endsWith('/accounttypes') && method === 'GET':
|
||||
return getAccountTypes();
|
||||
case url.endsWith('/transactiontypes') && method === 'GET':
|
||||
return getTransactionTypes();
|
||||
case url.endsWith('/genders') && method === 'GET':
|
||||
return getGenders();
|
||||
default:
|
||||
// pass through any requests not handled above
|
||||
return next.handle(request);
|
||||
@ -124,76 +286,77 @@ export class MockBackendInterceptor implements HttpInterceptor {
|
||||
|
||||
// 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> {
|
||||
function getActions(): Observable<HttpResponse<any>> {
|
||||
return ok(actions);
|
||||
}
|
||||
|
||||
function getActionById(): Observable<any> {
|
||||
const queriedAction = actions.find(action => action.id === idFromUrl());
|
||||
function getActionById(): Observable<HttpResponse<any>> {
|
||||
const queriedAction: Action = actions.find(action => action.id === idFromUrl());
|
||||
return ok(queriedAction);
|
||||
}
|
||||
|
||||
function approveAction(): Observable<any> {
|
||||
const queriedAction = actions.find(action => action.id === idFromUrl());
|
||||
function approveAction(): Observable<HttpResponse<any>> {
|
||||
const queriedAction: Action = actions.find(action => action.id === idFromUrl());
|
||||
queriedAction.approval = body.approval;
|
||||
const message = `Action approval status set to ${body.approval} successfully!`;
|
||||
const message: string = `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> {
|
||||
function getTokens(): Observable<HttpResponse<any>> {
|
||||
return ok(tokens);
|
||||
}
|
||||
|
||||
function getTokenBySymbol(): Observable<any> {
|
||||
const queriedToken = tokens.find(token => token.symbol === stringFromUrl());
|
||||
function getTokenBySymbol(): Observable<HttpResponse<any>> {
|
||||
const queriedToken: Token = tokens.find(token => token.symbol === stringFromUrl());
|
||||
return ok(queriedToken);
|
||||
}
|
||||
|
||||
function getCategories(): Observable<HttpResponse<any>> {
|
||||
const categoryList: Array<string> = categories.map(category => category.name);
|
||||
return ok(categoryList);
|
||||
}
|
||||
|
||||
function getCategoryByProduct(): Observable<HttpResponse<any>> {
|
||||
const queriedCategory: Category = categories.find(category => category.products.includes(stringFromUrl()));
|
||||
return ok(queriedCategory.name);
|
||||
}
|
||||
|
||||
function getAreaNames(): Observable<HttpResponse<any>> {
|
||||
const areaNameList: Array<string> = areaNames.map(areaName => areaName.name);
|
||||
return ok(areaNameList);
|
||||
}
|
||||
|
||||
function getAreaNameByLocation(): Observable<HttpResponse<any>> {
|
||||
const queriedAreaName: AreaName = areaNames.find(areaName => areaName.locations.includes(stringFromUrl()));
|
||||
return ok(queriedAreaName.name);
|
||||
}
|
||||
|
||||
function getAreaTypes(): Observable<HttpResponse<any>> {
|
||||
const areaTypeList: Array<string> = areaTypes.map(areaType => areaType.name);
|
||||
return ok(areaTypeList);
|
||||
}
|
||||
|
||||
function getAreaTypeByArea(): Observable<HttpResponse<any>> {
|
||||
const queriedAreaType: AreaType = areaTypes.find(areaType => areaType.area.includes(stringFromUrl()));
|
||||
return ok(queriedAreaType.name);
|
||||
}
|
||||
|
||||
function getAccountTypes(): Observable<HttpResponse<any>> {
|
||||
return ok(accountTypes);
|
||||
}
|
||||
|
||||
function getTransactionTypes(): Observable<HttpResponse<any>> {
|
||||
return ok(transactionTypes);
|
||||
}
|
||||
|
||||
function getGenders(): Observable<HttpResponse<any>> {
|
||||
return ok(genders);
|
||||
}
|
||||
|
||||
// helper functions
|
||||
|
||||
function ok(body): Observable<any> {
|
||||
return of(new HttpResponse({ status: 200, body }));
|
||||
function ok(responseBody: any): Observable<HttpResponse<any>> {
|
||||
return of(new HttpResponse({ status: 200, body: responseBody }));
|
||||
}
|
||||
|
||||
function error(message): Observable<any> {
|
||||
@ -201,12 +364,12 @@ export class MockBackendInterceptor implements HttpInterceptor {
|
||||
}
|
||||
|
||||
function idFromUrl(): number {
|
||||
const urlParts = url.split('/');
|
||||
const urlParts: Array<string> = url.split('/');
|
||||
return parseInt(urlParts[urlParts.length - 1], 10);
|
||||
}
|
||||
|
||||
function stringFromUrl(): string {
|
||||
const urlParts = url.split('/');
|
||||
const urlParts: Array<string> = url.split('/');
|
||||
return urlParts[urlParts.length - 1];
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,23 @@
|
||||
let objCsv = {
|
||||
const objCsv: { size: number, dataFile: any } = {
|
||||
size: 0,
|
||||
dataFile: []
|
||||
};
|
||||
|
||||
function readCsv(input: any): any {
|
||||
function readCsv(input: any): Array<any> | void {
|
||||
if (input.files && input.files[0]) {
|
||||
let reader = new FileReader();
|
||||
const reader: FileReader = new FileReader();
|
||||
reader.readAsBinaryString(input.files[0]);
|
||||
reader.onload = event => {
|
||||
objCsv.size = event.total;
|
||||
// @ts-ignore
|
||||
objCsv.dataFile = event.target.result;
|
||||
return parseData(objCsv.dataFile);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function parseData(data: any): any {
|
||||
let csvData = [];
|
||||
const lineBreak = data.split('\n');
|
||||
function parseData(data: any): Array<any> {
|
||||
const csvData: Array<any> = [];
|
||||
const lineBreak: Array<any> = data.split('\n');
|
||||
lineBreak.forEach(res => {
|
||||
csvData.push(res.split(','));
|
||||
});
|
||||
|
22
src/app/_helpers/schema-validation.ts
Normal file
22
src/app/_helpers/schema-validation.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { validatePerson, validateVcard } from 'cic-schemas-data-validator';
|
||||
|
||||
async function personValidation(person: any): Promise<void> {
|
||||
const personValidationErrors: any = await validatePerson(person);
|
||||
|
||||
if (personValidationErrors) {
|
||||
personValidationErrors.map(error => console.error(`${error.message}`));
|
||||
}
|
||||
}
|
||||
|
||||
async function vcardValidation(vcard: any): Promise<void> {
|
||||
const vcardValidationErrors: any = await validateVcard(vcard);
|
||||
|
||||
if (vcardValidationErrors) {
|
||||
vcardValidationErrors.map(error => console.error(`${error.message}`));
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
personValidation,
|
||||
vcardValidation,
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import {Injectable, isDevMode} from '@angular/core';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {
|
||||
HttpRequest,
|
||||
HttpHandler,
|
||||
@ -6,7 +6,7 @@ import {
|
||||
HttpInterceptor, HttpErrorResponse
|
||||
} from '@angular/common/http';
|
||||
import {Observable, throwError} from 'rxjs';
|
||||
import {catchError, retry} from 'rxjs/operators';
|
||||
import {catchError} from 'rxjs/operators';
|
||||
import {ErrorDialogService, LoggingService} from '@app/_services';
|
||||
import {Router} from '@angular/router';
|
||||
|
||||
@ -22,7 +22,7 @@ export class ErrorInterceptor implements HttpInterceptor {
|
||||
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
|
||||
return next.handle(request).pipe(
|
||||
catchError((err: HttpErrorResponse) => {
|
||||
let errorMessage;
|
||||
let errorMessage: string;
|
||||
if (err.error instanceof ErrorEvent) {
|
||||
// A client-side or network error occurred. Handle it accordingly.
|
||||
errorMessage = `An error occurred: ${err.error.message}`;
|
||||
@ -37,7 +37,7 @@ export class ErrorInterceptor implements HttpInterceptor {
|
||||
this.router.navigateByUrl('/auth').then();
|
||||
break;
|
||||
case 403: // forbidden
|
||||
location.reload(true);
|
||||
alert('Access to resource is not allowed!');
|
||||
break;
|
||||
}
|
||||
// Return an observable with a user-facing error message.
|
||||
|
@ -13,11 +13,11 @@ export class HttpConfigInterceptor implements HttpInterceptor {
|
||||
constructor() {}
|
||||
|
||||
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
|
||||
const token = sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'));
|
||||
// const token: string = sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'));
|
||||
|
||||
if (token) {
|
||||
request = request.clone({headers: request.headers.set('Authorization', 'Bearer ' + token)});
|
||||
}
|
||||
// if (token) {
|
||||
// request = request.clone({headers: request.headers.set('Authorization', 'Bearer ' + token)});
|
||||
// }
|
||||
|
||||
return next.handle(request);
|
||||
}
|
||||
|
@ -18,20 +18,21 @@ export class LoggingInterceptor implements HttpInterceptor {
|
||||
) {}
|
||||
|
||||
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);
|
||||
}));
|
||||
return next.handle(request);
|
||||
// this.loggingService.sendInfoLevelMessage(request);
|
||||
// const startTime: number = 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: number = Date.now() - startTime;
|
||||
// const message: string = `${request.method} request for ${request.urlWithParams} ${status} in ${elapsedTime} ms`;
|
||||
// this.loggingService.sendInfoLevelMessage(message);
|
||||
// }));
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
export interface AccountDetails {
|
||||
interface AccountDetails {
|
||||
date_registered: number;
|
||||
gender: string;
|
||||
age?: string;
|
||||
type?: string;
|
||||
balance?: number;
|
||||
identities: {
|
||||
evm: {
|
||||
'bloxberg:8996': string[];
|
||||
@ -40,25 +41,25 @@ export interface AccountDetails {
|
||||
};
|
||||
}
|
||||
|
||||
export interface Signature {
|
||||
interface Signature {
|
||||
algo: string;
|
||||
data: string;
|
||||
digest: string;
|
||||
engine: string;
|
||||
}
|
||||
|
||||
export interface Meta {
|
||||
interface Meta {
|
||||
data: AccountDetails;
|
||||
id: string;
|
||||
signature: Signature;
|
||||
}
|
||||
|
||||
export interface MetaResponse {
|
||||
interface MetaResponse {
|
||||
id: string;
|
||||
m: Meta;
|
||||
}
|
||||
|
||||
export const defaultAccount: AccountDetails = {
|
||||
const defaultAccount: AccountDetails = {
|
||||
date_registered: Date.now(),
|
||||
gender: 'other',
|
||||
identities: {
|
||||
@ -94,3 +95,11 @@ export const defaultAccount: AccountDetails = {
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
export {
|
||||
AccountDetails,
|
||||
Signature,
|
||||
Meta,
|
||||
MetaResponse,
|
||||
defaultAccount
|
||||
};
|
||||
|
@ -1,4 +1,6 @@
|
||||
export * from '@app/_models/transaction';
|
||||
export * from '@app/_models/settings';
|
||||
export * from '@app/_models/user';
|
||||
export * from '@app/_models/account';
|
||||
export * from '@app/_models/staff';
|
||||
export * from '@app/_models/token';
|
||||
export * from '@app/_models/mappings';
|
||||
|
29
src/app/_models/mappings.ts
Normal file
29
src/app/_models/mappings.ts
Normal file
@ -0,0 +1,29 @@
|
||||
interface Action {
|
||||
id: number;
|
||||
user: string;
|
||||
role: string;
|
||||
action: string;
|
||||
approval: boolean;
|
||||
}
|
||||
|
||||
interface Category {
|
||||
name: string;
|
||||
products: Array<string>;
|
||||
}
|
||||
|
||||
interface AreaName {
|
||||
name: string;
|
||||
locations: Array<string>;
|
||||
}
|
||||
|
||||
interface AreaType {
|
||||
name: string;
|
||||
area: Array<string>;
|
||||
}
|
||||
|
||||
export {
|
||||
Action,
|
||||
Category,
|
||||
AreaName,
|
||||
AreaType
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
export class Settings {
|
||||
class Settings {
|
||||
w3: W3 = {
|
||||
engine: undefined,
|
||||
provider: undefined,
|
||||
@ -12,7 +12,12 @@ export class Settings {
|
||||
}
|
||||
}
|
||||
|
||||
export class W3 {
|
||||
class W3 {
|
||||
engine: any;
|
||||
provider: any;
|
||||
}
|
||||
|
||||
export {
|
||||
Settings,
|
||||
W3
|
||||
};
|
||||
|
@ -1,7 +1,11 @@
|
||||
export interface Staff {
|
||||
interface Staff {
|
||||
comment: string;
|
||||
email: string;
|
||||
name: string;
|
||||
tag: number;
|
||||
userid: string;
|
||||
}
|
||||
|
||||
export {
|
||||
Staff
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
export interface Token {
|
||||
interface Token {
|
||||
name: string;
|
||||
symbol: string;
|
||||
address: string;
|
||||
@ -13,3 +13,7 @@ export interface Token {
|
||||
reserveRatio?: string;
|
||||
owner?: string;
|
||||
}
|
||||
|
||||
export {
|
||||
Token
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {User} from '@app/_models/user';
|
||||
import {AccountDetails} from '@app/_models/account';
|
||||
|
||||
export class BlocksBloom {
|
||||
class BlocksBloom {
|
||||
low: number;
|
||||
blockFilter: string;
|
||||
blocktxFilter: string;
|
||||
@ -8,13 +8,13 @@ export class BlocksBloom {
|
||||
filterRounds: number;
|
||||
}
|
||||
|
||||
export class Token {
|
||||
class TxToken {
|
||||
address: string;
|
||||
name: string;
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
export class Tx {
|
||||
class Tx {
|
||||
block: number;
|
||||
success: boolean;
|
||||
timestamp: number;
|
||||
@ -22,22 +22,31 @@ export class Tx {
|
||||
txIndex: number;
|
||||
}
|
||||
|
||||
export class Transaction {
|
||||
class Transaction {
|
||||
from: string;
|
||||
sender: User;
|
||||
sender: AccountDetails;
|
||||
to: string;
|
||||
recipient: User;
|
||||
token: Token;
|
||||
recipient: AccountDetails;
|
||||
token: TxToken;
|
||||
tx: Tx;
|
||||
value: number;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export class Conversion {
|
||||
destinationToken: Token;
|
||||
class Conversion {
|
||||
destinationToken: TxToken;
|
||||
fromValue: number;
|
||||
sourceToken: Token;
|
||||
sourceToken: TxToken;
|
||||
toValue: number;
|
||||
trader: string;
|
||||
user: User;
|
||||
user: AccountDetails;
|
||||
tx: Tx;
|
||||
}
|
||||
|
||||
export {
|
||||
BlocksBloom,
|
||||
TxToken,
|
||||
Tx,
|
||||
Transaction,
|
||||
Conversion
|
||||
};
|
||||
|
@ -1,22 +0,0 @@
|
||||
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[];
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { KeyStore } from 'cic-client-meta';
|
||||
// TODO should we put this on the mutalble key store object
|
||||
// TODO should we put this on the mutable key store object
|
||||
import * as openpgp from 'openpgp';
|
||||
const keyring = new openpgp.Keyring();
|
||||
|
||||
@ -76,15 +76,14 @@ class MutablePgpKeyStore implements MutableKeyStore{
|
||||
}
|
||||
|
||||
async isValidKey(key): Promise<boolean> {
|
||||
// There is supposed to be an opengpg.readKey() method but I can't find it?
|
||||
// There is supposed to be an openpgp.readKey() method but I can't find it?
|
||||
const _key = await openpgp.key.readArmored(key);
|
||||
return !_key.err;
|
||||
}
|
||||
|
||||
async isEncryptedPrivateKey(privateKey: any): Promise<boolean> {
|
||||
const imported = await openpgp.key.readArmored(privateKey);
|
||||
for (let i = 0; i < imported.keys.length; i++) {
|
||||
const key = imported.keys[i];
|
||||
for (const key of imported.keys) {
|
||||
if (key.isDecrypted()) {
|
||||
return false;
|
||||
}
|
||||
|
@ -6,28 +6,32 @@ import {LoggingService} from '@app/_services/logging.service';
|
||||
import {MutableKeyStore, MutablePgpKeyStore} from '@app/_pgp';
|
||||
import {ErrorDialogService} from '@app/_services/error-dialog.service';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {Observable } from 'rxjs';
|
||||
import {HttpError} from '@app/_helpers/global-error-handler';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
sessionToken: any;
|
||||
sessionLoginCount = 0;
|
||||
privateKey: any;
|
||||
mutableKeyStore: MutableKeyStore = new MutablePgpKeyStore();
|
||||
sessionLoginCount: number = 0;
|
||||
mutableKeyStore: MutableKeyStore;
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
private loggingService: LoggingService,
|
||||
private errorDialogService: ErrorDialogService
|
||||
) {
|
||||
// TODO setting these together shoulds be atomic
|
||||
this.mutableKeyStore = new MutablePgpKeyStore();
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
await this.mutableKeyStore.loadKeyring();
|
||||
// TODO setting these together should be atomic
|
||||
if (sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'))) {
|
||||
this.sessionToken = sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'));
|
||||
}
|
||||
if (localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))) {
|
||||
this.privateKey = localStorage.getItem(btoa('CICADA_PRIVATE_KEY'));
|
||||
await this.mutableKeyStore.importPrivateKey(localStorage.getItem(btoa('CICADA_PRIVATE_KEY')));
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,7 +40,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
getWithToken(): void {
|
||||
const xhr = new XMLHttpRequest();
|
||||
const xhr: XMLHttpRequest = new XMLHttpRequest();
|
||||
xhr.responseType = 'text';
|
||||
xhr.open('GET', environment.cicMetaUrl + window.location.search.substring(1));
|
||||
xhr.setRequestHeader('Authorization', 'Bearer ' + this.sessionToken);
|
||||
@ -53,35 +57,39 @@ export class AuthService {
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
sendResponse(hobaResponseEncoded): void {
|
||||
const xhr = new XMLHttpRequest();
|
||||
// TODO rename to send signed challenge and set session. Also separate these responsibilities
|
||||
sendResponse(hobaResponseEncoded: any): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr: XMLHttpRequest = new XMLHttpRequest();
|
||||
xhr.responseType = 'text';
|
||||
xhr.open('GET', environment.cicMetaUrl + window.location.search.substring(1));
|
||||
xhr.setRequestHeader('Authorization', 'HOBA ' + hobaResponseEncoded);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.setRequestHeader('x-cic-automerge', 'none');
|
||||
xhr.addEventListener('load', (e) => {
|
||||
if (xhr.status === 401) {
|
||||
throw new Error('login rejected');
|
||||
if (xhr.status !== 200) {
|
||||
const error = new HttpError(xhr.statusText, xhr.status);
|
||||
return reject(error);
|
||||
}
|
||||
this.sessionToken = xhr.getResponseHeader('Token');
|
||||
sessionStorage.setItem(btoa('CICADA_SESSION_TOKEN'), this.sessionToken);
|
||||
this.sessionLoginCount++;
|
||||
this.setState('Click button to log in');
|
||||
return;
|
||||
return resolve(true);
|
||||
});
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
|
||||
getChallenge(): void {
|
||||
const xhr = new XMLHttpRequest();
|
||||
const xhr: XMLHttpRequest = new XMLHttpRequest();
|
||||
xhr.responseType = 'arraybuffer';
|
||||
xhr.open('GET', environment.cicMetaUrl + window.location.search.substring(1));
|
||||
xhr.onload = async (e) => {
|
||||
if (xhr.status === 401) {
|
||||
const authHeader = xhr.getResponseHeader('WWW-Authenticate');
|
||||
const o = hobaParseChallengeHeader(authHeader);
|
||||
await this.loginResponse(o);
|
||||
this.loginResponse(o);
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
@ -107,13 +115,33 @@ export class AuthService {
|
||||
}
|
||||
|
||||
|
||||
async loginResponse(o): Promise<any> {
|
||||
async loginResponse(o: { challenge: string, realm: any }): Promise<any> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const r = await signChallenge(o.challenge, o.realm, environment.cicMetaUrl, this.mutableKeyStore);
|
||||
this.sendResponse(r);
|
||||
const r = await signChallenge(o.challenge,
|
||||
o.realm,
|
||||
environment.cicMetaUrl,
|
||||
this.mutableKeyStore);
|
||||
const sessionTokenResult: boolean = await this.sendResponse(r);
|
||||
} catch (error) {
|
||||
this.errorDialogService.openDialog({message: 'Incorrect key passphrase.'});
|
||||
if (error instanceof HttpError) {
|
||||
if (error.status === 403) {
|
||||
this.errorDialogService.openDialog({ message: 'You are not authorized to use this system' });
|
||||
}
|
||||
if (error.status === 401) {
|
||||
this.errorDialogService.openDialog({
|
||||
message: 'Unable to authenticate with the service. ' +
|
||||
'Please speak with the staff at Grassroots ' +
|
||||
'Economics for requesting access ' +
|
||||
'staff@grassrootseconomics.net.'
|
||||
});
|
||||
}
|
||||
}
|
||||
// TODO define this error
|
||||
this.errorDialogService.openDialog({message: 'Incorrect key passphrase.'});
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loginView(): void {
|
||||
@ -128,10 +156,11 @@ export class AuthService {
|
||||
if (!isValidKeyCheck) {
|
||||
throw Error('The private key is invalid');
|
||||
}
|
||||
const isEncryptedKeyCheck = await this.mutableKeyStore.isEncryptedPrivateKey(privateKeyArmored);
|
||||
if (!isEncryptedKeyCheck) {
|
||||
throw Error('The private key doesn\'t have a password!');
|
||||
}
|
||||
// TODO leaving this out for now.
|
||||
// const isEncryptedKeyCheck = await this.mutableKeyStore.isEncryptedPrivateKey(privateKeyArmored);
|
||||
// if (!isEncryptedKeyCheck) {
|
||||
// throw Error('The private key doesn\'t have a password!');
|
||||
// }
|
||||
const key = await this.mutableKeyStore.importPrivateKey(privateKeyArmored);
|
||||
localStorage.setItem(btoa('CICADA_PRIVATE_KEY'), privateKeyArmored);
|
||||
} catch (err) {
|
||||
@ -152,18 +181,23 @@ export class AuthService {
|
||||
}
|
||||
|
||||
getTrustedUsers(): any {
|
||||
let trustedUsers = [];
|
||||
const trustedUsers: Array<any> = [];
|
||||
this.mutableKeyStore.getPublicKeys().forEach(key => trustedUsers.push(key.users[0].userId));
|
||||
return trustedUsers;
|
||||
}
|
||||
|
||||
getPublicKeys(): Observable<any> {
|
||||
return this.httpClient.get(`${environment.publicKeysUrl}`, {responseType: 'text'});
|
||||
async getPublicKeys(): Promise<any> {
|
||||
return await fetch(environment.publicKeysUrl)
|
||||
.then(res => {
|
||||
if (!res.ok) {
|
||||
// TODO does angular recommend an error interface?
|
||||
throw Error(`${res.statusText} - ${res.status}`);
|
||||
}
|
||||
return res.text();
|
||||
});
|
||||
}
|
||||
|
||||
async getPrivateKeys(): Promise<void> {
|
||||
if (this.privateKey !== undefined) {
|
||||
await this.mutableKeyStore.importPrivateKey(this.privateKey);
|
||||
}
|
||||
getPrivateKey(): any {
|
||||
return this.mutableKeyStore.getPrivateKey();
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,11 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Settings} from '@app/_models';
|
||||
import Web3 from 'web3';
|
||||
import {CICRegistry, TransactionHelper} from 'cic-client';
|
||||
import {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';
|
||||
import {RegistryService} from '@app/_services/registry.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -14,23 +13,20 @@ import {LoggingService} from '@app/_services/logging.service';
|
||||
export class BlockSyncService {
|
||||
readyStateTarget: number = 2;
|
||||
readyState: number = 0;
|
||||
fileGetter = new HttpGetter();
|
||||
|
||||
constructor(
|
||||
private transactionService: TransactionService,
|
||||
private loggingService: LoggingService
|
||||
private loggingService: LoggingService,
|
||||
private registryService: RegistryService,
|
||||
) { }
|
||||
|
||||
blockSync(address: string = null, offset: number = 0, limit: number = 100): any {
|
||||
blockSync(address: string = null, offset: number = 0, limit: number = 100): void {
|
||||
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);
|
||||
const settings: Settings = new Settings(this.scan);
|
||||
const readyStateElements: { network: number } = { network: 2 };
|
||||
settings.w3.provider = environment.web3Provider;
|
||||
settings.w3.engine = this.registryService.getWeb3();
|
||||
settings.registry = this.registryService.getRegistry();
|
||||
settings.txHelper = new TransactionHelper(settings.w3.engine, settings.registry);
|
||||
|
||||
settings.txHelper.ontransfer = async (transaction: any): Promise<void> => {
|
||||
@ -50,7 +46,7 @@ export class BlockSyncService {
|
||||
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');
|
||||
const wHeadSync: Worker = new Worker('./../assets/js/block-sync/head.js');
|
||||
wHeadSync.onmessage = (m) => {
|
||||
settings.txHelper.processReceipt(m.data);
|
||||
};
|
||||
@ -69,7 +65,7 @@ export class BlockSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
newTransferEvent(tx): any {
|
||||
newTransferEvent(tx: any): any {
|
||||
return new CustomEvent('cic_transfer', {
|
||||
detail: {
|
||||
tx,
|
||||
@ -77,7 +73,7 @@ export class BlockSyncService {
|
||||
});
|
||||
}
|
||||
|
||||
newConversionEvent(tx): any {
|
||||
newConversionEvent(tx: any): any {
|
||||
return new CustomEvent('cic_convert', {
|
||||
detail: {
|
||||
tx,
|
||||
@ -85,8 +81,8 @@ export class BlockSyncService {
|
||||
});
|
||||
}
|
||||
|
||||
async scan(settings, lo, hi, bloomBlockBytes, bloomBlocktxBytes, bloomRounds): Promise<void> {
|
||||
const w = new Worker('./../assets/js/block-sync/ondemand.js');
|
||||
async scan(settings: Settings, lo: number, hi: number, bloomBlockBytes: Uint8Array, bloomBlocktxBytes: Uint8Array, bloomRounds: any): Promise<void> {
|
||||
const w: Worker = new Worker('./../assets/js/block-sync/ondemand.js');
|
||||
w.onmessage = (m) => {
|
||||
settings.txHelper.processReceipt(m.data);
|
||||
};
|
||||
@ -103,12 +99,12 @@ export class BlockSyncService {
|
||||
}
|
||||
|
||||
fetcher(settings: Settings, transactionsInfo: any): void {
|
||||
const blockFilterBinstr = window.atob(transactionsInfo.block_filter);
|
||||
const bOne = new Uint8Array(blockFilterBinstr.length);
|
||||
const blockFilterBinstr: string = window.atob(transactionsInfo.block_filter);
|
||||
const bOne: Uint8Array = 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);
|
||||
const blocktxFilterBinstr: string = window.atob(transactionsInfo.blocktx_filter);
|
||||
const bTwo: Uint8Array = 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);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import {MatDialog, MatDialogRef} from '@angular/material/dialog';
|
||||
import {ErrorDialogComponent} from '@app/shared/error-dialog/error-dialog.component';
|
||||
|
||||
@Injectable({
|
||||
@ -17,7 +17,7 @@ export class ErrorDialogService {
|
||||
return false;
|
||||
}
|
||||
this.isDialogOpen = true;
|
||||
const dialogRef = this.dialog.open(ErrorDialogComponent, {
|
||||
const dialogRef: MatDialogRef<any> = this.dialog.open(ErrorDialogComponent, {
|
||||
width: '300px',
|
||||
data
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
import {Observable} from 'rxjs';
|
||||
import {environment} from '@src/environments/environment';
|
||||
import {first} from 'rxjs/operators';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
@ -8,15 +8,24 @@ import {HttpClient} from '@angular/common/http';
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class LocationService {
|
||||
locations: any = '';
|
||||
private locationsList = new BehaviorSubject<any>(this.locations);
|
||||
locationsSubject = this.locationsList.asObservable();
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
) { }
|
||||
|
||||
getLocations(): void {
|
||||
this.httpClient.get(`${environment.cicCacheUrl}/locations`).pipe(first()).subscribe(res => this.locationsList.next(res));
|
||||
getAreaNames(): Observable<any> {
|
||||
return this.httpClient.get(`${environment.cicMetaUrl}/areanames`);
|
||||
}
|
||||
|
||||
getAreaNameByLocation(location: string): Observable<any> {
|
||||
return this.httpClient.get(`${environment.cicMetaUrl}/areanames/${location.toLowerCase()}`);
|
||||
}
|
||||
|
||||
getAreaTypes(): Observable<any> {
|
||||
return this.httpClient.get(`${environment.cicMetaUrl}/areatypes`).pipe(first());
|
||||
}
|
||||
|
||||
getAreaTypeByArea(area: string): Observable<any> {
|
||||
return this.httpClient.get(`${environment.cicMetaUrl}/areatypes/${area.toLowerCase()}`).pipe(first());
|
||||
}
|
||||
}
|
||||
|
@ -15,31 +15,31 @@ export class LoggingService {
|
||||
}
|
||||
}
|
||||
|
||||
sendTraceLevelMessage(message, source, error): void {
|
||||
sendTraceLevelMessage(message: any, source: any, error: any): void {
|
||||
this.logger.trace(message, source, error);
|
||||
}
|
||||
|
||||
sendDebugLevelMessage(message, source, error): void {
|
||||
sendDebugLevelMessage(message: any, source: any, error: any): void {
|
||||
this.logger.debug(message, source, error);
|
||||
}
|
||||
|
||||
sendInfoLevelMessage(message): void {
|
||||
sendInfoLevelMessage(message: any): void {
|
||||
this.logger.info(message);
|
||||
}
|
||||
|
||||
sendLogLevelMessage(message, source, error): void {
|
||||
sendLogLevelMessage(message: any, source: any, error: any): void {
|
||||
this.logger.log(message, source, error);
|
||||
}
|
||||
|
||||
sendWarnLevelMessage(message, error): void {
|
||||
sendWarnLevelMessage(message: any, error: any): void {
|
||||
this.logger.warn(message, error);
|
||||
}
|
||||
|
||||
sendErrorLevelMessage(message, source, error): void {
|
||||
sendErrorLevelMessage(message: any, source: any, error: any): void {
|
||||
this.logger.error(message, source, error);
|
||||
}
|
||||
|
||||
sendFatalLevelMessage(message, source, error): void {
|
||||
sendFatalLevelMessage(message: any, source: any, error: any): void {
|
||||
this.logger.fatal(message, source, error);
|
||||
}
|
||||
}
|
||||
|
16
src/app/_services/registry.service.spec.ts
Normal file
16
src/app/_services/registry.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RegistryService } from './registry.service';
|
||||
|
||||
describe('RegistryService', () => {
|
||||
let service: RegistryService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(RegistryService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
28
src/app/_services/registry.service.ts
Normal file
28
src/app/_services/registry.service.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import Web3 from 'web3';
|
||||
import {environment} from '@src/environments/environment';
|
||||
import {CICRegistry, FileGetter} from 'cic-client';
|
||||
import {HttpGetter} from '@app/_helpers';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class RegistryService {
|
||||
web3: Web3 = new Web3(environment.web3Provider);
|
||||
fileGetter: FileGetter = new HttpGetter();
|
||||
registry: CICRegistry = new CICRegistry(this.web3, environment.registryAddress, 'CICRegistry', this.fileGetter,
|
||||
['../../assets/js/block-sync/data']);
|
||||
|
||||
constructor() {
|
||||
this.registry.declaratorHelper.addTrust(environment.trustedDeclaratorAddress);
|
||||
this.registry.load();
|
||||
}
|
||||
|
||||
getRegistry(): any {
|
||||
return this.registry;
|
||||
}
|
||||
|
||||
getWeb3(): any {
|
||||
return this.web3;
|
||||
}
|
||||
}
|
@ -1,32 +1,34 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { EventEmitter, 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 {Registry, TokenRegistry} from '@app/_eth';
|
||||
import {TokenRegistry} from '@app/_eth';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {RegistryService} from '@app/_services/registry.service';
|
||||
|
||||
@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();
|
||||
registry: CICRegistry;
|
||||
tokenRegistry: TokenRegistry;
|
||||
LoadEvent: EventEmitter<number> = new EventEmitter<number>();
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
) { }
|
||||
private registryService: RegistryService,
|
||||
) {
|
||||
this.registry = registryService.getRegistry();
|
||||
this.registry.load();
|
||||
this.registry.onload = async (address: string): Promise<void> => {
|
||||
this.tokenRegistry = new TokenRegistry(await this.registry.getContractAddressByName('TokenRegistry'));
|
||||
this.LoadEvent.next(Date.now());
|
||||
};
|
||||
}
|
||||
|
||||
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));
|
||||
async getTokens(): Promise<Array<Promise<string>>> {
|
||||
const count: number = await this.tokenRegistry.totalTokens();
|
||||
return Array.from({length: count}, async (v, i) => await this.tokenRegistry.entry(i));
|
||||
}
|
||||
|
||||
getTokenBySymbol(symbol: string): Observable<any> {
|
||||
@ -34,8 +36,7 @@ export class TokenService {
|
||||
}
|
||||
|
||||
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));
|
||||
const sarafuToken = await this.registry.addToken(await this.tokenRegistry.entry(0));
|
||||
return await sarafuToken.methods.balanceOf(address).call();
|
||||
}
|
||||
}
|
||||
|
@ -13,9 +13,10 @@ 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 {Registry} from '@app/_eth';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
const Web3 = require('web3');
|
||||
import {CICRegistry} from 'cic-client';
|
||||
import {RegistryService} from '@app/_services/registry.service';
|
||||
import Web3 from 'web3';
|
||||
const vCard = require('vcard-parser');
|
||||
|
||||
@Injectable({
|
||||
@ -26,15 +27,20 @@ export class TransactionService {
|
||||
private transactionList = new BehaviorSubject<any[]>(this.transactions);
|
||||
transactionsSubject = this.transactionList.asObservable();
|
||||
userInfo: any;
|
||||
web3 = new Web3(environment.web3Provider);
|
||||
registry = new Registry(environment.registryAddress);
|
||||
web3: Web3;
|
||||
registry: CICRegistry;
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
private authService: AuthService,
|
||||
private userService: UserService,
|
||||
private loggingService: LoggingService
|
||||
) { }
|
||||
private loggingService: LoggingService,
|
||||
private registryService: RegistryService,
|
||||
) {
|
||||
this.web3 = this.registryService.getWeb3();
|
||||
this.registry = registryService.getRegistry();
|
||||
this.registry.load();
|
||||
}
|
||||
|
||||
getAllTransactions(offset: number, limit: number): Observable<any> {
|
||||
return this.httpClient.get(`${environment.cicCacheUrl}/tx/${offset}/${limit}`);
|
||||
@ -100,7 +106,7 @@ export class TransactionService {
|
||||
}
|
||||
|
||||
async transferRequest(tokenAddress: string, senderAddress: string, recipientAddress: string, value: number): Promise<any> {
|
||||
const transferAuthAddress = await this.registry.addressOf('TransferAuthorization');
|
||||
const transferAuthAddress = await this.registry.getContractAddressByName('TransferAuthorization');
|
||||
const hashFunction = new Keccak(256);
|
||||
hashFunction.update('createRequest(address,address,address,uint256)');
|
||||
const hash = hashFunction.digest();
|
||||
@ -110,7 +116,7 @@ export class TransactionService {
|
||||
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.gasPrice = Number(await this.web3.eth.getGasPrice());
|
||||
tx.gasLimit = 8000000;
|
||||
tx.to = fromHex(strip0x(transferAuthAddress));
|
||||
tx.value = toValue(value);
|
||||
|
@ -1,14 +1,19 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
import {BehaviorSubject, Observable, Subject} 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 {ArgPair, Envelope, Phone, Syncable, User} from 'cic-client-meta';
|
||||
import {AccountDetails} from '@app/_models';
|
||||
import {LoggingService} from '@app/_services/logging.service';
|
||||
import {TokenService} from '@app/_services/token.service';
|
||||
import {AccountIndex, Registry} from '@app/_eth';
|
||||
import {MutableKeyStore, MutablePgpKeyStore, PGPSigner, Signer} from '@app/_pgp';
|
||||
import {AccountIndex} from '@app/_eth';
|
||||
import {MutableKeyStore, PGPSigner, Signer} from '@app/_pgp';
|
||||
import {RegistryService} from '@app/_services/registry.service';
|
||||
import {CICRegistry} from 'cic-client';
|
||||
import {AuthService} from '@app/_services/auth.service';
|
||||
import {personValidation, vcardValidation} from '@app/_helpers';
|
||||
import {add0x} from '@src/assets/js/ethtx/dist/hex';
|
||||
const vCard = require('vcard-parser');
|
||||
|
||||
@Injectable({
|
||||
@ -16,49 +21,58 @@ const vCard = require('vcard-parser');
|
||||
})
|
||||
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);
|
||||
keystore: MutableKeyStore;
|
||||
signer: Signer;
|
||||
registry: CICRegistry;
|
||||
|
||||
accountsMeta = [];
|
||||
accounts: any = [];
|
||||
private accountsList = new BehaviorSubject<any>(this.accounts);
|
||||
accountsSubject = this.accountsList.asObservable();
|
||||
accounts: Array<AccountDetails> = [];
|
||||
private accountsList: BehaviorSubject<Array<AccountDetails>> = new BehaviorSubject<Array<AccountDetails>>(this.accounts);
|
||||
accountsSubject: Observable<Array<AccountDetails>> = 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();
|
||||
actions: Array<any> = [];
|
||||
private actionsList: BehaviorSubject<any> = new BehaviorSubject<any>(this.actions);
|
||||
actionsSubject: Observable<Array<any>> = this.actionsList.asObservable();
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
private loggingService: LoggingService,
|
||||
private tokenService: TokenService
|
||||
private tokenService: TokenService,
|
||||
private registryService: RegistryService,
|
||||
private authService: AuthService,
|
||||
) {
|
||||
this.authService.init().then(() => {
|
||||
this.keystore = authService.mutableKeyStore;
|
||||
this.signer = new PGPSigner(this.keystore);
|
||||
});
|
||||
this.registry = registryService.getRegistry();
|
||||
this.registry.load();
|
||||
}
|
||||
|
||||
resetPin(phone: string): Observable<any> {
|
||||
const params = new HttpParams().set('phoneNumber', phone);
|
||||
const params: HttpParams = new HttpParams().set('phoneNumber', phone);
|
||||
return this.httpClient.get(`${environment.cicUssdUrl}/pin`, {params});
|
||||
}
|
||||
|
||||
getAccountStatus(phone: string): any {
|
||||
const params = new HttpParams().set('phoneNumber', phone);
|
||||
getAccountStatus(phone: string): Observable<any> {
|
||||
const params: HttpParams = new HttpParams().set('phoneNumber', phone);
|
||||
return this.httpClient.get(`${environment.cicUssdUrl}/pin`, {params});
|
||||
}
|
||||
|
||||
getLockedAccounts(offset: number, limit: number): any {
|
||||
getLockedAccounts(offset: number, limit: number): Observable<any> {
|
||||
return this.httpClient.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
|
||||
businessCategory: string, userLocation: string, location: string, locationType: string
|
||||
): Promise<any> {
|
||||
let reqBody = metaAccount;
|
||||
let accountInfo = reqBody.m.data;
|
||||
const accountInfo: any = {
|
||||
vcard: {
|
||||
fn: [{}],
|
||||
n: [{}],
|
||||
tel: [{}]
|
||||
},
|
||||
location: {}
|
||||
};
|
||||
accountInfo.vcard.fn[0].value = name;
|
||||
accountInfo.vcard.n[0].value = name.split(' ');
|
||||
accountInfo.vcard.tel[0].value = phoneNumber;
|
||||
@ -70,16 +84,17 @@ export class UserService {
|
||||
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.httpClient.get(`${environment.cicMetaUrl}/${accountKey}`, { headers: this.headers }).pipe(first()).subscribe(async res => {
|
||||
await vcardValidation(accountInfo.vcard);
|
||||
accountInfo.vcard = btoa(vCard.generate(accountInfo.vcard));
|
||||
const accountKey: string = await User.toKey(address);
|
||||
this.getAccountDetailsFromMeta(accountKey).pipe(first()).subscribe(async res => {
|
||||
const syncableAccount: Syncable = Envelope.fromJSON(JSON.stringify(res)).unwrap();
|
||||
let update = [];
|
||||
for (const prop in reqBody) {
|
||||
update.push(new ArgPair(prop, reqBody[prop]));
|
||||
const update: Array<ArgPair> = [];
|
||||
for (const prop in accountInfo) {
|
||||
update.push(new ArgPair(prop, accountInfo[prop]));
|
||||
}
|
||||
syncableAccount.update(update, 'client-branch');
|
||||
await personValidation(syncableAccount.m.data);
|
||||
await this.updateMeta(syncableAccount, accountKey, this.headers);
|
||||
}, async error => {
|
||||
this.loggingService.sendErrorLevelMessage('Can\'t find account info in meta service', this, {error});
|
||||
@ -90,26 +105,18 @@ export class UserService {
|
||||
}
|
||||
|
||||
async updateMeta(syncableAccount: Syncable, accountKey: string, headers: HttpHeaders): Promise<any> {
|
||||
const envelope = await this.wrap(syncableAccount , this.signer);
|
||||
const reqBody = envelope.toJSON();
|
||||
const envelope: Envelope = await this.wrap(syncableAccount , this.signer);
|
||||
const reqBody: string = envelope.toJSON();
|
||||
this.httpClient.put(`${environment.cicMetaUrl}/${accountKey}`, reqBody , { headers }).pipe(first()).subscribe(res => {
|
||||
this.loggingService.sendInfoLevelMessage(`Response: ${res}`);
|
||||
});
|
||||
}
|
||||
|
||||
getAccounts(): void {
|
||||
this.httpClient.get(`${environment.cicCacheUrl}/accounts`).pipe(first()).subscribe(res => this.accountsList.next(res));
|
||||
}
|
||||
|
||||
getAccountById(id: number): Observable<any> {
|
||||
return this.httpClient.get(`${environment.cicCacheUrl}/accounts/${id}`);
|
||||
}
|
||||
|
||||
getActions(): void {
|
||||
this.httpClient.get(`${environment.cicCacheUrl}/actions`).pipe(first()).subscribe(res => this.actionsList.next(res));
|
||||
}
|
||||
|
||||
getActionById(id: string): any {
|
||||
getActionById(id: string): Observable<any> {
|
||||
return this.httpClient.get(`${environment.cicCacheUrl}/actions/${id}`);
|
||||
}
|
||||
|
||||
@ -121,30 +128,19 @@ export class UserService {
|
||||
return this.httpClient.post(`${environment.cicCacheUrl}/actions/${id}`, { approval: false });
|
||||
}
|
||||
|
||||
getHistoryByUser(id: string): Observable<any> {
|
||||
return this.httpClient.get(`${environment.cicCacheUrl}/history/${id}`);
|
||||
}
|
||||
|
||||
getAccountDetailsFromMeta(userKey: string): Observable<any> {
|
||||
return this.httpClient.get(`${environment.cicMetaUrl}/${userKey}`, { headers: this.headers });
|
||||
}
|
||||
|
||||
getUser(userKey: string): any {
|
||||
return this.httpClient.get(`${environment.cicMetaUrl}/${userKey}`, { headers: this.headers })
|
||||
.pipe(first()).subscribe(async res => {
|
||||
return Envelope.fromJSON(JSON.stringify(res)).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
wrap(syncable: Syncable, signer: Signer): Promise<Envelope> {
|
||||
return new Promise<Envelope>(async (whohoo, doh) => {
|
||||
return new Promise<Envelope>(async (resolve, reject) => {
|
||||
syncable.setSigner(signer);
|
||||
syncable.onwrap = async (env) => {
|
||||
if (env === undefined) {
|
||||
doh();
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
whohoo(env);
|
||||
resolve(env);
|
||||
};
|
||||
await syncable.sign();
|
||||
});
|
||||
@ -152,28 +148,71 @@ export class UserService {
|
||||
|
||||
async loadAccounts(limit: number = 100, offset: number = 0): Promise<void> {
|
||||
this.resetAccountsList();
|
||||
const accountIndexAddress = await this.registry.addressOf('AccountRegistry');
|
||||
const accountIndexAddress: string = await this.registry.getContractAddressByName('AccountRegistry');
|
||||
const accountIndexQuery = new AccountIndex(accountIndexAddress);
|
||||
const accountAddresses = await accountIndexQuery.last(await accountIndexQuery.totalAccounts());
|
||||
const accountAddresses: Array<string> = 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);
|
||||
await this.getAccountByAddress(accountAddress, limit);
|
||||
}
|
||||
}
|
||||
|
||||
async getAccountByAddress(accountAddress: string, limit: number = 100): Promise<Observable<AccountDetails>> {
|
||||
let accountSubject: Subject<any> = new Subject<any>();
|
||||
this.getAccountDetailsFromMeta(await User.toKey(add0x(accountAddress))).pipe(first()).subscribe(async res => {
|
||||
const account: Syncable = Envelope.fromJSON(JSON.stringify(res)).unwrap();
|
||||
const accountInfo = account.m.data;
|
||||
accountInfo.balance = await this.tokenService.getTokenBalance(accountInfo.identities.evm['bloxberg:8996'][0]);
|
||||
await personValidation(accountInfo);
|
||||
accountInfo.balance = await this.tokenService.getTokenBalance(accountInfo.identities.evm[`bloxberg:${environment.bloxbergChainId}`][0]);
|
||||
accountInfo.vcard = vCard.parse(atob(accountInfo.vcard));
|
||||
await vcardValidation(accountInfo.vcard);
|
||||
this.accounts.unshift(accountInfo);
|
||||
if (this.accounts.length > limit) {
|
||||
this.accounts.length = limit;
|
||||
}
|
||||
this.accountsList.next(this.accounts);
|
||||
accountSubject.next(accountInfo);
|
||||
});
|
||||
return accountSubject.asObservable();
|
||||
}
|
||||
|
||||
async getAccountByPhone(phoneNumber: string, limit: number = 100): Promise<Observable<AccountDetails>> {
|
||||
let accountSubject: Subject<any> = new Subject<any>();
|
||||
this.getAccountDetailsFromMeta(await Phone.toKey(phoneNumber)).pipe(first()).subscribe(async res => {
|
||||
const response: Syncable = Envelope.fromJSON(JSON.stringify(res)).unwrap();
|
||||
const address: string = response.m.data;
|
||||
const account: Observable<AccountDetails> = await this.getAccountByAddress(address, limit);
|
||||
account.subscribe(result => {
|
||||
accountSubject.next(result);
|
||||
});
|
||||
});
|
||||
return accountSubject.asObservable();
|
||||
}
|
||||
|
||||
resetAccountsList(): void {
|
||||
this.accounts = [];
|
||||
this.accountsList.next(this.accounts);
|
||||
}
|
||||
|
||||
searchAccountByName(name: string): any { return; }
|
||||
|
||||
getCategories(): Observable<any> {
|
||||
return this.httpClient.get(`${environment.cicMetaUrl}/categories`);
|
||||
}
|
||||
|
||||
getCategoryByProduct(product: string): Observable<any> {
|
||||
return this.httpClient.get(`${environment.cicMetaUrl}/categories/${product.toLowerCase()}`);
|
||||
}
|
||||
|
||||
getAccountTypes(): Observable<any> {
|
||||
return this.httpClient.get(`${environment.cicMetaUrl}/accounttypes`);
|
||||
}
|
||||
|
||||
getTransactionTypes(): Observable<any> {
|
||||
return this.httpClient.get(`${environment.cicMetaUrl}/transactiontypes`);
|
||||
}
|
||||
|
||||
getGenders(): Observable<any> {
|
||||
return this.httpClient.get(`${environment.cicMetaUrl}/genders`);
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ export class AppComponent implements OnInit {
|
||||
title = 'CICADA';
|
||||
readyStateTarget: number = 3;
|
||||
readyState: number = 0;
|
||||
mediaQuery = window.matchMedia('(max-width: 768px)');
|
||||
mediaQuery: MediaQueryList = window.matchMedia('(max-width: 768px)');
|
||||
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
@ -23,12 +23,19 @@ export class AppComponent implements OnInit {
|
||||
private swUpdate: SwUpdate
|
||||
) {
|
||||
(async () => {
|
||||
await this.authService.mutableKeyStore.loadKeyring();
|
||||
this.authService.getPublicKeys()
|
||||
.pipe(catchError(async (error) => {
|
||||
this.loggingService.sendErrorLevelMessage('Unable to load trusted public keys.', this, {error});
|
||||
try {
|
||||
await this.authService.init();
|
||||
// this.authService.getPublicKeys()
|
||||
// .pipe(catchError(async (error) => {
|
||||
// this.loggingService.sendErrorLevelMessage('Unable to load trusted public keys.', this, {error});
|
||||
// this.errorDialogService.openDialog({message: 'Trusted keys endpoint can\'t be reached. Please try again later.'});
|
||||
// })).subscribe(this.authService.mutableKeyStore.importPublicKey);
|
||||
const publicKeys = await this.authService.getPublicKeys();
|
||||
await this.authService.mutableKeyStore.importPublicKey(publicKeys);
|
||||
} catch (error) {
|
||||
this.errorDialogService.openDialog({message: 'Trusted keys endpoint can\'t be reached. Please try again later.'});
|
||||
})).subscribe(this.authService.mutableKeyStore.importPublicKey);
|
||||
// TODO do something to halt user progress...show a sad cicada page 🦗?
|
||||
}
|
||||
})();
|
||||
this.mediaQuery.addListener(this.onResize);
|
||||
this.onResize(this.mediaQuery);
|
||||
@ -46,9 +53,9 @@ export class AppComponent implements OnInit {
|
||||
|
||||
// Load resize
|
||||
onResize(e): void {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const content = document.getElementById('content');
|
||||
const sidebarCollapse = document.getElementById('sidebarCollapse');
|
||||
const sidebar: HTMLElement = document.getElementById('sidebar');
|
||||
const content: HTMLElement = document.getElementById('content');
|
||||
const sidebarCollapse: HTMLElement = document.getElementById('sidebarCollapse');
|
||||
if (sidebarCollapse?.classList.contains('active')) {
|
||||
sidebarCollapse?.classList.remove('active');
|
||||
}
|
||||
@ -71,13 +78,13 @@ export class AppComponent implements OnInit {
|
||||
|
||||
@HostListener('window:cic_transfer', ['$event'])
|
||||
async cicTransfer(event: CustomEvent): Promise<void> {
|
||||
const transaction = event.detail.tx;
|
||||
const transaction: any = event.detail.tx;
|
||||
await this.transactionService.setTransaction(transaction, 100);
|
||||
}
|
||||
|
||||
@HostListener('window:cic_convert', ['$event'])
|
||||
async cicConvert(event: CustomEvent): Promise<void> {
|
||||
const conversion = event.detail.tx;
|
||||
const conversion: any = event.detail.tx;
|
||||
await this.transactionService.setConversion(conversion, 100);
|
||||
}
|
||||
}
|
||||
|
@ -20,8 +20,8 @@ export class PasswordToggleDirective {
|
||||
}
|
||||
|
||||
togglePasswordVisibility(): void {
|
||||
const password = document.getElementById(this.id);
|
||||
const icon = document.getElementById(this.iconId);
|
||||
const password: HTMLElement = document.getElementById(this.id);
|
||||
const icon: HTMLElement = document.getElementById(this.iconId);
|
||||
// @ts-ignore
|
||||
if (password.type === 'password') {
|
||||
// @ts-ignore
|
||||
|
@ -14,7 +14,7 @@ export class AuthComponent implements OnInit {
|
||||
keyForm: FormGroup;
|
||||
submitted: boolean = false;
|
||||
loading: boolean = false;
|
||||
matcher = new CustomErrorStateMatcher();
|
||||
matcher: CustomErrorStateMatcher = new CustomErrorStateMatcher();
|
||||
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
@ -26,12 +26,11 @@ export class AuthComponent implements OnInit {
|
||||
this.keyForm = this.formBuilder.group({
|
||||
key: ['', Validators.required],
|
||||
});
|
||||
if (this.authService.privateKey !== undefined) {
|
||||
const setKey = await this.authService.setKey(this.authService.privateKey);
|
||||
if (setKey && this.authService.sessionToken !== undefined) {
|
||||
this.authService.setState('Click button to log in');
|
||||
}
|
||||
}
|
||||
await this.authService.init();
|
||||
// if (this.authService.privateKey !== undefined) {
|
||||
// const setKey = await this.authService.setKey(this.authService.privateKey);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
get keyFormStub(): any { return this.keyForm.controls; }
|
||||
@ -47,22 +46,26 @@ export class AuthComponent implements OnInit {
|
||||
}
|
||||
|
||||
login(): void {
|
||||
const loginStatus = this.authService.login();
|
||||
if (loginStatus) {
|
||||
// TODO check if we have privatekey
|
||||
// Send us to home if we have a private key
|
||||
// talk to meta somehow
|
||||
// in the error interceptor if 401/403 handle it
|
||||
// if 200 go /home
|
||||
if (this.authService.getPrivateKey()) {
|
||||
this.router.navigate(['/home']);
|
||||
}
|
||||
}
|
||||
|
||||
switchWindows(): void {
|
||||
this.authService.sessionToken = undefined;
|
||||
const divOne = document.getElementById('one');
|
||||
const divTwo = document.getElementById('two');
|
||||
const divOne: HTMLElement = document.getElementById('one');
|
||||
const divTwo: HTMLElement = document.getElementById('two');
|
||||
this.toggleDisplay(divOne);
|
||||
this.toggleDisplay(divTwo);
|
||||
}
|
||||
|
||||
toggleDisplay(element: any): void {
|
||||
const style = window.getComputedStyle(element).display;
|
||||
const style: string = window.getComputedStyle(element).display;
|
||||
if (style === 'block') {
|
||||
element.style.display = 'none';
|
||||
} else {
|
||||
|
@ -14,7 +14,7 @@
|
||||
<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 !== undefined" class="breadcrumb-item active" aria-current="page">{{account?.vcard?.fn[0].value}}</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">
|
||||
@ -35,7 +35,10 @@
|
||||
</h3>
|
||||
<span class="ml-auto"><strong>Balance:</strong> {{account?.balance | tokenRatio}} 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>
|
||||
<span class="ml-2"><strong>Address:</strong>
|
||||
<a href="{{bloxbergLink}}" target="_blank"> {{accountAddress}} </a>
|
||||
<img src="assets/images/checklist.svg" class="ml-2" height="20rem" (click)="copyAddress()" alt="Copy">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="account" class="card mt-3 mb-3">
|
||||
@ -75,11 +78,9 @@
|
||||
<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-option *ngFor="let accountType of accountTypes" [value]="accountType">
|
||||
{{accountType | uppercase}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="submitted && accountInfoFormStub.type.errors">Type is required.</mat-error>
|
||||
</mat-form-field>
|
||||
@ -99,9 +100,9 @@
|
||||
<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-option *ngFor="let gender of genders" [value]="gender">
|
||||
{{gender | uppercase}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="submitted && accountInfoFormStub.gender.errors">Gender is required.</mat-error>
|
||||
</mat-form-field>
|
||||
@ -112,16 +113,9 @@
|
||||
<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-option *ngFor="let category of categories" [value]="category">
|
||||
{{category | titlecase}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="submitted && accountInfoFormStub.businessCategory.errors">
|
||||
Category is required.
|
||||
@ -146,16 +140,9 @@
|
||||
<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 *ngFor="let area of areaNames" [value]="area">
|
||||
{{area | uppercase}}
|
||||
</mat-option>
|
||||
</mat-optgroup>
|
||||
</div>
|
||||
</div>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="submitted && accountInfoFormStub.location.errors">Location is required.</mat-error>
|
||||
</mat-form-field>
|
||||
@ -166,23 +153,22 @@
|
||||
<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-option *ngFor="let type of areaTypes" [value]="type">
|
||||
{{type | uppercase}}
|
||||
</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">
|
||||
<button mat-raised-button color="primary" type="button" class="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"
|
||||
<button mat-raised-button color="primary" type="button" class="btn btn-outline-success mb-3"
|
||||
(click)="resetPin()">
|
||||
Reset Pin
|
||||
</button>
|
||||
@ -240,7 +226,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-tab-group dynamicHeight mat-align-tabs="start">
|
||||
<mat-tab-group *ngIf="account" dynamicHeight mat-align-tabs="start">
|
||||
<mat-tab label="Transactions">
|
||||
<app-transaction-details [transaction]="transaction"></app-transaction-details>
|
||||
<div class="card mt-1">
|
||||
@ -250,11 +236,9 @@
|
||||
<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-option *ngFor="let transactionType of transactionsTypes" [value]="transactionType">
|
||||
{{transactionType | uppercase}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" type="button" class="btn btn-outline-primary ml-auto mr-2" (click)="downloadCsv(transactions, 'transactions')"> EXPORT </button>
|
||||
@ -324,11 +308,9 @@
|
||||
<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-option *ngFor="let accountType of accountTypes" [value]="accountType">
|
||||
{{accountType | uppercase}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" type="button" class="btn btn-outline-primary ml-auto mr-2" (click)="downloadCsv(accounts, 'accounts')"> EXPORT </button>
|
||||
|
@ -50,12 +50,4 @@ describe('AccountDetailsComponent', () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {ChangeDetectionStrategy, Component, OnInit, ViewChild} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, ViewChild} from '@angular/core';
|
||||
import {MatTableDataSource} from '@angular/material/table';
|
||||
import {MatPaginator} from '@angular/material/paginator';
|
||||
import {MatSort} from '@angular/material/sort';
|
||||
@ -6,9 +6,11 @@ import {BlockSyncService, LocationService, LoggingService, TokenService, Transac
|
||||
import {ActivatedRoute, Params, Router} from '@angular/router';
|
||||
import {first} from 'rxjs/operators';
|
||||
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
|
||||
import {CustomErrorStateMatcher, exportCsv} from '@app/_helpers';
|
||||
import {Envelope, User} from 'cic-client-meta';
|
||||
const vCard = require('vcard-parser');
|
||||
import {copyToClipboard, CustomErrorStateMatcher, exportCsv} from '@app/_helpers';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
import {add0x, strip0x} from '@src/assets/js/ethtx/dist/hex';
|
||||
import {environment} from '@src/environments/environment';
|
||||
import {AccountDetails, AreaName, AreaType, Category, Transaction} from '@app/_models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-account-details',
|
||||
@ -18,32 +20,35 @@ const vCard = require('vcard-parser');
|
||||
})
|
||||
export class AccountDetailsComponent implements OnInit {
|
||||
transactionsDataSource: MatTableDataSource<any>;
|
||||
transactionsDisplayedColumns = ['sender', 'recipient', 'value', 'created', 'type'];
|
||||
transactionsDefaultPageSize = 10;
|
||||
transactionsPageSizeOptions = [10, 20, 50, 100];
|
||||
transactionsDisplayedColumns: Array<string> = ['sender', 'recipient', 'value', 'created', 'type'];
|
||||
transactionsDefaultPageSize: number = 10;
|
||||
transactionsPageSizeOptions: Array<number> = [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];
|
||||
userDisplayedColumns: Array<string> = ['name', 'phone', 'created', 'balance', 'location'];
|
||||
usersDefaultPageSize: number = 10;
|
||||
usersPageSizeOptions: Array<number> = [10, 20, 50, 100];
|
||||
@ViewChild('UserTablePaginator', {static: true}) userTablePaginator: MatPaginator;
|
||||
@ViewChild('UserTableSort', {static: true}) userTableSort: MatSort;
|
||||
|
||||
accountInfoForm: FormGroup;
|
||||
account: any;
|
||||
account: AccountDetails;
|
||||
accountAddress: string;
|
||||
accountBalance: number;
|
||||
accountStatus: any;
|
||||
metaAccount: any;
|
||||
accounts: any[] = [];
|
||||
accountsType = 'all';
|
||||
locations: any;
|
||||
accounts: Array<AccountDetails> = [];
|
||||
accountsType: string = 'all';
|
||||
categories: Array<Category>;
|
||||
areaNames: Array<AreaName>;
|
||||
areaTypes: Array<AreaType>;
|
||||
transaction: any;
|
||||
transactions: any[];
|
||||
transactionsType = 'all';
|
||||
matcher = new CustomErrorStateMatcher();
|
||||
transactions: Array<Transaction>;
|
||||
transactionsType: string = 'all';
|
||||
accountTypes: Array<string>;
|
||||
transactionsTypes: Array<string>;
|
||||
genders: Array<string>;
|
||||
matcher: CustomErrorStateMatcher = new CustomErrorStateMatcher();
|
||||
submitted: boolean = false;
|
||||
bloxbergLink: string;
|
||||
|
||||
@ -56,7 +61,9 @@ export class AccountDetailsComponent implements OnInit {
|
||||
private router: Router,
|
||||
private tokenService: TokenService,
|
||||
private loggingService: LoggingService,
|
||||
private blockSyncService: BlockSyncService
|
||||
private blockSyncService: BlockSyncService,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private snackBar: MatSnackBar,
|
||||
) {
|
||||
this.accountInfoForm = this.formBuilder.group({
|
||||
name: ['', Validators.required],
|
||||
@ -71,15 +78,15 @@ export class AccountDetailsComponent implements OnInit {
|
||||
locationType: ['', Validators.required],
|
||||
});
|
||||
this.route.paramMap.subscribe(async (params: Params) => {
|
||||
this.accountAddress = params.get('id');
|
||||
this.accountAddress = add0x(params.get('id'));
|
||||
this.bloxbergLink = 'https://blockexplorer.bloxberg.org/address/' + this.accountAddress + '/transactions';
|
||||
this.userService.getAccountDetailsFromMeta(await User.toKey(this.accountAddress)).pipe(first()).subscribe(async res => {
|
||||
this.metaAccount = Envelope.fromJSON(JSON.stringify(res.body)).unwrap();
|
||||
this.account = this.metaAccount.m.data;
|
||||
(await this.userService.getAccountByAddress(this.accountAddress, 100)).subscribe(async res => {
|
||||
if (res !== undefined) {
|
||||
this.account = res;
|
||||
this.cdr.detectChanges();
|
||||
this.loggingService.sendInfoLevelMessage(this.account);
|
||||
this.accountBalance = await this.tokenService.getTokenBalance(this.accountAddress);
|
||||
this.account.vcard = vCard.parse(atob(this.account.vcard));
|
||||
this.userService.getAccountStatus(this.account.vcard?.tel[0].value).pipe(first()).subscribe(response => this.accountStatus = response);
|
||||
// this.userService.getAccountStatus(this.account.vcard?.tel[0].value).pipe(first())
|
||||
// .subscribe(response => this.accountStatus = response);
|
||||
this.accountInfoForm.patchValue({
|
||||
name: this.account.vcard?.fn[0].value,
|
||||
phoneNumber: this.account.vcard?.tel[0].value,
|
||||
@ -92,14 +99,18 @@ export class AccountDetailsComponent implements OnInit {
|
||||
location: this.account.location.area,
|
||||
locationType: this.account.location.area_type,
|
||||
});
|
||||
} else {
|
||||
alert('Account not found!');
|
||||
}
|
||||
});
|
||||
this.blockSyncService.blockSync(this.accountAddress);
|
||||
});
|
||||
this.userService.getAccounts();
|
||||
this.locationService.getLocations();
|
||||
this.locationService.locationsSubject.subscribe(locations => {
|
||||
this.locations = locations;
|
||||
});
|
||||
this.userService.getCategories().pipe(first()).subscribe(res => this.categories = res);
|
||||
this.locationService.getAreaNames().pipe(first()).subscribe(res => this.areaNames = res);
|
||||
this.locationService.getAreaTypes().pipe(first()).subscribe(res => this.areaTypes = res);
|
||||
this.userService.getAccountTypes().pipe(first()).subscribe(res => this.accountTypes = res);
|
||||
this.userService.getTransactionTypes().pipe(first()).subscribe(res => this.transactionsTypes = res);
|
||||
this.userService.getGenders().pipe(first()).subscribe(res => this.genders = res);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -131,7 +142,7 @@ export class AccountDetailsComponent implements OnInit {
|
||||
}
|
||||
|
||||
viewAccount(account): void {
|
||||
this.router.navigateByUrl(`/accounts/${account.id}`);
|
||||
this.router.navigateByUrl(`/accounts/${strip0x(account.identities.evm[`bloxberg:${environment.bloxbergChainId}`][0])}`);
|
||||
}
|
||||
|
||||
get accountInfoFormStub(): any { return this.accountInfoForm.controls; }
|
||||
@ -140,7 +151,7 @@ export class AccountDetailsComponent implements OnInit {
|
||||
this.submitted = true;
|
||||
if (this.accountInfoForm.invalid || !confirm('Change user\'s profile information?')) { return; }
|
||||
const accountKey = await this.userService.changeAccountInfo(
|
||||
this.account.address,
|
||||
this.accountAddress,
|
||||
this.accountInfoFormStub.name.value,
|
||||
this.accountInfoFormStub.phoneNumber.value,
|
||||
this.accountInfoFormStub.age.value,
|
||||
@ -150,10 +161,8 @@ export class AccountDetailsComponent implements OnInit {
|
||||
this.accountInfoFormStub.businessCategory.value,
|
||||
this.accountInfoFormStub.userLocation.value,
|
||||
this.accountInfoFormStub.location.value,
|
||||
this.accountInfoFormStub.locationType.value,
|
||||
this.metaAccount
|
||||
this.accountInfoFormStub.locationType.value
|
||||
);
|
||||
this.loggingService.sendInfoLevelMessage(`Response: ${accountKey}`);
|
||||
this.submitted = false;
|
||||
}
|
||||
|
||||
@ -181,16 +190,18 @@ export class AccountDetailsComponent implements OnInit {
|
||||
|
||||
resetPin(): void {
|
||||
if (!confirm('Reset user\'s pin?')) { return; }
|
||||
this.userService.resetPin(this.account.phone).pipe(first()).subscribe(res => {
|
||||
this.userService.resetPin(this.account.vcard.tel[0].value).pipe(first()).subscribe(res => {
|
||||
this.loggingService.sendInfoLevelMessage(`Response: ${res}`);
|
||||
});
|
||||
}
|
||||
|
||||
public trackByName(index, item): string {
|
||||
return item.name;
|
||||
}
|
||||
|
||||
downloadCsv(data: any, filename: string): void {
|
||||
exportCsv(data, filename);
|
||||
}
|
||||
|
||||
copyAddress(): void {
|
||||
if (copyToClipboard(this.accountAddress)) {
|
||||
this.snackBar.open(this.accountAddress + ' copied successfully!', 'Close', { duration: 3000 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,59 @@
|
||||
<!-- 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">Search</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="card">
|
||||
<mat-card-title class="card-header">
|
||||
Accounts
|
||||
</mat-card-title>
|
||||
<div class="card-body">
|
||||
<mat-tab-group>
|
||||
<mat-tab label="Phone Number">
|
||||
<form [formGroup]="phoneSearchForm" (ngSubmit)="onPhoneSearch()">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label> Search </mat-label>
|
||||
<input matInput type="text" placeholder="Search by phone number" formControlName="phoneNumber" [errorStateMatcher]="matcher">
|
||||
<mat-error *ngIf="phoneSearchSubmitted && phoneSearchFormStub.phoneNumber.errors">Phone Number is required.</mat-error>
|
||||
<mat-icon matSuffix>phone</mat-icon>
|
||||
<mat-hint>Phone Number</mat-hint>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" type="submit" class="btn btn-outline-primary ml-3"> SEARCH </button>
|
||||
</form>
|
||||
</mat-tab>
|
||||
<mat-tab label="Account Address">
|
||||
<form [formGroup]="addressSearchForm" (ngSubmit)="onAddressSearch()">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label> Search </mat-label>
|
||||
<input matInput type="text" placeholder="Search by account address" formControlName="address" [errorStateMatcher]="matcher">
|
||||
<mat-error *ngIf="addressSearchSubmitted && addressSearchFormStub.address.errors">Account Address is required.</mat-error>
|
||||
<mat-icon matSuffix>view_in_ar</mat-icon>
|
||||
<mat-hint>Account Address</mat-hint>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" type="submit" class="btn btn-outline-primary ml-3"> SEARCH </button>
|
||||
</form>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<app-footer appMenuSelection></app-footer>
|
||||
</div>
|
||||
<!-- ============================================================== -->
|
||||
<!-- End Page content -->
|
||||
<!-- ============================================================== -->
|
||||
</div>
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AccountSearchComponent } from './account-search.component';
|
||||
|
||||
describe('AccountSearchComponent', () => {
|
||||
let component: AccountSearchComponent;
|
||||
let fixture: ComponentFixture<AccountSearchComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ AccountSearchComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AccountSearchComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,84 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
||||
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
|
||||
import {CustomErrorStateMatcher} from '@app/_helpers';
|
||||
import {UserService} from '@app/_services';
|
||||
import {Router} from '@angular/router';
|
||||
import {strip0x} from '@src/assets/js/ethtx/dist/hex';
|
||||
import {environment} from '@src/environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-account-search',
|
||||
templateUrl: './account-search.component.html',
|
||||
styleUrls: ['./account-search.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class AccountSearchComponent implements OnInit {
|
||||
nameSearchForm: FormGroup;
|
||||
nameSearchSubmitted: boolean = false;
|
||||
nameSearchLoading: boolean = false;
|
||||
phoneSearchForm: FormGroup;
|
||||
phoneSearchSubmitted: boolean = false;
|
||||
phoneSearchLoading: boolean = false;
|
||||
addressSearchForm: FormGroup;
|
||||
addressSearchSubmitted: boolean = false;
|
||||
addressSearchLoading: boolean = false;
|
||||
matcher: CustomErrorStateMatcher = new CustomErrorStateMatcher();
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private userService: UserService,
|
||||
private router: Router,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.nameSearchForm = this.formBuilder.group({
|
||||
name: ['', Validators.required],
|
||||
});
|
||||
this.phoneSearchForm = this.formBuilder.group({
|
||||
phoneNumber: ['', Validators.required],
|
||||
});
|
||||
this.addressSearchForm = this.formBuilder.group({
|
||||
address: ['', Validators.required],
|
||||
});
|
||||
}
|
||||
|
||||
get nameSearchFormStub(): any { return this.nameSearchForm.controls; }
|
||||
get phoneSearchFormStub(): any { return this.phoneSearchForm.controls; }
|
||||
get addressSearchFormStub(): any { return this.addressSearchForm.controls; }
|
||||
|
||||
onNameSearch(): void {
|
||||
this.nameSearchSubmitted = true;
|
||||
if (this.nameSearchForm.invalid) { return; }
|
||||
this.nameSearchLoading = true;
|
||||
this.userService.searchAccountByName(this.nameSearchFormStub.name.value);
|
||||
this.nameSearchLoading = false;
|
||||
}
|
||||
|
||||
async onPhoneSearch(): Promise<void> {
|
||||
this.phoneSearchSubmitted = true;
|
||||
if (this.phoneSearchForm.invalid) { return; }
|
||||
this.phoneSearchLoading = true;
|
||||
(await this.userService.getAccountByPhone(this.phoneSearchFormStub.phoneNumber.value, 100)).subscribe(async res => {
|
||||
if (res !== undefined) {
|
||||
await this.router.navigateByUrl(`/accounts/${strip0x(res.identities.evm[`bloxberg:${environment.bloxbergChainId}`][0])}`);
|
||||
} else {
|
||||
alert('Account not found!');
|
||||
}
|
||||
});
|
||||
this.phoneSearchLoading = false;
|
||||
}
|
||||
|
||||
async onAddressSearch(): Promise<void> {
|
||||
this.addressSearchSubmitted = true;
|
||||
if (this.addressSearchForm.invalid) { return; }
|
||||
this.addressSearchLoading = true;
|
||||
(await this.userService.getAccountByAddress(this.addressSearchFormStub.address.value, 100)).subscribe(async res => {
|
||||
if (res !== undefined) {
|
||||
await this.router.navigateByUrl(`/accounts/${strip0x(res.identities.evm[`bloxberg:${environment.bloxbergChainId}`][0])}`);
|
||||
} else {
|
||||
alert('Account not found!');
|
||||
}
|
||||
});
|
||||
this.addressSearchLoading = false;
|
||||
}
|
||||
}
|
@ -3,13 +3,13 @@ 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';
|
||||
import {AccountSearchComponent} from '@pages/accounts/account-search/account-search.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: AccountsComponent },
|
||||
{ path: 'search', component: AccountSearchComponent },
|
||||
// { path: 'create', component: CreateAccountComponent },
|
||||
{ path: 'export', component: ExportAccountsComponent },
|
||||
{ path: ':id', component: AccountDetailsComponent },
|
||||
{ path: '**', redirectTo: '', pathMatch: 'full' }
|
||||
];
|
||||
|
@ -26,14 +26,13 @@
|
||||
<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-option *ngFor="let accountType of accountTypes" [value]="accountType">
|
||||
{{accountType | uppercase}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" type="button" class="btn btn-outline-primary ml-auto mr-2" (click)="downloadCsv()"> EXPORT </button>
|
||||
<button mat-raised-button color="primary" type="button" class="btn btn-outline-primary ml-auto mr-2" routerLink="/accounts/search"> SEARCH </button>
|
||||
<button mat-raised-button color="primary" type="button" class="btn btn-outline-primary mr-2" (click)="downloadCsv()"> EXPORT </button>
|
||||
</div>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
|
@ -5,6 +5,10 @@ import {MatSort} from '@angular/material/sort';
|
||||
import {LoggingService, UserService} from '@app/_services';
|
||||
import {Router} from '@angular/router';
|
||||
import {exportCsv} from '@app/_helpers';
|
||||
import {strip0x} from '@src/assets/js/ethtx/dist/hex';
|
||||
import {first} from 'rxjs/operators';
|
||||
import {environment} from '@src/environments/environment';
|
||||
import {AccountDetails} from '@app/_models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-accounts',
|
||||
@ -14,11 +18,12 @@ import {exportCsv} from '@app/_helpers';
|
||||
})
|
||||
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';
|
||||
accounts: Array<AccountDetails> = [];
|
||||
displayedColumns: Array<string> = ['name', 'phone', 'created', 'balance', 'location'];
|
||||
defaultPageSize: number = 10;
|
||||
pageSizeOptions: Array<number> = [10, 20, 50, 100];
|
||||
accountsType: string = 'all';
|
||||
accountTypes: Array<string>;
|
||||
|
||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
@ -27,14 +32,17 @@ export class AccountsComponent implements OnInit {
|
||||
private userService: UserService,
|
||||
private loggingService: LoggingService,
|
||||
private router: Router
|
||||
) {
|
||||
)
|
||||
{
|
||||
(async () => {
|
||||
try {
|
||||
// TODO it feels like this should be in the onInit handler
|
||||
await this.userService.loadAccounts(100);
|
||||
} catch (error) {
|
||||
this.loggingService.sendErrorLevelMessage('Failed to load accounts', this, {error});
|
||||
}
|
||||
})();
|
||||
this.userService.getAccountTypes().pipe(first()).subscribe(res => this.accountTypes = res);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -51,7 +59,7 @@ export class AccountsComponent implements OnInit {
|
||||
}
|
||||
|
||||
async viewAccount(account): Promise<void> {
|
||||
await this.router.navigateByUrl(`/accounts/${account.identities.evm['bloxberg:8996']}`);
|
||||
await this.router.navigateByUrl(`/accounts/${strip0x(account.identities.evm[`bloxberg:${environment.bloxbergChainId}`][0])}`);
|
||||
}
|
||||
|
||||
filterAccounts(): void {
|
||||
|
@ -7,8 +7,6 @@ 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';
|
||||
@ -24,10 +22,17 @@ import {MatTabsModule} from '@angular/material/tabs';
|
||||
import {MatRippleModule} from '@angular/material/core';
|
||||
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
|
||||
import {ReactiveFormsModule} from '@angular/forms';
|
||||
import { AccountSearchComponent } from './account-search/account-search.component';
|
||||
import {MatSnackBarModule} from '@angular/material/snack-bar';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [AccountsComponent, AccountDetailsComponent, CreateAccountComponent, DisbursementComponent, ExportAccountsComponent],
|
||||
declarations: [
|
||||
AccountsComponent,
|
||||
AccountDetailsComponent,
|
||||
CreateAccountComponent,
|
||||
AccountSearchComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
AccountsRoutingModule,
|
||||
@ -47,7 +52,8 @@ import {ReactiveFormsModule} from '@angular/forms';
|
||||
MatTabsModule,
|
||||
MatRippleModule,
|
||||
MatProgressSpinnerModule,
|
||||
ReactiveFormsModule
|
||||
ReactiveFormsModule,
|
||||
MatSnackBarModule,
|
||||
]
|
||||
})
|
||||
export class AccountsModule { }
|
||||
|
@ -27,11 +27,9 @@
|
||||
<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-option *ngFor="let accountType of accountTypes" [value]="accountType">
|
||||
{{accountType | uppercase}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="submitted && createFormStub.accountType.errors">Account type is required.</mat-error>
|
||||
</mat-form-field><br>
|
||||
@ -81,15 +79,9 @@
|
||||
<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 *ngFor="let area of areaNames" [value]="area">
|
||||
{{area | uppercase}}
|
||||
</mat-option>
|
||||
</mat-optgroup>
|
||||
</div>
|
||||
</div>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="submitted && createFormStub.location.errors">Location is required.</mat-error>
|
||||
</mat-form-field><br>
|
||||
@ -99,9 +91,9 @@
|
||||
<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-option *ngFor="let gender of genders" [value]="gender">
|
||||
{{gender | uppercase}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="submitted && createFormStub.gender.errors">Gender is required.</mat-error>
|
||||
</mat-form-field><br>
|
||||
@ -119,16 +111,9 @@
|
||||
<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-option *ngFor="let category of categories" [value]="category">
|
||||
{{category | titlecase}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="submitted && createFormStub.businessCategory.errors">Business Category is required.</mat-error>
|
||||
</mat-form-field>
|
||||
|
@ -1,7 +1,9 @@
|
||||
import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core';
|
||||
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
|
||||
import {LocationService} from '@app/_services';
|
||||
import {LocationService, UserService} from '@app/_services';
|
||||
import {CustomErrorStateMatcher} from '@app/_helpers';
|
||||
import {first} from 'rxjs/operators';
|
||||
import {AreaName, Category} from '@app/_models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-account',
|
||||
@ -11,13 +13,17 @@ import {CustomErrorStateMatcher} from '@app/_helpers';
|
||||
})
|
||||
export class CreateAccountComponent implements OnInit {
|
||||
createForm: FormGroup;
|
||||
matcher = new CustomErrorStateMatcher();
|
||||
matcher: CustomErrorStateMatcher = new CustomErrorStateMatcher();
|
||||
submitted: boolean = false;
|
||||
locations: any;
|
||||
categories: Array<Category>;
|
||||
areaNames: Array<AreaName>;
|
||||
accountTypes: Array<string>;
|
||||
genders: Array<string>;
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private locationService: LocationService
|
||||
private locationService: LocationService,
|
||||
private userService: UserService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -33,10 +39,10 @@ export class CreateAccountComponent implements OnInit {
|
||||
referrer: ['', Validators.required],
|
||||
businessCategory: ['', Validators.required]
|
||||
});
|
||||
this.locationService.getLocations();
|
||||
this.locationService.locationsSubject.subscribe(locations => {
|
||||
this.locations = locations;
|
||||
});
|
||||
this.userService.getCategories().pipe(first()).subscribe(res => this.categories = res);
|
||||
this.locationService.getAreaNames().pipe(first()).subscribe(res => this.areaNames = res);
|
||||
this.userService.getAccountTypes().pipe(first()).subscribe(res => this.accountTypes = res);
|
||||
this.userService.getGenders().pipe(first()).subscribe(res => this.genders = res);
|
||||
}
|
||||
|
||||
get createFormStub(): any { return this.createForm.controls; }
|
||||
@ -46,8 +52,4 @@ export class CreateAccountComponent implements OnInit {
|
||||
if (this.createForm.invalid || !confirm('Create account?')) { return; }
|
||||
this.submitted = false;
|
||||
}
|
||||
|
||||
public trackByName(index, item): string {
|
||||
return item.name;
|
||||
}
|
||||
}
|
||||
|
@ -1,36 +0,0 @@
|
||||
<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>
|
@ -1,37 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
@ -1,51 +0,0 @@
|
||||
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 || !confirm('Make transfer?')) { 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();
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
<!-- 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>
|
@ -1,37 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
@ -1,34 +0,0 @@
|
||||
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 || !confirm('Export accounts?')) { return; }
|
||||
this.submitted = false;
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import {LoggingService, UserService} from '@app/_services';
|
||||
import {animate, state, style, transition, trigger} from '@angular/animations';
|
||||
import {first} from 'rxjs/operators';
|
||||
import {exportCsv} from '@app/_helpers';
|
||||
import {Action} from '../../_models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin',
|
||||
@ -22,9 +23,9 @@ import {exportCsv} from '@app/_helpers';
|
||||
})
|
||||
export class AdminComponent implements OnInit {
|
||||
dataSource: MatTableDataSource<any>;
|
||||
displayedColumns = ['expand', 'user', 'role', 'action', 'status', 'approve'];
|
||||
action: any;
|
||||
actions: any;
|
||||
displayedColumns: Array<string> = ['expand', 'user', 'role', 'action', 'status', 'approve'];
|
||||
action: Action;
|
||||
actions: Array<Action>;
|
||||
|
||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
@ -39,7 +40,6 @@ export class AdminComponent implements OnInit {
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
this.actions = actions;
|
||||
console.log(this.actions);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -15,97 +15,13 @@
|
||||
<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 class="embed-responsive embed-responsive-16by9">
|
||||
<iframe class="embed-responsive-item" [src]="url | safe" allow="fullscreen" loading="lazy"
|
||||
title="Community inclusion currencies dashboard" referrerpolicy="no-referrer">
|
||||
<p>
|
||||
<a href="{{url}}"> Your browser does not support iframes. </a>
|
||||
</p>
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
<app-footer appMenuSelection></app-footer>
|
||||
|
@ -1,8 +1,4 @@
|
||||
import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core';
|
||||
import {Color, Label} from 'ng2-charts';
|
||||
import {ChartDataSets, ChartOptions, ChartType} from 'chart.js';
|
||||
import {LocationService, LoggingService} from '@app/_services';
|
||||
import {ArraySum} from '@app/_helpers';
|
||||
import {ChangeDetectionStrategy, Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pages',
|
||||
@ -10,181 +6,8 @@ import {ArraySum} from '@app/_helpers';
|
||||
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'}
|
||||
];
|
||||
export class PagesComponent {
|
||||
url: string = 'https://dashboard.sarafu.network/';
|
||||
|
||||
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 locationService: LocationService,
|
||||
private loggingService: LoggingService
|
||||
) {
|
||||
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 {
|
||||
this.loggingService.sendInfoLevelMessage(`Event: ${event}, ${active}`);
|
||||
}
|
||||
|
||||
public chartHovered({ event, active }: { event: MouseEvent, active: {}[] }): void {
|
||||
this.loggingService.sendInfoLevelMessage(`Event: ${event}, ${active}`);
|
||||
}
|
||||
|
||||
public trackByName(index, item): string {
|
||||
return item.name;
|
||||
}
|
||||
constructor() { }
|
||||
}
|
||||
|
@ -1,51 +0,0 @@
|
||||
<!-- 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>
|
@ -1,37 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
@ -1,34 +0,0 @@
|
||||
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 || !confirm('Invite user?')) { return; }
|
||||
this.submitted = false;
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ import {CustomErrorStateMatcher} from '@app/_helpers';
|
||||
export class OrganizationComponent implements OnInit {
|
||||
organizationForm: FormGroup;
|
||||
submitted: boolean = false;
|
||||
matcher = new CustomErrorStateMatcher();
|
||||
matcher: CustomErrorStateMatcher = new CustomErrorStateMatcher();
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder
|
||||
|
@ -2,12 +2,10 @@ import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { SettingsComponent } from '@pages/settings/settings.component';
|
||||
import {InviteComponent} from '@pages/settings/invite/invite.component';
|
||||
import {OrganizationComponent} from '@pages/settings/organization/organization.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: SettingsComponent },
|
||||
{ path: 'invite', component: InviteComponent },
|
||||
{ path: 'organization', component: OrganizationComponent },
|
||||
{ path: '**', redirectTo: '', pathMatch: 'full' }
|
||||
];
|
||||
|
@ -15,8 +15,8 @@ import {exportCsv} from '@app/_helpers';
|
||||
export class SettingsComponent implements OnInit {
|
||||
date: string;
|
||||
dataSource: MatTableDataSource<any>;
|
||||
displayedColumns = ['name', 'email', 'userId'];
|
||||
trustedUsers: Staff[];
|
||||
displayedColumns: Array<string> = ['name', 'email', 'userId'];
|
||||
trustedUsers: Array<Staff>;
|
||||
|
||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
|
@ -4,7 +4,6 @@ import { CommonModule } from '@angular/common';
|
||||
import { SettingsRoutingModule } from '@pages/settings/settings-routing.module';
|
||||
import { SettingsComponent } from '@pages/settings/settings.component';
|
||||
import {SharedModule} from '@app/shared/shared.module';
|
||||
import { InviteComponent } from '@pages/settings/invite/invite.component';
|
||||
import { OrganizationComponent } from '@pages/settings/organization/organization.component';
|
||||
import {MatTableModule} from '@angular/material/table';
|
||||
import {MatSortModule} from '@angular/material/sort';
|
||||
@ -22,7 +21,7 @@ import {ReactiveFormsModule} from '@angular/forms';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [SettingsComponent, InviteComponent, OrganizationComponent],
|
||||
declarations: [SettingsComponent, OrganizationComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
SettingsRoutingModule,
|
||||
|
@ -2,6 +2,7 @@ import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core';
|
||||
import {ActivatedRoute, Params} from '@angular/router';
|
||||
import {TokenService} from '@app/_services';
|
||||
import {first} from 'rxjs/operators';
|
||||
import {Token} from '../../../_models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-token-details',
|
||||
@ -10,7 +11,7 @@ import {first} from 'rxjs/operators';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class TokenDetailsComponent implements OnInit {
|
||||
token: any;
|
||||
token: Token;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
|
@ -5,6 +5,8 @@ import {LoggingService, TokenService} from '@app/_services';
|
||||
import {MatTableDataSource} from '@angular/material/table';
|
||||
import {Router} from '@angular/router';
|
||||
import {exportCsv} from '@app/_helpers';
|
||||
import {TokenRegistry} from '../../_eth';
|
||||
import {Token} from '../../_models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tokens',
|
||||
@ -14,10 +16,10 @@ import {exportCsv} from '@app/_helpers';
|
||||
})
|
||||
export class TokensComponent implements OnInit {
|
||||
dataSource: MatTableDataSource<any>;
|
||||
columnsToDisplay = ['name', 'symbol', 'address', 'supply'];
|
||||
columnsToDisplay: Array<string> = ['name', 'symbol', 'address', 'supply'];
|
||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
tokens: any;
|
||||
tokens: Array<Promise<string>>;
|
||||
|
||||
constructor(
|
||||
private tokenService: TokenService,
|
||||
@ -26,13 +28,14 @@ export class TokensComponent implements OnInit {
|
||||
) { }
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.tokenService.LoadEvent.subscribe(async () => {
|
||||
this.tokens = await this.tokenService.getTokens();
|
||||
});
|
||||
this.tokens = await this.tokenService.getTokens();
|
||||
this.loggingService.sendInfoLevelMessage(this.tokens);
|
||||
this.tokenService.tokensSubject.subscribe(tokens => {
|
||||
this.dataSource = new MatTableDataSource(tokens);
|
||||
this.dataSource = new MatTableDataSource(this.tokens);
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
});
|
||||
}
|
||||
|
||||
doFilter(value: string): void {
|
||||
|
@ -13,12 +13,20 @@
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<span>Sender: {{transaction.sender?.vcard.fn[0].value}}</span><br><br>
|
||||
<span>Sender Address: <a href="{{senderBloxbergLink}}" target="_blank"> {{transaction.from}} </a></span><br><br>
|
||||
<span>
|
||||
Sender Address:
|
||||
<a href="{{senderBloxbergLink}}" target="_blank"> {{transaction.from}} </a>
|
||||
<img src="assets/images/checklist.svg" class="ml-2" height="20rem" (click)="copyAddress(transaction.from)" alt="Copy">
|
||||
</span><br><br>
|
||||
<button mat-raised-button color="primary" class="btn btn-outline-info" (click)="viewSender()">View Sender</button>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span>Recipient: {{transaction.recipient?.vcard.fn[0].value}}</span><br><br>
|
||||
<span>Recipient Address: <a href="{{recipientBloxbergLink}}" target="_blank"> {{transaction.to}} </a></span><br><br>
|
||||
<span>
|
||||
Recipient Address:
|
||||
<a href="{{recipientBloxbergLink}}" target="_blank"> {{transaction.to}} </a>
|
||||
<img src="assets/images/checklist.svg" class="ml-2" height="20rem" (click)="copyAddress(transaction.to)" alt="Copy">
|
||||
</span><br><br>
|
||||
<button mat-raised-button color="primary" class="btn btn-outline-info" (click)="viewRecipient()">View Recipient</button>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
@ -28,7 +36,11 @@
|
||||
<h4 class="mt-2">Token: </h4>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<span>Address: {{transaction.token._address}}</span>
|
||||
<span>
|
||||
Address:
|
||||
{{transaction.token._address}}
|
||||
<img src="assets/images/checklist.svg" class="ml-2" height="20rem" (click)="copyAddress(transaction.token._address)" alt="Copy">
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span>Name: Sarafu Token</span>
|
||||
@ -73,17 +85,25 @@
|
||||
<span><strong>Trader: {{transaction.sender?.vcard.fn[0].value}}</strong></span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span>Trader Address: {{transaction.trader}}</span>
|
||||
<span>
|
||||
Trader Address:
|
||||
<a href="{{traderBloxbergLink}}" target="_blank"> {{transaction.trader}} </a>
|
||||
<img src="assets/images/checklist.svg" class="ml-2" height="20rem" (click)="copyAddress(transaction.trader)" alt="Copy">
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<button mat-raised-button color="primary" class="btn btn-outline-info" routerLink="/accounts/1">View Trader</button>
|
||||
<button mat-raised-button color="primary" class="btn btn-outline-info" (click)="viewTrader()">View Trader</button>
|
||||
<br><br>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h4>Source Token: </h4>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<span>Address: {{transaction.sourceToken.address}}</span>
|
||||
<span>
|
||||
Address:
|
||||
{{transaction.sourceToken.address}}
|
||||
<img src="assets/images/checklist.svg" class="ml-2" height="20rem" (click)="copyAddress(transaction.sourceToken.address)" alt="Copy">
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span>Name: {{transaction.sourceToken.name}}</span>
|
||||
@ -100,7 +120,11 @@
|
||||
<h4>Destination Token: </h4>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<span>Address: {{transaction.destinationToken.address}}</span>
|
||||
<span>
|
||||
Address:
|
||||
{{transaction.destinationToken.address}}
|
||||
<img src="assets/images/checklist.svg" class="ml-2" height="20rem" (click)="copyAddress(transaction.destinationToken.address)" alt="Copy">
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span>Name: {{transaction.destinationToken.name}}</span>
|
||||
|
@ -1,6 +1,9 @@
|
||||
import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core';
|
||||
import {Router} from '@angular/router';
|
||||
import {TransactionService} from '@app/_services';
|
||||
import {copyToClipboard} from '@app/_helpers';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
import {strip0x} from '@src/assets/js/ethtx/dist/hex';
|
||||
|
||||
@Component({
|
||||
selector: 'app-transaction-details',
|
||||
@ -12,23 +15,33 @@ export class TransactionDetailsComponent implements OnInit {
|
||||
@Input() transaction;
|
||||
senderBloxbergLink: string;
|
||||
recipientBloxbergLink: string;
|
||||
traderBloxbergLink: string;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private transactionService: TransactionService
|
||||
private transactionService: TransactionService,
|
||||
private snackBar: MatSnackBar,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.transaction?.type === 'conversion') {
|
||||
this.traderBloxbergLink = 'https://blockexplorer.bloxberg.org/address/' + this.transaction?.trader + '/transactions';
|
||||
} else {
|
||||
this.senderBloxbergLink = 'https://blockexplorer.bloxberg.org/address/' + this.transaction?.from + '/transactions';
|
||||
this.recipientBloxbergLink = 'https://blockexplorer.bloxberg.org/address/' + this.transaction?.to + '/transactions';
|
||||
}
|
||||
}
|
||||
|
||||
async viewSender(): Promise<void> {
|
||||
await this.router.navigateByUrl(`/accounts/${this.transaction.from}`);
|
||||
await this.router.navigateByUrl(`/accounts/${strip0x(this.transaction.from)}`);
|
||||
}
|
||||
|
||||
async viewRecipient(): Promise<void> {
|
||||
await this.router.navigateByUrl(`/accounts/${this.transaction.to}`);
|
||||
await this.router.navigateByUrl(`/accounts/${strip0x(this.transaction.to)}`);
|
||||
}
|
||||
|
||||
async viewTrader(): Promise<void> {
|
||||
await this.router.navigateByUrl(`/accounts/${strip0x(this.transaction.trader)}`);
|
||||
}
|
||||
|
||||
async reverseTransaction(): Promise<void> {
|
||||
@ -39,4 +52,10 @@ export class TransactionDetailsComponent implements OnInit {
|
||||
this.transaction.value
|
||||
);
|
||||
}
|
||||
|
||||
copyAddress(address: string): void {
|
||||
if (copyToClipboard(address)) {
|
||||
this.snackBar.open(address + ' copied successfully!', 'Close', { duration: 3000 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,11 +29,9 @@
|
||||
<mat-label> TRANSFER TYPE </mat-label>
|
||||
<mat-select id="typeSelect" [(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-option *ngFor="let transactionType of transactionsTypes" [value]="transactionType">
|
||||
{{transactionType | uppercase}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" type="button" class="btn btn-outline-primary ml-auto mr-2" (click)="downloadCsv()"> EXPORT </button>
|
||||
|
@ -1,9 +1,11 @@
|
||||
import {AfterViewInit, ChangeDetectionStrategy, Component, OnInit, ViewChild} from '@angular/core';
|
||||
import {BlockSyncService, TransactionService} from '@app/_services';
|
||||
import {BlockSyncService, TransactionService, UserService} from '@app/_services';
|
||||
import {MatTableDataSource} from '@angular/material/table';
|
||||
import {MatPaginator} from '@angular/material/paginator';
|
||||
import {MatSort} from '@angular/material/sort';
|
||||
import {exportCsv} from '@app/_helpers';
|
||||
import {first} from 'rxjs/operators';
|
||||
import {Transaction} from '@app/_models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-transactions',
|
||||
@ -13,12 +15,13 @@ import {exportCsv} from '@app/_helpers';
|
||||
})
|
||||
export class TransactionsComponent implements OnInit, AfterViewInit {
|
||||
transactionDataSource: MatTableDataSource<any>;
|
||||
transactionDisplayedColumns = ['sender', 'recipient', 'value', 'created', 'type'];
|
||||
defaultPageSize = 10;
|
||||
pageSizeOptions = [10, 20, 50, 100];
|
||||
transactions: any[];
|
||||
transaction: any;
|
||||
transactionsType = 'all';
|
||||
transactionDisplayedColumns: Array<string> = ['sender', 'recipient', 'value', 'created', 'type'];
|
||||
defaultPageSize: number = 10;
|
||||
pageSizeOptions: Array<number> = [10, 20, 50, 100];
|
||||
transactions: Array<Transaction>;
|
||||
transaction: Transaction;
|
||||
transactionsType: string = 'all';
|
||||
transactionsTypes: Array<string>;
|
||||
|
||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
@ -26,6 +29,7 @@ export class TransactionsComponent implements OnInit, AfterViewInit {
|
||||
constructor(
|
||||
private blockSyncService: BlockSyncService,
|
||||
private transactionService: TransactionService,
|
||||
private userService: UserService
|
||||
) {
|
||||
this.blockSyncService.blockSync();
|
||||
}
|
||||
@ -37,6 +41,7 @@ export class TransactionsComponent implements OnInit, AfterViewInit {
|
||||
this.transactionDataSource.sort = this.sort;
|
||||
this.transactions = transactions;
|
||||
});
|
||||
this.userService.getTransactionTypes().pipe(first()).subscribe(res => this.transactionsTypes = res);
|
||||
}
|
||||
|
||||
viewTransaction(transaction): void {
|
||||
|
@ -17,6 +17,7 @@ import {MatIconModule} from '@angular/material/icon';
|
||||
import {MatSelectModule} from '@angular/material/select';
|
||||
import {MatCardModule} from '@angular/material/card';
|
||||
import {MatRippleModule} from '@angular/material/core';
|
||||
import {MatSnackBarModule} from '@angular/material/snack-bar';
|
||||
|
||||
|
||||
@NgModule({
|
||||
@ -39,7 +40,8 @@ import {MatRippleModule} from '@angular/material/core';
|
||||
MatIconModule,
|
||||
MatSelectModule,
|
||||
MatCardModule,
|
||||
MatRippleModule
|
||||
MatRippleModule,
|
||||
MatSnackBarModule,
|
||||
]
|
||||
})
|
||||
export class TransactionsModule { }
|
||||
|
@ -18,15 +18,15 @@ export class MenuSelectionDirective {
|
||||
}
|
||||
|
||||
onMenuSelect(): void {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const sidebar: HTMLElement = document.getElementById('sidebar');
|
||||
if (!sidebar?.classList.contains('active')) {
|
||||
sidebar?.classList.add('active');
|
||||
}
|
||||
const content = document.getElementById('content');
|
||||
const content: HTMLElement = document.getElementById('content');
|
||||
if (!content?.classList.contains('active')) {
|
||||
content?.classList.add('active');
|
||||
}
|
||||
const sidebarCollapse = document.getElementById('sidebarCollapse');
|
||||
const sidebarCollapse: HTMLElement = document.getElementById('sidebarCollapse');
|
||||
if (sidebarCollapse?.classList.contains('active')) {
|
||||
sidebarCollapse?.classList.remove('active');
|
||||
}
|
||||
|
@ -16,11 +16,11 @@ export class MenuToggleDirective {
|
||||
|
||||
// Menu Trigger
|
||||
onMenuToggle(): void {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const sidebar: HTMLElement = document.getElementById('sidebar');
|
||||
sidebar?.classList.toggle('active');
|
||||
const content = document.getElementById('content');
|
||||
const content: HTMLElement = document.getElementById('content');
|
||||
content?.classList.toggle('active');
|
||||
const sidebarCollapse = document.getElementById('sidebarCollapse');
|
||||
const sidebarCollapse: HTMLElement = document.getElementById('sidebarCollapse');
|
||||
sidebarCollapse?.classList.toggle('active');
|
||||
}
|
||||
}
|
||||
|
8
src/app/shared/_pipes/safe.pipe.spec.ts
Normal file
8
src/app/shared/_pipes/safe.pipe.spec.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { SafePipe } from './safe.pipe';
|
||||
|
||||
describe('SafePipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new SafePipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
});
|
15
src/app/shared/_pipes/safe.pipe.ts
Normal file
15
src/app/shared/_pipes/safe.pipe.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import {DomSanitizer} from '@angular/platform-browser';
|
||||
|
||||
@Pipe({
|
||||
name: 'safe'
|
||||
})
|
||||
export class SafePipe implements PipeTransform {
|
||||
|
||||
constructor(private sanitizer: DomSanitizer) {}
|
||||
|
||||
transform(url: string, ...args: unknown[]): unknown {
|
||||
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
|
||||
}
|
||||
|
||||
}
|
@ -8,8 +8,9 @@ import { MenuToggleDirective } from '@app/shared/_directives/menu-toggle.directi
|
||||
import {RouterModule} from '@angular/router';
|
||||
import {MatIconModule} from '@angular/material/icon';
|
||||
import {TokenRatioPipe} from '@app/shared/_pipes/token-ratio.pipe';
|
||||
import { ErrorDialogComponent } from './error-dialog/error-dialog.component';
|
||||
import { ErrorDialogComponent } from '@app/shared/error-dialog/error-dialog.component';
|
||||
import {MatDialogModule} from '@angular/material/dialog';
|
||||
import { SafePipe } from '@app/shared/_pipes/safe.pipe';
|
||||
import { NetworkStatusComponent } from './network-status/network-status.component';
|
||||
|
||||
|
||||
@ -23,6 +24,7 @@ import { NetworkStatusComponent } from './network-status/network-status.componen
|
||||
MenuToggleDirective,
|
||||
TokenRatioPipe,
|
||||
ErrorDialogComponent,
|
||||
SafePipe,
|
||||
NetworkStatusComponent
|
||||
],
|
||||
exports: [
|
||||
@ -31,6 +33,7 @@ import { NetworkStatusComponent } from './network-status/network-status.componen
|
||||
SidebarComponent,
|
||||
MenuSelectionDirective,
|
||||
TokenRatioPipe,
|
||||
SafePipe,
|
||||
NetworkStatusComponent
|
||||
],
|
||||
imports: [
|
||||
|
1
src/assets/images/checklist.svg
Normal file
1
src/assets/images/checklist.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 7.8 KiB |
@ -0,0 +1 @@
|
||||
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"}],"name":"addressOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_idx","type":"uint256"}],"name":"entry","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"entryCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"register","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"registry","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes4","name":"_interfaceCode","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"}]
|
@ -11,6 +11,6 @@ export const environment = {
|
||||
cicCacheUrl: 'https://cache.dev.grassrootseconomics.net',
|
||||
web3Provider: 'wss://bloxberg-ws.dev.grassrootseconomics.net',
|
||||
cicUssdUrl: 'https://ussd.dev.grassrootseconomics.net',
|
||||
registryAddress: '0xAf1B487491073C2d49136Db3FD87E293302CF839',
|
||||
registryAddress: '0xea6225212005e86a4490018ded4bf37f3e772161',
|
||||
trustedDeclaratorAddress: '0xEb3907eCad74a0013c259D5874AE7f22DcBcC95C'
|
||||
};
|
||||
|
271
src/styles.scss
271
src/styles.scss
@ -1,18 +1,23 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
.declaration-order {
|
||||
/* Positioning */
|
||||
/* Box-model */
|
||||
/* Typography */
|
||||
/* Visual */
|
||||
/* Misc */
|
||||
}
|
||||
@import "~bootstrap/dist/css/bootstrap.css";
|
||||
@import "https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap";
|
||||
|
||||
html, body { height: 100%; }
|
||||
html,
|
||||
body { height: 100%; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Roboto, 'Roboto', "Helvetica Neue", sans-serif;
|
||||
font-family: Roboto, 'Roboto', sans-serif;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.full-width, table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bg-dark {
|
||||
background: #313a46;
|
||||
}
|
||||
@ -22,16 +27,20 @@ p {
|
||||
font-size: 1.1em;
|
||||
font-weight: 300;
|
||||
line-height: 1.7em;
|
||||
color: #000000;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
a, a:hover, a:focus {
|
||||
color: inherit;
|
||||
a,
|
||||
a:hover,
|
||||
a:focus {
|
||||
text-decoration: none;
|
||||
transition: all 0.3s;
|
||||
color: inherit;
|
||||
transition: all .3s;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
perspective: 1500px;
|
||||
align-items: stretch;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: stretch;
|
||||
@ -39,18 +48,34 @@ a, a:hover, a:focus {
|
||||
perspective: 1500px;
|
||||
}
|
||||
|
||||
ul ul a {
|
||||
padding-left: 30px;
|
||||
font-size: .9em;
|
||||
background: #6d7fcc;
|
||||
}
|
||||
|
||||
.full-width,
|
||||
table { width: 100%; }
|
||||
|
||||
li.breadcrumb-item.active,
|
||||
footer.footer { color: black; }
|
||||
|
||||
.clipboard {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
min-width: 250px;
|
||||
max-width: 250px;
|
||||
height: 100vh;
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 9999;
|
||||
min-width: 250px;
|
||||
max-width: 250px;
|
||||
height: 100vh;
|
||||
background: #313a46;
|
||||
color: #fff;
|
||||
transition: all 0.3s cubic-bezier(0.945, 0.020, 0.270, 0.665);
|
||||
transform-origin: center left;
|
||||
}
|
||||
|
||||
@ -58,7 +83,6 @@ a, a:hover, a:focus {
|
||||
min-width: 100px;
|
||||
max-width: 100px;
|
||||
text-align: center;
|
||||
transition: all 0.3s cubic-bezier(0.945, 0.020, 0.270, 0.665);
|
||||
}
|
||||
|
||||
#sidebar .sidebar-header {
|
||||
@ -66,55 +90,44 @@ a, a:hover, a:focus {
|
||||
background: #313a46;
|
||||
}
|
||||
|
||||
#sidebar .sidebar-header strong {
|
||||
display: none;
|
||||
}
|
||||
#sidebar .sidebar-header strong,
|
||||
#sidebar.active .sidebar-header h3 { display: none; }
|
||||
|
||||
#sidebar.active .sidebar-header h3 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#sidebar.active .sidebar-header strong {
|
||||
display: block;
|
||||
}
|
||||
#sidebar.active .sidebar-header strong { display: block; }
|
||||
|
||||
#sidebar ul li a {
|
||||
color: #ffffff;
|
||||
display: block;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
font-size: 1.1em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#sidebar ul li a.active {
|
||||
background: #000000;
|
||||
}
|
||||
#sidebar ul li a.active,
|
||||
#sidebar.active ul li a.active { background: #000; }
|
||||
|
||||
#sidebar.active ul li a {
|
||||
padding: 20px 10px;
|
||||
font-size: .85em;
|
||||
text-align: center;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
#sidebar.active ul li a.active {
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
#sidebar.active ul li a i {
|
||||
margin-right: 0;
|
||||
display: block;
|
||||
font-size: 1.8em;
|
||||
margin-right: 0;
|
||||
margin-bottom: 5px;
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
#sidebar.active ul li a {
|
||||
padding: 10px !important;
|
||||
}
|
||||
#sidebar.active ul li a { padding: 10px; }
|
||||
|
||||
#sidebar.active .dropdown-toggle::after {
|
||||
top: auto;
|
||||
bottom: 10px;
|
||||
right: 50%;
|
||||
-webkit-transform: translateX(50%);
|
||||
-ms-transform: translateX(50%);
|
||||
transform: translateX(50%);
|
||||
-ms-transform: translateX(50%);
|
||||
-webkit-transform: translateX(50%);
|
||||
}
|
||||
|
||||
#sidebar ul.components {
|
||||
@ -123,14 +136,8 @@ a, a:hover, a:focus {
|
||||
}
|
||||
|
||||
#sidebar ul p {
|
||||
padding: 10px;
|
||||
color: #fff;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#sidebar ul li a {
|
||||
padding: 10px;
|
||||
font-size: 1.1em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#sidebar ul li a:hover {
|
||||
@ -138,43 +145,32 @@ a, a:hover, a:focus {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#sidebar ul li.active > a, a[aria-expanded="true"] {
|
||||
#sidebar ul li.active > a,
|
||||
a[aria-expanded="true"] {
|
||||
color: #fff;
|
||||
background: #313a46;
|
||||
}
|
||||
|
||||
ul ul a {
|
||||
font-size: 0.9em !important;
|
||||
padding-left: 30px !important;
|
||||
}
|
||||
|
||||
li.breadcrumb-item.active, footer.footer {
|
||||
color: black;
|
||||
}
|
||||
|
||||
a[data-toggle="collapse"] {
|
||||
position: relative;
|
||||
}
|
||||
a[data-toggle="collapse"] { position: relative; }
|
||||
|
||||
.dropdown-toggle::after {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 20%;
|
||||
display: block;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
#content {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
transition: all 0.3s cubic-bezier(0.945, 0.020, 0.270, 0.665);
|
||||
}
|
||||
|
||||
#content.active {
|
||||
transition: all 0.3s cubic-bezier(0.945, 0.020, 0.270, 0.665);
|
||||
}
|
||||
#sidebar,
|
||||
#sidebar.active,
|
||||
#content,
|
||||
#content.active { transition: all .3s cubic-bezier(.945, .020, .270, .665); }
|
||||
|
||||
#sidebarCollapse {
|
||||
width: 40px;
|
||||
@ -183,84 +179,57 @@ a[data-toggle="collapse"] {
|
||||
}
|
||||
|
||||
#sidebarCollapse span {
|
||||
display: block;
|
||||
width: 80%;
|
||||
height: 2px;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
background: #555;
|
||||
transition: all 0.8s cubic-bezier(0.810, -0.330, 0.345, 1.375);
|
||||
transition: all .8s cubic-bezier(.810, -.330, .345, 1.375);
|
||||
}
|
||||
|
||||
#sidebarCollapse span:first-of-type {
|
||||
/* rotate first one */
|
||||
transform: rotate(45deg) translate(2px, 2px);
|
||||
}
|
||||
#sidebarCollapse span:first-of-type { transform: rotate(45deg) translate(2px, 2px); }
|
||||
|
||||
#sidebarCollapse span:nth-of-type(2) {
|
||||
/* second one is not visible */
|
||||
opacity: 0;
|
||||
}
|
||||
#sidebarCollapse span:nth-of-type(2) { opacity: 0; }
|
||||
|
||||
#sidebarCollapse span:last-of-type {
|
||||
/* rotate third one */
|
||||
transform: rotate(-45deg) translate(1px, -1px);
|
||||
}
|
||||
#sidebarCollapse span:last-of-type { transform: rotate(-45deg) translate(1px, -1px); }
|
||||
|
||||
#sidebarCollapse.active span {
|
||||
/* no rotation */
|
||||
transform: none;
|
||||
/* all bars are visible */
|
||||
opacity: 1;
|
||||
margin: 5px auto;
|
||||
transform: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.footer {
|
||||
color: #98a6ad;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
color: #98a6ad;
|
||||
}
|
||||
|
||||
.mat-column-select {
|
||||
overflow: initial;
|
||||
}
|
||||
.mat-column-select { overflow: initial; }
|
||||
|
||||
button {
|
||||
height: 2.5rem;
|
||||
}
|
||||
button { height: 2.5rem; }
|
||||
|
||||
.badge-pill {
|
||||
width: 5rem;
|
||||
}
|
||||
.badge-pill { width: 5rem; }
|
||||
|
||||
.mat-column {
|
||||
word-wrap: break-word !important;
|
||||
white-space: unset !important;
|
||||
word-wrap: break-word;
|
||||
white-space: unset;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
hyphens: auto;
|
||||
-ms-hyphens: auto;
|
||||
-moz-hyphens: auto;
|
||||
-webkit-hyphens: auto;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.mat-column-address {
|
||||
flex: 0 0 30% !important;
|
||||
width: 30% !important;
|
||||
flex: 0 0 30%;
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.mat-column-supply {
|
||||
flex: 0 0 25% !important;
|
||||
width: 25% !important;
|
||||
}
|
||||
|
||||
.mat-column-select {
|
||||
flex: 0 0 10% !important;
|
||||
width: 10% !important;
|
||||
}
|
||||
|
||||
.mat-column-view {
|
||||
flex: 0 0 5% !important;
|
||||
width: 5% !important;
|
||||
flex: 0 0 25%;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.center-body {
|
||||
@ -269,92 +238,70 @@ button {
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#sidebar {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#sidebar.active {
|
||||
min-width: 100px;
|
||||
max-width: 100px;
|
||||
margin-left: -100px;
|
||||
text-align: center;
|
||||
margin-left: -100px !important;
|
||||
}
|
||||
|
||||
#sidebar .sidebar-header strong {
|
||||
display: none;
|
||||
}
|
||||
#sidebar .sidebar-header strong,
|
||||
#sidebar.active .sidebar-header h3 { display: none; }
|
||||
|
||||
#sidebar.active .sidebar-header h3 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#sidebar.active .sidebar-header strong {
|
||||
display: block;
|
||||
}
|
||||
#sidebar.active .sidebar-header strong,
|
||||
#sidebar.active ul li a i { display: block; }
|
||||
|
||||
#sidebar.active ul li a {
|
||||
padding: 20px 10px;
|
||||
font-size: 0.85em;
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
#sidebar.active ul li a i {
|
||||
margin-right: 0;
|
||||
display: block;
|
||||
font-size: 1.8em;
|
||||
margin-bottom: 5px;
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
#sidebar.active ul ul a {
|
||||
padding: 10px !important;
|
||||
}
|
||||
#sidebar.active ul ul a { padding: 10px; }
|
||||
|
||||
.dropdown-toggle::after {
|
||||
top: auto;
|
||||
bottom: 10px;
|
||||
right: 50%;
|
||||
-webkit-transform: translateX(50%);
|
||||
-ms-transform: translateX(50%);
|
||||
transform: translateX(50%);
|
||||
-ms-transform: translateX(50%);
|
||||
-webkit-transform: translateX(50%);
|
||||
}
|
||||
|
||||
#content, #content.active {
|
||||
height: 100vh;
|
||||
#content,
|
||||
#content.active {
|
||||
position: fixed;
|
||||
margin-left: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#content .menutoggle {
|
||||
margin-left: 250px;
|
||||
transition: all 0.3s cubic-bezier(0.945, 0.020, 0.270, 0.665);
|
||||
}
|
||||
#content .menutoggle { margin-left: 250px; }
|
||||
|
||||
#content.active .menutoggle {
|
||||
margin-left: 0;
|
||||
transition: all 0.3s cubic-bezier(0.945, 0.020, 0.270, 0.665);
|
||||
}
|
||||
#sidebar,
|
||||
#content,
|
||||
#content.active,
|
||||
#content.active .menutoggle { margin-left: 0; }
|
||||
|
||||
#content .menutoggle,
|
||||
#content.active .menutoggle { transition: all .3s cubic-bezier(.945, .020, .270, .665); }
|
||||
|
||||
#sidebarCollapse span:first-of-type,
|
||||
#sidebarCollapse span:nth-of-type(2),
|
||||
#sidebarCollapse span:last-of-type {
|
||||
margin: 5px auto;
|
||||
transform: none;
|
||||
opacity: 1;
|
||||
margin: 5px auto;
|
||||
}
|
||||
|
||||
#sidebarCollapse.active span {
|
||||
margin: 0 auto;
|
||||
}
|
||||
#sidebarCollapse.active span { margin: 0 auto; }
|
||||
|
||||
#sidebarCollapse.active span:first-of-type {
|
||||
transform: rotate(45deg) translate(2px, 2px);
|
||||
}
|
||||
#sidebarCollapse.active span:first-of-type { transform: rotate(45deg) translate(2px, 2px); }
|
||||
|
||||
#sidebarCollapse.active span:nth-of-type(2) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#sidebarCollapse.active span:last-of-type {
|
||||
transform: rotate(-45deg) translate(1px, -1px);
|
||||
}
|
||||
#sidebarCollapse.active span:nth-of-type(2) { opacity: 0; }
|
||||
|
||||
#sidebarCollapse.active span:last-of-type { transform: rotate(-45deg) translate(1px, -1px); }
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user