@@ -0,0 +1,30 @@
|
||||
<mat-card class="card">
|
||||
|
||||
<mat-card-header class="header">
|
||||
<h3 fxFlex="100%">
|
||||
<fa-icon icon="donate"></fa-icon>
|
||||
Amount Collected
|
||||
<div>
|
||||
<mat-form-field id="office" fxFlex="94%">
|
||||
<mat-label>Office</mat-label>
|
||||
<mat-select [formControl]="officeId">
|
||||
<mat-option *ngFor="let office of officeData" [value]="office.id">
|
||||
{{ office.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</h3>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
<!-- Structural directives like *ngIf break the canvas -->
|
||||
<div [ngStyle]="{'display': (hideOutput) ? 'none' : 'block'}">
|
||||
<canvas id="collection-pie" width="800" height="600"></canvas>
|
||||
</div>
|
||||
<div *ngIf="showFallback" class="fallback" fxLayoutAlign="center center">
|
||||
<p>No Data</p>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
|
||||
</mat-card>
|
||||
@@ -0,0 +1,12 @@
|
||||
.card {
|
||||
width: 27.5rem;
|
||||
padding: 0;
|
||||
.header {
|
||||
#office{
|
||||
margin-bottom: -1.20em;
|
||||
}
|
||||
}
|
||||
.fallback {
|
||||
height: 20rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AmountCollectedPieComponent } from './amount-collected-pie.component';
|
||||
|
||||
describe('AmountCollectedPieComponent', () => {
|
||||
let component: AmountCollectedPieComponent;
|
||||
let fixture: ComponentFixture<AmountCollectedPieComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AmountCollectedPieComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AmountCollectedPieComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
/** Angular Imports */
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
/** Custom Services */
|
||||
import { HomeService } from '../../home.service';
|
||||
|
||||
/** Charting Imports */
|
||||
import Chart from 'chart.js';
|
||||
|
||||
/**
|
||||
* Amount Collected Pie Chart Component
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-amount-collected-pie',
|
||||
templateUrl: './amount-collected-pie.component.html',
|
||||
styleUrls: ['./amount-collected-pie.component.scss']
|
||||
})
|
||||
export class AmountCollectedPieComponent implements OnInit {
|
||||
|
||||
/** Static Form control for office Id */
|
||||
officeId = new FormControl();
|
||||
/** Office Data */
|
||||
officeData: any;
|
||||
/** Chart.js chart */
|
||||
chart: any;
|
||||
/** Substitute for resolver */
|
||||
hideOutput = true;
|
||||
/** Shows fallback element */
|
||||
showFallback = false;
|
||||
|
||||
/**
|
||||
* Fetches offices data from `resolve`.
|
||||
* @param {HomeService} homeService Home Service.
|
||||
* @param {ActivatedRoute} route Activated Route.
|
||||
*/
|
||||
constructor(private homeService: HomeService,
|
||||
private route: ActivatedRoute) {
|
||||
this.route.data.subscribe( (data: { offices: any }) => {
|
||||
this.officeData = data.offices;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the pie chart
|
||||
* Initialize with office Id 1 for better UX.
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.getChartData();
|
||||
this.officeId.patchValue(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to value changes of office Id fetches chart data accordingly.
|
||||
*/
|
||||
getChartData() {
|
||||
this.officeId.valueChanges.subscribe((value: number) => {
|
||||
this.homeService.getCollectedAmount(value).subscribe((response: any) => {
|
||||
const data = Object.entries(response[0]).map(entry => entry[1]);
|
||||
if (!(data[0] === 0 && data[1] === 0)) {
|
||||
this.setChart(data);
|
||||
this.hideOutput = false;
|
||||
this.showFallback = false;
|
||||
} else {
|
||||
this.showFallback = true;
|
||||
this.hideOutput = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of Chart.js pie chart
|
||||
* Refer: https://www.chartjs.org/docs/latest/charts/doughnut.html for configuration details.
|
||||
* @param {any} data Chart Data.
|
||||
*/
|
||||
setChart(data: any) {
|
||||
if (!this.chart) {
|
||||
this.chart = new Chart('collection-pie', {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: ['Pending', 'Collected'],
|
||||
datasets: [{
|
||||
backgroundColor: ['red', 'green'],
|
||||
data: data
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
layout: {
|
||||
padding: {
|
||||
top: 10,
|
||||
bottom: 15
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.chart.data.datasets[0].data = data;
|
||||
this.chart.update();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<mat-card class="card">
|
||||
|
||||
<mat-card-header class="header">
|
||||
<h3 fxFlex="100%">
|
||||
<fa-icon icon="money-bill"></fa-icon>
|
||||
Amount Disbursed
|
||||
<div>
|
||||
<mat-form-field id="office" fxFlex="94%">
|
||||
<mat-label>Office</mat-label>
|
||||
<mat-select [formControl]="officeId">
|
||||
<mat-option *ngFor="let office of officeData" [value]="office.id">
|
||||
{{ office.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</h3>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
<!-- Structural directives like *ngIf break the canvas -->
|
||||
<div [ngStyle]="{'display': (hideOutput) ? 'none' : 'block'}">
|
||||
<canvas id="disbursement-pie" width="800" height="600"></canvas>
|
||||
</div>
|
||||
<div *ngIf="showFallback" class="fallback" fxLayoutAlign="center center">
|
||||
<p>No Data</p>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
|
||||
</mat-card>
|
||||
@@ -0,0 +1,12 @@
|
||||
.card {
|
||||
width: 27.5rem;
|
||||
padding: 0;
|
||||
.header {
|
||||
#office{
|
||||
margin-bottom: -1.20em;
|
||||
}
|
||||
}
|
||||
.fallback {
|
||||
height: 20rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AmountDisbursedPieComponent } from './amount-disbursed-pie.component';
|
||||
|
||||
describe('AmountDisbursedPieComponent', () => {
|
||||
let component: AmountDisbursedPieComponent;
|
||||
let fixture: ComponentFixture<AmountDisbursedPieComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AmountDisbursedPieComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AmountDisbursedPieComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
/** Angular Imports */
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
/** Custom Services */
|
||||
import { HomeService } from '../../home.service';
|
||||
|
||||
/** Charting Imports */
|
||||
import Chart from 'chart.js';
|
||||
|
||||
/**
|
||||
* Amount Disbursed Pie Chart Component
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-amount-disbursed-pie',
|
||||
templateUrl: './amount-disbursed-pie.component.html',
|
||||
styleUrls: ['./amount-disbursed-pie.component.scss']
|
||||
})
|
||||
export class AmountDisbursedPieComponent implements OnInit {
|
||||
|
||||
/** Static Form control for office Id */
|
||||
officeId = new FormControl();
|
||||
/** Office Data */
|
||||
officeData: any;
|
||||
/** Chart.js chart */
|
||||
chart: any;
|
||||
/** Substitute for resolver */
|
||||
hideOutput = true;
|
||||
/** Shows fallback element */
|
||||
showFallback = false;
|
||||
|
||||
/**
|
||||
* Fetches offices data from `resolve`.
|
||||
* @param {HomeService} homeService Home Service.
|
||||
* @param {ActivatedRoute} route Activated Route.
|
||||
*/
|
||||
constructor(private homeService: HomeService,
|
||||
private route: ActivatedRoute) {
|
||||
this.route.data.subscribe( (data: { offices: any }) => {
|
||||
this.officeData = data.offices;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the pie chart with initial office Id 1.
|
||||
* Initialize with office Id 1 for better UX.
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.getChartData();
|
||||
this.officeId.patchValue(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to value changes of office Id fetches chart data accordingly.
|
||||
*/
|
||||
getChartData() {
|
||||
this.officeId.valueChanges.subscribe((value: number) => {
|
||||
this.homeService.getDisbursedAmount(value).subscribe((response: any) => {
|
||||
const data = Object.entries(response[0]).map(entry => entry[1]);
|
||||
if (!(data[0] === 0 && data[1] === 0)) {
|
||||
this.setChart(data);
|
||||
this.showFallback = false;
|
||||
this.hideOutput = false;
|
||||
} else {
|
||||
this.showFallback = true;
|
||||
this.hideOutput = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of Chart.js pie chart
|
||||
* Refer: https://www.chartjs.org/docs/latest/charts/doughnut.html for configuration details.
|
||||
* @param {any} data Chart Data.
|
||||
*/
|
||||
setChart(data: any) {
|
||||
if (!this.chart) {
|
||||
this.chart = new Chart('disbursement-pie', {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: ['Pending', 'Disbursed'],
|
||||
datasets: [{
|
||||
backgroundColor: ['red', 'yellow'],
|
||||
data: data
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
layout: {
|
||||
padding: {
|
||||
top: 10,
|
||||
bottom: 15
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.chart.data.datasets[0].data = data;
|
||||
this.chart.update();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<mat-card class="card">
|
||||
|
||||
<mat-card-header class="header">
|
||||
<h3 fxFlex="100%">
|
||||
<fa-icon icon="chart-line"></fa-icon>
|
||||
Client Trends
|
||||
<div>
|
||||
<mat-form-field id="office" fxFlex="94%">
|
||||
<mat-label>Office</mat-label>
|
||||
<mat-select [formControl]="officeId">
|
||||
<mat-option *ngFor="let office of officeData" [value]="office.id">
|
||||
{{ office.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</h3>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
<!-- Structural directives like *ngIf break the canvas -->
|
||||
<div [ngStyle]="{'display': (hideOutput) ? 'none' : 'block'}">
|
||||
<canvas id="client-trends-bar" width="800" height="465"></canvas>
|
||||
</div>
|
||||
<mat-button-toggle-group [formControl]="timescale" id='timescale' appearance="legacy">
|
||||
<mat-button-toggle value="Day">Day</mat-button-toggle>
|
||||
<mat-button-toggle value="Week">Week</mat-button-toggle>
|
||||
<mat-button-toggle value="Month">Month</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</mat-card-content>
|
||||
|
||||
</mat-card>
|
||||
@@ -0,0 +1,13 @@
|
||||
.card {
|
||||
width: 41.25rem;
|
||||
padding: 0;
|
||||
.header {
|
||||
#office{
|
||||
margin-bottom: -1.20em;
|
||||
}
|
||||
}
|
||||
#timescale {
|
||||
margin-block: 2%;
|
||||
margin-inline: 35%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ClientTrendsBarComponent } from './client-trends-bar.component';
|
||||
|
||||
describe('ClientTrendsBarComponent', () => {
|
||||
let component: ClientTrendsBarComponent;
|
||||
let fixture: ComponentFixture<ClientTrendsBarComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ClientTrendsBarComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ClientTrendsBarComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,255 @@
|
||||
/** Angular Imports */
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { DatePipe } from '@angular/common';
|
||||
|
||||
/** rxjs Imports */
|
||||
import { merge, forkJoin } from 'rxjs';
|
||||
import { skip } from 'rxjs/operators';
|
||||
|
||||
/** Custom Services */
|
||||
import { HomeService } from '../../home.service';
|
||||
|
||||
/** Charting Imports */
|
||||
import Chart from 'chart.js';
|
||||
|
||||
/**
|
||||
* Client Trends Bar Chart Component.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-client-trends-bar',
|
||||
templateUrl: './client-trends-bar.component.html',
|
||||
styleUrls: ['./client-trends-bar.component.scss']
|
||||
})
|
||||
export class ClientTrendsBarComponent implements OnInit {
|
||||
|
||||
/** Static Form control for office Id */
|
||||
officeId = new FormControl();
|
||||
/** Static Form control for time scale */
|
||||
timescale = new FormControl();
|
||||
/** Office Data */
|
||||
officeData: any;
|
||||
/** Chart.js chart */
|
||||
chart: any;
|
||||
/** Substitute for resolver */
|
||||
hideOutput = true;
|
||||
|
||||
/**
|
||||
* Fetches offices data from `resolve`
|
||||
* @param {HomeService} homeService Home Service
|
||||
* @param {ActivatedRoute} route Activated Route
|
||||
* @param {DatePipe} datePipe Date Pipe
|
||||
*/
|
||||
constructor(private homeService: HomeService,
|
||||
private route: ActivatedRoute,
|
||||
private datePipe: DatePipe) {
|
||||
this.route.data.subscribe( (data: { offices: any }) => {
|
||||
this.officeData = data.offices;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.getChartData();
|
||||
this.initializeControls();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the form controls for better UX.
|
||||
*/
|
||||
initializeControls() {
|
||||
this.officeId.patchValue(1);
|
||||
this.timescale.patchValue('Day');
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to value changes of officeID and timescale controls,
|
||||
* Fetches data accordingly and sets charts based on fetched data.
|
||||
*/
|
||||
getChartData() {
|
||||
merge(this.officeId.valueChanges, this.timescale.valueChanges).pipe(skip(1))
|
||||
.subscribe(() => {
|
||||
const officeId = this.officeId.value;
|
||||
const timescale = this.timescale.value;
|
||||
switch (timescale) {
|
||||
case 'Day':
|
||||
const clientsByDay = this.homeService.getClientTrendsByDay(officeId);
|
||||
const loansByDay = this.homeService.getLoanTrendsByDay(officeId);
|
||||
forkJoin([clientsByDay, loansByDay]).subscribe((data: any[]) => {
|
||||
const dayLabels = this.getLabels(timescale);
|
||||
const clientCounts = this.getCounts(data[0], dayLabels, timescale, 'client');
|
||||
const loanCounts = this.getCounts(data[1], dayLabels, timescale, 'loan');
|
||||
this.setChart(dayLabels, clientCounts, loanCounts);
|
||||
this.hideOutput = false;
|
||||
});
|
||||
break;
|
||||
case 'Week':
|
||||
const clientsByWeek = this.homeService.getClientTrendsByWeek(officeId);
|
||||
const loansByWeek = this.homeService.getLoanTrendsByWeek(officeId);
|
||||
forkJoin([clientsByWeek, loansByWeek]).subscribe((data: any[]) => {
|
||||
const weekLabels = this.getLabels(timescale);
|
||||
const clientCounts = this.getCounts(data[0], weekLabels, timescale, 'client');
|
||||
const loanCounts = this.getCounts(data[1], weekLabels, timescale, 'loan');
|
||||
this.setChart(weekLabels, clientCounts, loanCounts);
|
||||
this.hideOutput = false;
|
||||
});
|
||||
break;
|
||||
case 'Month':
|
||||
const clientsByMonth = this.homeService.getClientTrendsByMonth(officeId);
|
||||
const loansByMonth = this.homeService.getLoanTrendsByMonth(officeId);
|
||||
forkJoin([clientsByMonth, loansByMonth]).subscribe((data: any[]) => {
|
||||
const monthLabels = this.getLabels(timescale);
|
||||
const clientCounts = this.getCounts(data[0], monthLabels, timescale, 'client');
|
||||
const loanCounts = this.getCounts(data[1], monthLabels, timescale, 'loan');
|
||||
this.setChart(monthLabels, clientCounts, loanCounts);
|
||||
this.hideOutput = false;
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets Abscissa Labels.
|
||||
* @param {string} timescale User's timescale choice.
|
||||
*/
|
||||
getLabels(timescale: string) {
|
||||
const date = new Date();
|
||||
const labelsArray = [];
|
||||
switch (timescale) {
|
||||
case 'Day':
|
||||
while (labelsArray.length < 12) {
|
||||
date.setDate(date.getDate() - 1);
|
||||
const transformedDate = this.datePipe.transform(date, 'd/M');
|
||||
labelsArray.push(transformedDate);
|
||||
}
|
||||
break;
|
||||
case 'Week':
|
||||
/** 1st January of present year */
|
||||
const onejan = new Date(date.getFullYear(), 0, 1);
|
||||
while (labelsArray.length < 12) {
|
||||
date.setDate(date.getDate() - 7);
|
||||
/** Gets current week number */
|
||||
const weekNumber = Math.ceil(
|
||||
(((date.getTime() - onejan.getTime()) / 86400000) + onejan.getDay() + 1) / 7
|
||||
);
|
||||
labelsArray.push(weekNumber);
|
||||
}
|
||||
break;
|
||||
case 'Month':
|
||||
while (labelsArray.length < 12) {
|
||||
const transformedDate = this.datePipe.transform(date, 'MMMM');
|
||||
labelsArray.push(transformedDate);
|
||||
date.setMonth(date.getMonth() - 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return labelsArray.reverse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bar heights for clients/loans trends.
|
||||
* @param {any[]} response API response array.
|
||||
* @param {any[]} labels Abscissa Labels.
|
||||
* @param {string} timescale User's timescale choice.
|
||||
* @param {string} type 'client' or 'loan'.
|
||||
*/
|
||||
getCounts(response: any[], labels: any[], timescale: string, type: string) {
|
||||
let counts: number[] = [];
|
||||
switch (timescale) {
|
||||
case 'Day':
|
||||
labels.forEach((label: any) => {
|
||||
const day = response.find((entry: any) => {
|
||||
const transformedDate = this.datePipe.transform(entry.days, 'd/M');
|
||||
return transformedDate === label;
|
||||
});
|
||||
counts = this.updateCount(day, counts, type);
|
||||
});
|
||||
break;
|
||||
case 'Week':
|
||||
labels.forEach((label: any) => {
|
||||
const week = response.find((entry: any) => {
|
||||
return entry.Weeks === label;
|
||||
});
|
||||
counts = this.updateCount(week, counts, type);
|
||||
});
|
||||
break;
|
||||
case 'Month':
|
||||
labels.forEach((label: any) => {
|
||||
const month = response.find((entry: any) => {
|
||||
return entry.Months === label;
|
||||
});
|
||||
counts = this.updateCount(month, counts, type);
|
||||
});
|
||||
break;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the counts array.
|
||||
* @param {any} span Time span.
|
||||
* @param {any[]} counts Counts.
|
||||
* @param {string} type 'client' or 'loan'
|
||||
*/
|
||||
updateCount(span: any, counts: any[], type: string) {
|
||||
if (span) {
|
||||
switch (type) {
|
||||
case 'client':
|
||||
counts.push(span.count);
|
||||
break;
|
||||
case 'loan':
|
||||
counts.push(span.lcount);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
counts.push(0);
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of Chart.js multi-bar chart.
|
||||
* Refer: https://www.chartjs.org/docs/latest/charts/bar.html for configuration details.
|
||||
* @param {any[]} labels Abscissa Labels.
|
||||
* @param {number[]} clientCounts Clients Ordinate.
|
||||
* @param {number[]} loanCounts Loans Ordinate.
|
||||
*/
|
||||
setChart(labels: any[], clientCounts: number[], loanCounts: number[]) {
|
||||
if (!this.chart) {
|
||||
this.chart = new Chart('client-trends-bar', {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'New Clients',
|
||||
backgroundColor: 'dodgerblue',
|
||||
data: clientCounts
|
||||
},
|
||||
{
|
||||
label: 'Loans Disbursed',
|
||||
backgroundColor: 'green',
|
||||
data: loanCounts
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
layout: {
|
||||
padding: {
|
||||
top: 5,
|
||||
left: 10,
|
||||
right: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.chart.data.labels = labels;
|
||||
this.chart.data.datasets[0].data = clientCounts;
|
||||
this.chart.data.datasets[1].data = loanCounts;
|
||||
this.chart.update();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
@import "~@angular/material/theming";
|
||||
|
||||
@mixin dashboard-component-theme($theme) {
|
||||
$primary: map-get($theme, primary);
|
||||
|
||||
mifosx-dashboard {
|
||||
|
||||
.recent-activities, .frequent-activities {
|
||||
.header {
|
||||
background-color: mat-color($primary, 500);
|
||||
color: mat-contrast($primary, 500);
|
||||
}
|
||||
}
|
||||
|
||||
mifosx-amount-collected-pie,
|
||||
mifosx-amount-disbursed-pie,
|
||||
mifosx-client-trends-bar {
|
||||
.card {
|
||||
.header {
|
||||
background-color: mat-color($primary, 500);
|
||||
color: mat-contrast($primary, 500);
|
||||
mat-label,
|
||||
.mat-select-value,
|
||||
.mat-select-arrow {
|
||||
color: mat-contrast($primary, 500);
|
||||
}
|
||||
.mat-form-field-underline,
|
||||
.mat-form-field-ripple {
|
||||
background-color: mat-contrast($primary, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,53 @@
|
||||
<div class="container">
|
||||
|
||||
<p>
|
||||
TODO: dashboard component.
|
||||
</p>
|
||||
<mat-card class="card" fxLayout="column" fxLayoutGap="40px">
|
||||
|
||||
<div fxLayout="row" fxLayoutGap="40px">
|
||||
|
||||
<mat-card class="recent-activities">
|
||||
|
||||
<mat-card-header class="header">
|
||||
<h3><fa-icon icon="bolt"></fa-icon>
|
||||
Recent Activities</h3>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
<mat-action-list class="list">
|
||||
<button mat-list-item *ngFor="let activity of recentActivities"
|
||||
[routerLink]="activity"> {{ activity | urlToString }} </button>
|
||||
</mat-action-list>
|
||||
</mat-card-content>
|
||||
|
||||
</mat-card>
|
||||
|
||||
<mat-card class="frequent-activities">
|
||||
|
||||
<mat-card-header class="header">
|
||||
<h3><fa-icon icon="laptop"></fa-icon>
|
||||
Frequent Activities</h3>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
<mat-action-list class="list">
|
||||
<button mat-list-item *ngFor="let activity of frequentActivities"
|
||||
[routerLink]="activity"> {{ activity | urlToString }} </button>
|
||||
</mat-action-list>
|
||||
</mat-card-content>
|
||||
|
||||
</mat-card>
|
||||
|
||||
<mifosx-amount-disbursed-pie></mifosx-amount-disbursed-pie>
|
||||
|
||||
</div>
|
||||
|
||||
<div fxLayout="row" fxLayoutGap="40px">
|
||||
|
||||
<mifosx-client-trends-bar></mifosx-client-trends-bar>
|
||||
|
||||
<mifosx-amount-collected-pie></mifosx-amount-collected-pie>
|
||||
|
||||
</div>
|
||||
|
||||
</mat-card>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.card {
|
||||
padding: 2%;
|
||||
.recent-activities, .frequent-activities {
|
||||
width: 20rem;
|
||||
padding: 0;
|
||||
.list {
|
||||
padding-top: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,60 @@ import { Component, OnInit } from '@angular/core';
|
||||
})
|
||||
export class DashboardComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
/** Array of all user activities */
|
||||
userActivity: string[];
|
||||
/** Array of most recent user activities */
|
||||
recentActivities: string[];
|
||||
/** Array of most frequent user activities */
|
||||
frequentActivities: string[];
|
||||
|
||||
/**
|
||||
* Gets user activities from local storage.
|
||||
*/
|
||||
constructor() {
|
||||
this.userActivity = JSON.parse(localStorage.getItem('mifosXLocation'));
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.recentActivities = this.getRecentActivities();
|
||||
this.frequentActivities = this.getFrequentActivities();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns top eight recent activities.
|
||||
*/
|
||||
getRecentActivities() {
|
||||
const reverseActivities = this.userActivity.reverse();
|
||||
const uniqueActivities: string[] = [];
|
||||
reverseActivities.forEach((activity: string) => {
|
||||
if (!uniqueActivities.includes(activity)) {
|
||||
uniqueActivities.push(activity);
|
||||
}
|
||||
});
|
||||
const topEightRecentActivities =
|
||||
uniqueActivities
|
||||
.filter((activity: string) => !['/', '/login', '/home', '/dashboard'].includes(activity))
|
||||
.slice(0, 8);
|
||||
return topEightRecentActivities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns top eight frequent activities.
|
||||
*/
|
||||
getFrequentActivities() {
|
||||
const frequencyCounts: any = {};
|
||||
let index = this.userActivity.length;
|
||||
while (index) {
|
||||
frequencyCounts[this.userActivity[--index]] = (frequencyCounts[this.userActivity[index]] || 0) + 1;
|
||||
}
|
||||
const frequencyCountsArray = Object.entries(frequencyCounts);
|
||||
const topEigthFrequentActivities =
|
||||
frequencyCountsArray
|
||||
.sort((a: any, b: any) => b[1] - a[1])
|
||||
.map((entry: any[]) => entry[0])
|
||||
.filter((activity: string) => !['/', '/login', '/home', '/dashboard'].includes(activity))
|
||||
.slice(0, 8);
|
||||
return topEigthFrequentActivities;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ import { extract } from '../core/i18n/i18n.service';
|
||||
import { HomeComponent } from './home.component';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
|
||||
/** Custom Resolvers */
|
||||
import { OfficesResolver } from '../accounting/common-resolvers/offices.resolver';
|
||||
|
||||
/** Home and Dashboard Routes */
|
||||
const routes: Routes = [
|
||||
Route.withShell([
|
||||
@@ -28,7 +31,10 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'dashboard',
|
||||
component: DashboardComponent,
|
||||
data: { title: extract('Dashboard'), breadcrumb: 'Dashboard' }
|
||||
data: { title: extract('Dashboard'), breadcrumb: 'Dashboard' },
|
||||
resolve: {
|
||||
offices: OfficesResolver
|
||||
}
|
||||
}
|
||||
])
|
||||
];
|
||||
@@ -41,6 +47,6 @@ const routes: Routes = [
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
providers: []
|
||||
providers: [OfficesResolver]
|
||||
})
|
||||
export class HomeRoutingModule { }
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
/** Angular Imports */
|
||||
import { NgModule } from '@angular/core';
|
||||
import { DatePipe } from '@angular/common';
|
||||
|
||||
/** Custom Modules */
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { HomeRoutingModule } from './home-routing.module';
|
||||
import { PipesModule } from '../pipes/pipes.module';
|
||||
|
||||
/** Custom Components */
|
||||
import { HomeComponent } from './home.component';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { AmountCollectedPieComponent } from './dashboard/amount-collected-pie/amount-collected-pie.component';
|
||||
import { AmountDisbursedPieComponent } from './dashboard/amount-disbursed-pie/amount-disbursed-pie.component';
|
||||
import { ClientTrendsBarComponent } from './dashboard/client-trends-bar/client-trends-bar.component';
|
||||
|
||||
/**
|
||||
* Home Component
|
||||
@@ -17,11 +22,18 @@ import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
@NgModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
PipesModule,
|
||||
HomeRoutingModule
|
||||
],
|
||||
declarations: [
|
||||
HomeComponent,
|
||||
DashboardComponent
|
||||
DashboardComponent,
|
||||
AmountCollectedPieComponent,
|
||||
AmountDisbursedPieComponent,
|
||||
ClientTrendsBarComponent
|
||||
],
|
||||
providers: [
|
||||
DatePipe
|
||||
]
|
||||
})
|
||||
export class HomeModule { }
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HomeService } from './home.service';
|
||||
|
||||
describe('HomeService', () => {
|
||||
beforeEach(() => TestBed.configureTestingModule({}));
|
||||
|
||||
it('should be created', () => {
|
||||
const service: HomeService = TestBed.get(HomeService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
/** Angular Imports */
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
|
||||
/** rxjs Imports */
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Home Service
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class HomeService {
|
||||
|
||||
/**
|
||||
* @param {HttpClient} http Http Client to send requests.
|
||||
*/
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
/**
|
||||
* @param {number} officeId Office Id.
|
||||
* @returns {Observable<any>}
|
||||
*/
|
||||
getCollectedAmount(officeId: number): Observable<any> {
|
||||
const httpParams = new HttpParams()
|
||||
.set('R_officeId', officeId.toString())
|
||||
.set('genericResultSet', 'false');
|
||||
return this.http.get('/runreports/Demand_Vs_Collection', { params: httpParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} officeId Office Id.
|
||||
* @returns {Observable<any>}
|
||||
*/
|
||||
getDisbursedAmount(officeId: number): Observable<any> {
|
||||
const httpParams = new HttpParams()
|
||||
.set('R_officeId', officeId.toString())
|
||||
.set('genericResultSet', 'false');
|
||||
return this.http.get('/runreports/Disbursal_Vs_Awaitingdisbursal', { params: httpParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} officeId Office Id.
|
||||
* @returns {Observable<any>}
|
||||
*/
|
||||
getClientTrendsByDay(officeId: number): Observable<any> {
|
||||
const httpParams = new HttpParams()
|
||||
.set('R_officeId', officeId.toString())
|
||||
.set('genericResultSet', 'false');
|
||||
return this.http.get('/runreports/ClientTrendsByDay', { params: httpParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} officeId Office Id.
|
||||
* @returns {Observable<any>}
|
||||
*/
|
||||
getClientTrendsByWeek(officeId: number): Observable<any> {
|
||||
const httpParams = new HttpParams()
|
||||
.set('R_officeId', officeId.toString())
|
||||
.set('genericResultSet', 'false');
|
||||
return this.http.get('/runreports/ClientTrendsByWeek', { params: httpParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} officeId Office Id.
|
||||
* @returns {Observable<any>}
|
||||
*/
|
||||
getClientTrendsByMonth(officeId: number): Observable<any> {
|
||||
const httpParams = new HttpParams()
|
||||
.set('R_officeId', officeId.toString())
|
||||
.set('genericResultSet', 'false');
|
||||
return this.http.get('/runreports/ClientTrendsByMonth', { params: httpParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} officeId Office Id.
|
||||
* @returns {Observable<any>}
|
||||
*/
|
||||
getLoanTrendsByDay(officeId: number): Observable<any> {
|
||||
const httpParams = new HttpParams()
|
||||
.set('R_officeId', officeId.toString())
|
||||
.set('genericResultSet', 'false');
|
||||
return this.http.get('/runreports/LoanTrendsByDay', { params: httpParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} officeId Office Id.
|
||||
* @returns {Observable<any>}
|
||||
*/
|
||||
getLoanTrendsByWeek(officeId: number): Observable<any> {
|
||||
const httpParams = new HttpParams()
|
||||
.set('R_officeId', officeId.toString())
|
||||
.set('genericResultSet', 'false');
|
||||
return this.http.get('/runreports/LoanTrendsByWeek', { params: httpParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} officeId Office Id.
|
||||
* @returns {Observable<any>}
|
||||
*/
|
||||
getLoanTrendsByMonth(officeId: number): Observable<any> {
|
||||
const httpParams = new HttpParams()
|
||||
.set('R_officeId', officeId.toString())
|
||||
.set('genericResultSet', 'false');
|
||||
return this.http.get('/runreports/LoanTrendsByMonth', { params: httpParams });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,13 +5,14 @@ import { AccountsFilterPipe } from './accounts-filter.pipe';
|
||||
import { ChargesFilterPipe } from './charges-filter.pipe';
|
||||
import { ChargesPenaltyFilterPipe } from './charges-penalty-filter.pipe';
|
||||
import { FindPipe } from './find.pipe';
|
||||
import { UrlToStringPipe } from './url-to-string.pipe';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule
|
||||
],
|
||||
declarations: [StatusLookupPipe, AccountsFilterPipe, ChargesFilterPipe, ChargesPenaltyFilterPipe, FindPipe],
|
||||
providers: [StatusLookupPipe, AccountsFilterPipe, ChargesFilterPipe, ChargesPenaltyFilterPipe, FindPipe],
|
||||
exports: [StatusLookupPipe, AccountsFilterPipe, ChargesFilterPipe, ChargesPenaltyFilterPipe, FindPipe]
|
||||
declarations: [StatusLookupPipe, AccountsFilterPipe, ChargesFilterPipe, ChargesPenaltyFilterPipe, FindPipe, UrlToStringPipe],
|
||||
providers: [StatusLookupPipe, AccountsFilterPipe, ChargesFilterPipe, ChargesPenaltyFilterPipe, FindPipe, UrlToStringPipe],
|
||||
exports: [StatusLookupPipe, AccountsFilterPipe, ChargesFilterPipe, ChargesPenaltyFilterPipe, FindPipe, UrlToStringPipe]
|
||||
})
|
||||
export class PipesModule { }
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { UrlToStringPipe } from './url-to-string.pipe';
|
||||
|
||||
describe('UrlToStringPipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new UrlToStringPipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
/** Angular Imports */
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
/**
|
||||
* URL to String pipe.
|
||||
* Transform eg: `self-service/users` :: `Self Service | Users`
|
||||
*/
|
||||
@Pipe({
|
||||
name: 'urlToString'
|
||||
})
|
||||
export class UrlToStringPipe implements PipeTransform {
|
||||
|
||||
transform(url: string): any {
|
||||
const urlSubstrings: string[] = url.slice(1).split('/');
|
||||
const stringRepresentation =
|
||||
urlSubstrings
|
||||
.map((path: string) => {
|
||||
return path.split('-')
|
||||
.map((str: string) => {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
})
|
||||
.join(' ');
|
||||
})
|
||||
.join(' | ');
|
||||
return stringRepresentation;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -83,7 +83,12 @@ import {
|
||||
faTimes,
|
||||
faStickyNote,
|
||||
faStop,
|
||||
faBan
|
||||
faBan,
|
||||
faBolt,
|
||||
faLaptop,
|
||||
faDonate,
|
||||
faMoneyBill,
|
||||
faChartLine
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
/** Add icons to the library for convenient access in other components. */
|
||||
@@ -166,7 +171,12 @@ library.add(
|
||||
faTimes,
|
||||
faStickyNote,
|
||||
faStop,
|
||||
faBan
|
||||
faBan,
|
||||
faBolt,
|
||||
faLaptop,
|
||||
faDonate,
|
||||
faMoneyBill,
|
||||
faChartLine
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -99,6 +99,19 @@ export class WebAppComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
|
||||
// Stores top 100 user activites as local storage object.
|
||||
let activities: string[] = [];
|
||||
if (localStorage.getItem('mifosXLocation')) {
|
||||
const activitiesArray: string[] = JSON.parse(localStorage.getItem('mifosXLocation'));
|
||||
const length = activitiesArray.length;
|
||||
activities = length > 100 ? activitiesArray.slice(length - 100) : activitiesArray;
|
||||
}
|
||||
// Store route URLs array in local storage on navigation end.
|
||||
onNavigationEnd.subscribe(() => {
|
||||
activities.push(this.router.url);
|
||||
localStorage.setItem('mifosXLocation', JSON.stringify(activities));
|
||||
});
|
||||
|
||||
// Setup theme
|
||||
const theme = this.themeStorageService.getTheme();
|
||||
if (theme) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@import '../../app/clients/clients-view/clients-view.component-theme.scss';
|
||||
@import '../../app/groups/groups-view/groups-view.component-theme.scss';
|
||||
@import '../../app/centers/centers-view/centers-view.component-theme.scss';
|
||||
@import '../../app/home/dashboard/dashboard.component-theme.scss';
|
||||
|
||||
// Custom material palette
|
||||
$mat-denim: (
|
||||
@@ -51,3 +52,4 @@ $mifosx-app-theme: mat-light-theme($mifosx-app-primary, $mifosx-app-accent, $mif
|
||||
@include clients-view-component-theme($mifosx-app-theme);
|
||||
@include groups-view-component-theme($mifosx-app-theme);
|
||||
@include centers-view-component-theme($mifosx-app-theme);
|
||||
@include dashboard-component-theme($mifosx-app-theme);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@import '../../app/clients/clients-view/clients-view.component-theme.scss';
|
||||
@import '../../app/groups/groups-view/groups-view.component-theme.scss';
|
||||
@import '../../app/centers/centers-view/centers-view.component-theme.scss';
|
||||
@import '../../app/home/dashboard/dashboard.component-theme.scss';
|
||||
|
||||
// Custom material palette
|
||||
$mat-picton-blue: (
|
||||
@@ -51,3 +52,4 @@ $mifosx-app-theme: mat-light-theme($mifosx-app-primary, $mifosx-app-accent, $mif
|
||||
@include clients-view-component-theme($mifosx-app-theme);
|
||||
@include groups-view-component-theme($mifosx-app-theme);
|
||||
@include centers-view-component-theme($mifosx-app-theme);
|
||||
@include dashboard-component-theme($mifosx-app-theme);
|
||||
|
||||
Reference in New Issue
Block a user