Merge branch 'master' into spencer/pwa

# Conflicts:
#	package-lock.json
#	src/app/shared/shared.module.ts
#	src/styles.scss
This commit is contained in:
Spencer Ofwiti 2021-05-11 18:28:54 +03:00
commit d4d2837d2e
93 changed files with 1710 additions and 22524 deletions

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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;
}
}

View File

@ -1,3 +1,2 @@
export * from '@app/_eth/accountIndex';
export * from '@app/_eth/registry';
export * from '@app/_eth/token-registry';

View File

@ -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();
});
});

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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
};

View 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
};

View File

@ -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));
}
}

View File

@ -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;
};
}

View File

@ -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';

View File

@ -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;
}

View File

@ -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();

View File

@ -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';

View File

@ -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];
}
}

View File

@ -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(','));
});

View 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,
};

View File

@ -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.

View File

@ -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);
}

View File

@ -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);
// }));
}
}

View File

@ -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
};

View File

@ -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';

View 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
};

View File

@ -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
};

View File

@ -1,7 +1,11 @@
export interface Staff {
interface Staff {
comment: string;
email: string;
name: string;
tag: number;
userid: string;
}
export {
Staff
};

View File

@ -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
};

View File

@ -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
};

View File

@ -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[];
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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
});

View File

@ -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());
}
}

View File

@ -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);
}
}

View 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();
});
});

View 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;
}
}

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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`);
}
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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 {

View File

@ -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>

View File

@ -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');
});
});

View File

@ -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 });
}
}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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;
}
}

View File

@ -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' }
];

View File

@ -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">

View File

@ -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 {

View File

@ -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 { }

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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;
}
}

View File

@ -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);
});
}

View File

@ -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>

View File

@ -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() { }
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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;
}
}

View File

@ -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

View File

@ -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' }
];

View File

@ -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;

View File

@ -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,

View File

@ -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,

View File

@ -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 {

View File

@ -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>

View File

@ -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 });
}
}
}

View File

@ -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>

View File

@ -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 {

View File

@ -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 { }

View File

@ -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');
}

View File

@ -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');
}
}

View File

@ -0,0 +1,8 @@
import { SafePipe } from './safe.pipe';
describe('SafePipe', () => {
it('create an instance', () => {
const pipe = new SafePipe();
expect(pipe).toBeTruthy();
});
});

View 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);
}
}

View File

@ -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: [

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -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"}]

View File

@ -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'
};

View File

@ -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); }
}