diff --git a/src/app/home/dashboard/amount-collected-pie/amount-collected-pie.component.html b/src/app/home/dashboard/amount-collected-pie/amount-collected-pie.component.html new file mode 100644 index 000000000..af688b1e3 --- /dev/null +++ b/src/app/home/dashboard/amount-collected-pie/amount-collected-pie.component.html @@ -0,0 +1,30 @@ + + + +

+ +    Amount Collected +
+ + Office + + + {{ office.name }} + + + +
+

+
+ + + +
+ +
+
+

No Data

+
+
+ +
diff --git a/src/app/home/dashboard/amount-collected-pie/amount-collected-pie.component.scss b/src/app/home/dashboard/amount-collected-pie/amount-collected-pie.component.scss new file mode 100644 index 000000000..1a7c79e55 --- /dev/null +++ b/src/app/home/dashboard/amount-collected-pie/amount-collected-pie.component.scss @@ -0,0 +1,12 @@ +.card { + width: 27.5rem; + padding: 0; + .header { + #office{ + margin-bottom: -1.20em; + } + } + .fallback { + height: 20rem; + } +} diff --git a/src/app/home/dashboard/amount-collected-pie/amount-collected-pie.component.spec.ts b/src/app/home/dashboard/amount-collected-pie/amount-collected-pie.component.spec.ts new file mode 100644 index 000000000..04f576807 --- /dev/null +++ b/src/app/home/dashboard/amount-collected-pie/amount-collected-pie.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ AmountCollectedPieComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AmountCollectedPieComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/home/dashboard/amount-collected-pie/amount-collected-pie.component.ts b/src/app/home/dashboard/amount-collected-pie/amount-collected-pie.component.ts new file mode 100644 index 000000000..ac66e4573 --- /dev/null +++ b/src/app/home/dashboard/amount-collected-pie/amount-collected-pie.component.ts @@ -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(); + } + } + +} diff --git a/src/app/home/dashboard/amount-disbursed-pie/amount-disbursed-pie.component.html b/src/app/home/dashboard/amount-disbursed-pie/amount-disbursed-pie.component.html new file mode 100644 index 000000000..b4ebb763c --- /dev/null +++ b/src/app/home/dashboard/amount-disbursed-pie/amount-disbursed-pie.component.html @@ -0,0 +1,30 @@ + + + +

+ +    Amount Disbursed +
+ + Office + + + {{ office.name }} + + + +
+

+
+ + + +
+ +
+
+

No Data

+
+
+ +
diff --git a/src/app/home/dashboard/amount-disbursed-pie/amount-disbursed-pie.component.scss b/src/app/home/dashboard/amount-disbursed-pie/amount-disbursed-pie.component.scss new file mode 100644 index 000000000..1a7c79e55 --- /dev/null +++ b/src/app/home/dashboard/amount-disbursed-pie/amount-disbursed-pie.component.scss @@ -0,0 +1,12 @@ +.card { + width: 27.5rem; + padding: 0; + .header { + #office{ + margin-bottom: -1.20em; + } + } + .fallback { + height: 20rem; + } +} diff --git a/src/app/home/dashboard/amount-disbursed-pie/amount-disbursed-pie.component.spec.ts b/src/app/home/dashboard/amount-disbursed-pie/amount-disbursed-pie.component.spec.ts new file mode 100644 index 000000000..2921cde16 --- /dev/null +++ b/src/app/home/dashboard/amount-disbursed-pie/amount-disbursed-pie.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ AmountDisbursedPieComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AmountDisbursedPieComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/home/dashboard/amount-disbursed-pie/amount-disbursed-pie.component.ts b/src/app/home/dashboard/amount-disbursed-pie/amount-disbursed-pie.component.ts new file mode 100644 index 000000000..4c97ec5b2 --- /dev/null +++ b/src/app/home/dashboard/amount-disbursed-pie/amount-disbursed-pie.component.ts @@ -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(); + } + } + +} diff --git a/src/app/home/dashboard/client-trends-bar/client-trends-bar.component.html b/src/app/home/dashboard/client-trends-bar/client-trends-bar.component.html new file mode 100644 index 000000000..7dd055f10 --- /dev/null +++ b/src/app/home/dashboard/client-trends-bar/client-trends-bar.component.html @@ -0,0 +1,32 @@ + + + +

+ +    Client Trends +
+ + Office + + + {{ office.name }} + + + +
+

+
+ + + +
+ +
+ + Day + Week + Month + +
+ +
diff --git a/src/app/home/dashboard/client-trends-bar/client-trends-bar.component.scss b/src/app/home/dashboard/client-trends-bar/client-trends-bar.component.scss new file mode 100644 index 000000000..8f272bcbc --- /dev/null +++ b/src/app/home/dashboard/client-trends-bar/client-trends-bar.component.scss @@ -0,0 +1,13 @@ +.card { + width: 41.25rem; + padding: 0; + .header { + #office{ + margin-bottom: -1.20em; + } + } + #timescale { + margin-block: 2%; + margin-inline: 35%; + } +} diff --git a/src/app/home/dashboard/client-trends-bar/client-trends-bar.component.spec.ts b/src/app/home/dashboard/client-trends-bar/client-trends-bar.component.spec.ts new file mode 100644 index 000000000..ad6f0639e --- /dev/null +++ b/src/app/home/dashboard/client-trends-bar/client-trends-bar.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ClientTrendsBarComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ClientTrendsBarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/home/dashboard/client-trends-bar/client-trends-bar.component.ts b/src/app/home/dashboard/client-trends-bar/client-trends-bar.component.ts new file mode 100644 index 000000000..369f49332 --- /dev/null +++ b/src/app/home/dashboard/client-trends-bar/client-trends-bar.component.ts @@ -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(); + } + } + +} diff --git a/src/app/home/dashboard/dashboard.component-theme.scss b/src/app/home/dashboard/dashboard.component-theme.scss new file mode 100644 index 000000000..842bb9d71 --- /dev/null +++ b/src/app/home/dashboard/dashboard.component-theme.scss @@ -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); + } + } + } + } + + } +} diff --git a/src/app/home/dashboard/dashboard.component.html b/src/app/home/dashboard/dashboard.component.html index d3fcf0142..3e10d390a 100644 --- a/src/app/home/dashboard/dashboard.component.html +++ b/src/app/home/dashboard/dashboard.component.html @@ -1,7 +1,53 @@
-

- TODO: dashboard component. -

+ + +
+ + + + +

+    Recent Activities

+
+ + + + + + + +
+ + + + +

+    Frequent Activities

+
+ + + + + + + +
+ + + +
+ +
+ + + + + +
+ +
diff --git a/src/app/home/dashboard/dashboard.component.scss b/src/app/home/dashboard/dashboard.component.scss index e69de29bb..25b6bc5a9 100644 --- a/src/app/home/dashboard/dashboard.component.scss +++ b/src/app/home/dashboard/dashboard.component.scss @@ -0,0 +1,10 @@ +.card { + padding: 2%; + .recent-activities, .frequent-activities { + width: 20rem; + padding: 0; + .list { + padding-top: 0px; + } + } +} diff --git a/src/app/home/dashboard/dashboard.component.ts b/src/app/home/dashboard/dashboard.component.ts index cf7487d81..d7ba1de2d 100644 --- a/src/app/home/dashboard/dashboard.component.ts +++ b/src/app/home/dashboard/dashboard.component.ts @@ -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; } } - diff --git a/src/app/home/home-routing.module.ts b/src/app/home/home-routing.module.ts index 885586b61..0d6670fda 100644 --- a/src/app/home/home-routing.module.ts +++ b/src/app/home/home-routing.module.ts @@ -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 { } diff --git a/src/app/home/home.module.ts b/src/app/home/home.module.ts index 407d447c7..b1b010153 100644 --- a/src/app/home/home.module.ts +++ b/src/app/home/home.module.ts @@ -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 { } diff --git a/src/app/home/home.service.spec.ts b/src/app/home/home.service.spec.ts new file mode 100644 index 000000000..2d073b453 --- /dev/null +++ b/src/app/home/home.service.spec.ts @@ -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(); + }); +}); diff --git a/src/app/home/home.service.ts b/src/app/home/home.service.ts new file mode 100644 index 000000000..ee098ae5a --- /dev/null +++ b/src/app/home/home.service.ts @@ -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} + */ + getCollectedAmount(officeId: number): Observable { + 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} + */ + getDisbursedAmount(officeId: number): Observable { + 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} + */ + getClientTrendsByDay(officeId: number): Observable { + 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} + */ + getClientTrendsByWeek(officeId: number): Observable { + 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} + */ + getClientTrendsByMonth(officeId: number): Observable { + 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} + */ + getLoanTrendsByDay(officeId: number): Observable { + 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} + */ + getLoanTrendsByWeek(officeId: number): Observable { + 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} + */ + getLoanTrendsByMonth(officeId: number): Observable { + const httpParams = new HttpParams() + .set('R_officeId', officeId.toString()) + .set('genericResultSet', 'false'); + return this.http.get('/runreports/LoanTrendsByMonth', { params: httpParams }); + } + +} diff --git a/src/app/pipes/pipes.module.ts b/src/app/pipes/pipes.module.ts index 1ad28d333..8283184d0 100644 --- a/src/app/pipes/pipes.module.ts +++ b/src/app/pipes/pipes.module.ts @@ -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 { } diff --git a/src/app/pipes/url-to-string.pipe.spec.ts b/src/app/pipes/url-to-string.pipe.spec.ts new file mode 100644 index 000000000..26e2e9d0b --- /dev/null +++ b/src/app/pipes/url-to-string.pipe.spec.ts @@ -0,0 +1,8 @@ +import { UrlToStringPipe } from './url-to-string.pipe'; + +describe('UrlToStringPipe', () => { + it('create an instance', () => { + const pipe = new UrlToStringPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/app/pipes/url-to-string.pipe.ts b/src/app/pipes/url-to-string.pipe.ts new file mode 100644 index 000000000..f955dd54e --- /dev/null +++ b/src/app/pipes/url-to-string.pipe.ts @@ -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; + } + +} diff --git a/src/app/shared/icons.module.ts b/src/app/shared/icons.module.ts index c624caa7c..a6150a454 100644 --- a/src/app/shared/icons.module.ts +++ b/src/app/shared/icons.module.ts @@ -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 ); /** diff --git a/src/app/web-app.component.ts b/src/app/web-app.component.ts index 087b91d15..9c98cdbf6 100644 --- a/src/app/web-app.component.ts +++ b/src/app/web-app.component.ts @@ -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) { diff --git a/src/theme/custom/denim-yellowgreen.scss b/src/theme/custom/denim-yellowgreen.scss index 40d8fc15d..fddc863f6 100644 --- a/src/theme/custom/denim-yellowgreen.scss +++ b/src/theme/custom/denim-yellowgreen.scss @@ -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); diff --git a/src/theme/custom/pictonblue-yellowgreen.scss b/src/theme/custom/pictonblue-yellowgreen.scss index 8a637355a..24d857b0c 100644 --- a/src/theme/custom/pictonblue-yellowgreen.scss +++ b/src/theme/custom/pictonblue-yellowgreen.scss @@ -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);