From ca9404213c0967baf0785339e6b6b43742abd19e Mon Sep 17 00:00:00 2001 From: Karan Takalkar Date: Sat, 30 May 2020 21:55:12 +0530 Subject: [PATCH] feat: add create report page (#592) Fixes: #233 --- .../reports/common-models/chart-data.model.ts | 16 ++ .../common-models/report-parameter.model.ts | 31 +++ .../common-models/select-option.model.ts | 11 + src/app/reports/reports-routing.module.ts | 28 ++- src/app/reports/reports.component.html | 2 +- src/app/reports/reports.module.ts | 14 +- src/app/reports/reports.service.ts | 84 ++++++++ .../run-report/chart/chart.component.html | 10 + .../run-report/chart/chart.component.scss | 0 .../run-report/chart/chart.component.spec.ts | 25 +++ .../run-report/chart/chart.component.ts | 139 ++++++++++++ .../run-report/pentaho/pentaho.component.html | 1 + .../run-report/pentaho/pentaho.component.scss | 0 .../pentaho/pentaho.component.spec.ts | 25 +++ .../run-report/pentaho/pentaho.component.ts | 52 +++++ .../run-report/run-report.component.html | 113 ++++++++++ .../run-report/run-report.component.scss | 3 + .../run-report/run-report.component.spec.ts | 25 +++ .../run-report/run-report.component.ts | 201 ++++++++++++++++++ .../reports/run-report/run-report.resolver.ts | 30 +++ .../table-and-sms.component.html | 26 +++ .../table-and-sms.component.scss | 13 ++ .../table-and-sms.component.spec.ts | 25 +++ .../table-and-sms/table-and-sms.component.ts | 110 ++++++++++ src/typings.d.ts | 2 + 25 files changed, 983 insertions(+), 3 deletions(-) create mode 100644 src/app/reports/common-models/chart-data.model.ts create mode 100644 src/app/reports/common-models/report-parameter.model.ts create mode 100644 src/app/reports/common-models/select-option.model.ts create mode 100644 src/app/reports/run-report/chart/chart.component.html create mode 100644 src/app/reports/run-report/chart/chart.component.scss create mode 100644 src/app/reports/run-report/chart/chart.component.spec.ts create mode 100644 src/app/reports/run-report/chart/chart.component.ts create mode 100644 src/app/reports/run-report/pentaho/pentaho.component.html create mode 100644 src/app/reports/run-report/pentaho/pentaho.component.scss create mode 100644 src/app/reports/run-report/pentaho/pentaho.component.spec.ts create mode 100644 src/app/reports/run-report/pentaho/pentaho.component.ts create mode 100644 src/app/reports/run-report/run-report.component.html create mode 100644 src/app/reports/run-report/run-report.component.scss create mode 100644 src/app/reports/run-report/run-report.component.spec.ts create mode 100644 src/app/reports/run-report/run-report.component.ts create mode 100644 src/app/reports/run-report/run-report.resolver.ts create mode 100644 src/app/reports/run-report/table-and-sms/table-and-sms.component.html create mode 100644 src/app/reports/run-report/table-and-sms/table-and-sms.component.scss create mode 100644 src/app/reports/run-report/table-and-sms/table-and-sms.component.spec.ts create mode 100644 src/app/reports/run-report/table-and-sms/table-and-sms.component.ts diff --git a/src/app/reports/common-models/chart-data.model.ts b/src/app/reports/common-models/chart-data.model.ts new file mode 100644 index 000000000..d2a89616c --- /dev/null +++ b/src/app/reports/common-models/chart-data.model.ts @@ -0,0 +1,16 @@ +/** Chart Data Model */ +export class ChartData { + + keysLabel: string; + valuesLabel: string; + keys: string[]; + values: number[]; + + constructor(response: any) { + this.keysLabel = response.columnHeaders[0].columnName; + this.valuesLabel = response.columnHeaders[1].columnName; + this.keys = response.data.map((object: any) => object.row[0]); + this.values = response.data.map((object: any) => object.row[1]); + } + +} diff --git a/src/app/reports/common-models/report-parameter.model.ts b/src/app/reports/common-models/report-parameter.model.ts new file mode 100644 index 000000000..38bd53f1d --- /dev/null +++ b/src/app/reports/common-models/report-parameter.model.ts @@ -0,0 +1,31 @@ +/** Report Parameter Model */ +export class ReportParameter { + + name: string; + variable: string; + label: string; + displayType: string; + formatType: string; + defaultVal: any; + selectOne: any; + selectAll: any; + parentParameterName: string; + inputName: string; + selectOptions: any[] = []; + childParameter: any; + pentahoName: any; + + constructor(options: any[]) { + this.name = options[0]; + this.variable = options[1]; + this.label = options[2]; + this.displayType = options[3]; + this.formatType = options[4]; + this.defaultVal = options[5]; + this.selectOne = options[6]; + this.selectAll = options[7]; + this.parentParameterName = options[8]; + this.inputName = `R_${options[1]}`; + } + +} diff --git a/src/app/reports/common-models/select-option.model.ts b/src/app/reports/common-models/select-option.model.ts new file mode 100644 index 000000000..d6520cc19 --- /dev/null +++ b/src/app/reports/common-models/select-option.model.ts @@ -0,0 +1,11 @@ +/** Select Option model */ +export class SelectOption { + + id: number; + name: string; + + constructor(options: any[]) { + this.id = options[0], + this.name = options[1]; + } +} diff --git a/src/app/reports/reports-routing.module.ts b/src/app/reports/reports-routing.module.ts index e88e69c72..eab3d3994 100644 --- a/src/app/reports/reports-routing.module.ts +++ b/src/app/reports/reports-routing.module.ts @@ -10,9 +10,11 @@ import { extract } from '../core/i18n/i18n.service'; /** Custom Components */ import { ReportsComponent } from './reports.component'; +import { RunReportComponent } from './run-report/run-report.component'; /** Custom Resolvers */ import { ReportsResolver } from './reports.resolver'; +import { RunReportResolver } from './run-report/run-report.resolver'; /** Reports Routes */ const routes: Routes = [ @@ -34,6 +36,29 @@ const routes: Routes = [ component: ReportsComponent, } ] + }, + { + path: 'run-report', + data: { title: extract('Reports'), breadcrumb: 'Reports' }, + children: [ + { + path: '', + redirectTo: '/reports', pathMatch: 'full' + }, + { + path: ':report-name/:report-type/:report-id', + data: { title: extract('Run Report'), routeParamBreadcrumb: 'report-name' }, + children: [ + { + path: '', + component: RunReportComponent, + resolve: { + params: RunReportResolver + } + } + ] + } + ] } ]) ]; @@ -47,7 +72,8 @@ const routes: Routes = [ imports: [RouterModule.forChild(routes)], exports: [RouterModule], providers: [ - ReportsResolver + ReportsResolver, + RunReportResolver ] }) export class ReportsRoutingModule { } diff --git a/src/app/reports/reports.component.html b/src/app/reports/reports.component.html index 616b2e6ef..77f1d602f 100644 --- a/src/app/reports/reports.component.html +++ b/src/app/reports/reports.component.html @@ -28,7 +28,7 @@ - + diff --git a/src/app/reports/reports.module.ts b/src/app/reports/reports.module.ts index d4a27bc81..c1e649778 100644 --- a/src/app/reports/reports.module.ts +++ b/src/app/reports/reports.module.ts @@ -1,5 +1,6 @@ /** Angular Imports */ import { NgModule } from '@angular/core'; +import { DecimalPipe } from '@angular/common'; /** Custom Modules */ import { SharedModule } from '../shared/shared.module'; @@ -7,6 +8,10 @@ import { ReportsRoutingModule } from 'app/reports/reports-routing.module'; /** Custom Components */ import { ReportsComponent } from './reports.component'; +import { RunReportComponent } from './run-report/run-report.component'; +import { TableAndSmsComponent } from './run-report/table-and-sms/table-and-sms.component'; +import { ChartComponent } from './run-report/chart/chart.component'; +import { PentahoComponent } from './run-report/pentaho/pentaho.component'; /** * Reports Module @@ -18,6 +23,13 @@ import { ReportsComponent } from './reports.component'; SharedModule, ReportsRoutingModule ], - declarations: [ReportsComponent], + declarations: [ + ReportsComponent, + RunReportComponent, + TableAndSmsComponent, + ChartComponent, + PentahoComponent + ], + providers: [ DecimalPipe ] }) export class ReportsModule { } diff --git a/src/app/reports/reports.service.ts b/src/app/reports/reports.service.ts index b3acea4f2..4a80b17a4 100644 --- a/src/app/reports/reports.service.ts +++ b/src/app/reports/reports.service.ts @@ -4,6 +4,12 @@ import { HttpClient, HttpParams } from '@angular/common/http'; /** rxjs Imports */ import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +/** Custom Models */ +import { ReportParameter } from './common-models/report-parameter.model'; +import { SelectOption } from './common-models/select-option.model'; +import { ChartData } from './common-models/chart-data.model'; /** * Reports service. @@ -25,4 +31,82 @@ export class ReportsService { return this.http.get('/reports'); } + /** + * @param {string} reportName Report name for which parameters are needed. + * @returns {Observable} + */ + getReportParams(reportName: string): Observable { + const httpParams = new HttpParams() + .set('R_reportListing', `'${reportName}'`) + .set('parameterType', 'true'); + return this.http.get(`/runreports/FullParameterList`, {params: httpParams}) + .pipe(map((response: any) => response.data.map((entry: any) => new ReportParameter(entry.row)) )); + } + + /** + * @param {string} inputString URL substring containing object details. + * @returns {Observable} + */ + getSelectOptions(inputString: string): Observable { + const httpParams = new HttpParams().set('parameterType', 'true'); + return this.http.get(`/runreports/${inputString}`, {params: httpParams}) + .pipe(map((response: any) => response.data.map((entry: any) => new SelectOption(entry.row)) )); + } + + /** + * @param {number} reportId Report id for which pentaho parameters are needed. + * @returns {Observable} + */ + getPentahoParams(reportId: number): Observable { + const httpParams = new HttpParams().set('fields', 'reportParameters'); + return this.http.get(`/reports/${reportId}`, {params: httpParams}) + .pipe(map((response: any) => response.reportParameters)); + } + + /** + * Run Report Data for Table and SMS. + * @param {any} report report object containing 'id', 'name' & 'type' attributes. + * @param {object} formData Form Data. + * @returns {Observable} + */ + getRunReportData(report: any, formData: object): Observable { + let httpParams = new HttpParams(); + for (const [key, value] of Object.entries(formData)) { + httpParams = httpParams.set(key, value); + } + return this.http.get(`/runreports/${report.name}`, {params: httpParams}); + } + + /** + * Run Report Data for Charts. + * @param {any} report report object containing 'id', 'name' & 'type' attributes. + * @param {object} formData Form Data. + * @returns {Observable} + */ + getChartRunReportData(report: any, formData: object): Observable { + let httpParams = new HttpParams(); + for (const [key, value] of Object.entries(formData)) { + httpParams = httpParams.set(key, value); + } + return this.http.get(`/runreports/${report.name}`, {params: httpParams}) + .pipe(map((response: any) => new ChartData(response))); + } + + /** + * Run Report Data for Pentaho. + * @param {any} report report object containing 'id', 'name' & 'type' attributes. + * @param {object} formData Form Data. + * @returns {Observable} + */ + getPentahoRunReportData(report: any, formData: object, tenantIdentifier: string, locale: string, dateFormat: string): Observable { + let httpParams = new HttpParams() + .set('tenantIdentifier', tenantIdentifier) + .set('locale', locale) + .set('dateFormat', dateFormat); + for (const [key, value] of Object.entries(formData)) { + httpParams = httpParams.set(key, value); + } + return this.http.get(`/runreports/${report.name}`, {responseType: 'arraybuffer', observe: 'response', params: httpParams}); + } + } diff --git a/src/app/reports/run-report/chart/chart.component.html b/src/app/reports/run-report/chart/chart.component.html new file mode 100644 index 000000000..ab16e0b74 --- /dev/null +++ b/src/app/reports/run-report/chart/chart.component.html @@ -0,0 +1,10 @@ +
+ + Bar Chart + Pie Chart + +
+ +
+ +
diff --git a/src/app/reports/run-report/chart/chart.component.scss b/src/app/reports/run-report/chart/chart.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/reports/run-report/chart/chart.component.spec.ts b/src/app/reports/run-report/chart/chart.component.spec.ts new file mode 100644 index 000000000..4bfa61135 --- /dev/null +++ b/src/app/reports/run-report/chart/chart.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChartComponent } from './chart.component'; + +describe('ChartComponent', () => { + let component: ChartComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ChartComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ChartComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/reports/run-report/chart/chart.component.ts b/src/app/reports/run-report/chart/chart.component.ts new file mode 100644 index 000000000..661c69210 --- /dev/null +++ b/src/app/reports/run-report/chart/chart.component.ts @@ -0,0 +1,139 @@ +/** Angular Imports */ +import { Component, OnChanges, Input } from '@angular/core'; + +/** Custom Services */ +import { ReportsService } from '../../reports.service'; + +/** Custom Models */ +import { ChartData } from '../../common-models/chart-data.model'; + +/** Charting Imports */ +import Chart from 'chart.js'; + +/** + * Chart Component + */ +@Component({ + selector: 'mifosx-chart', + templateUrl: './chart.component.html', + styleUrls: ['./chart.component.scss' ] +}) +export class ChartComponent implements OnChanges { + + /** Run Report Data */ + @Input() dataObject: any; + + /** chart data object */ + chart: any; + /** substitute for resolver */ + hideOutput = true; + /** Data object for witching charts in view. */ + inputData: ChartData; + + /** + * @param {ReportsService} reportsService Reports Service + */ + constructor(private reportsService: ReportsService) { } + + /** + * Fetches run report data post changes in run report form. + */ + ngOnChanges() { + this.getRunReportData(); + } + + getRunReportData() { + this.reportsService.getChartRunReportData(this.dataObject.reportData, this.dataObject.formData) + .subscribe((response: ChartData) => { + this.inputData = response; + this.setPieChart(this.inputData); + this.hideOutput = false; + }); + } + + /** + * Creates instance of chart.js pie chart. + * Refer: https://www.chartjs.org/docs/latest/charts/doughnut.html for configuration details. + */ + setPieChart(inputData: ChartData) { + if (this.chart) { + this.chart.destroy(); + } + this.chart = new Chart('output', { + type: 'pie', + data: { + labels: inputData.keys, + datasets: [{ + label: inputData.valuesLabel, + data: inputData.values, + backgroundColor: this.randomColorArray(inputData.values.length) + }] + }, + options: { + title: { + display: true, + text: inputData.keysLabel + } + } + }); + } + + /** + * Creates instance of chart.js bar chart. + * Refer: https://www.chartjs.org/docs/latest/charts/bar.html for configuration details. + */ + setBarChart(inputData: ChartData) { + if (this.chart) { + this.chart.destroy(); + } + this.chart = new Chart('output', { + type: 'bar', + data: { + labels: inputData.keys, + datasets: [{ + label: inputData.valuesLabel, + data: inputData.values, + backgroundColor: this.randomColorArray(inputData.values.length) + }] + }, + options: { + legend: { display: false }, + scales: { + xAxes: [{ + scaleLabel: { + display: true, + labelString: inputData.keysLabel + }, + ticks: { + beginAtZero: true + } + }] + } + } + }); + } + + /** + * Generates bar/pie-slice colors array for dynamic charts. + * @param {number} length Length of dataset array. + */ + randomColorArray(length: number) { + const colorArray: any[] = []; + while (length--) { + const color = this.randomColor(); + colorArray.push(color); + } + return colorArray; + } + + /** + * Returns a random rgb color. + */ + randomColor() { + const r = Math.floor(Math.random() * 255); + const g = Math.floor(Math.random() * 255); + const b = Math.floor(Math.random() * 255); + return `rgb(${r},${g},${b},0.6)`; + } + +} diff --git a/src/app/reports/run-report/pentaho/pentaho.component.html b/src/app/reports/run-report/pentaho/pentaho.component.html new file mode 100644 index 000000000..59244a56c --- /dev/null +++ b/src/app/reports/run-report/pentaho/pentaho.component.html @@ -0,0 +1 @@ + diff --git a/src/app/reports/run-report/pentaho/pentaho.component.scss b/src/app/reports/run-report/pentaho/pentaho.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/reports/run-report/pentaho/pentaho.component.spec.ts b/src/app/reports/run-report/pentaho/pentaho.component.spec.ts new file mode 100644 index 000000000..2b84baca4 --- /dev/null +++ b/src/app/reports/run-report/pentaho/pentaho.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PentahoComponent } from './pentaho.component'; + +describe('PentahoComponent', () => { + let component: PentahoComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ PentahoComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PentahoComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/reports/run-report/pentaho/pentaho.component.ts b/src/app/reports/run-report/pentaho/pentaho.component.ts new file mode 100644 index 000000000..73e3765f1 --- /dev/null +++ b/src/app/reports/run-report/pentaho/pentaho.component.ts @@ -0,0 +1,52 @@ +/** Angular Imports */ +import { Component, OnChanges, Input } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; + +/** Custom Services */ +import { ReportsService } from '../../reports.service'; + +/** + * Pentaho Component + */ +@Component({ + selector: 'mifosx-pentaho', + templateUrl: './pentaho.component.html', + styleUrls: ['./pentaho.component.scss'] +}) +export class PentahoComponent implements OnChanges { + + /** Run Report Data */ + @Input() dataObject: any; + + /** substitute for resolver */ + hideOutput = true; + /** trusted resource url for pentaho output */ + pentahoUrl: any; + + /** + * @param {DomSanitizer} sanitizer DOM Sanitizer + * @param {ReportsService} reportsService Reports Service + */ + constructor(private sanitizer: DomSanitizer, + private reportsService: ReportsService) { } + + /** + * Fetches run report data post changes in run report form. + */ + ngOnChanges() { + this.hideOutput = true; + this.getRunReportData(); + } + + getRunReportData() { + this.reportsService.getPentahoRunReportData(this.dataObject.reportData, this.dataObject.formData, 'default', 'en', 'dd MMMM yyyy') + .subscribe( (res: any) => { + const contentType = res.headers.get('Content-Type'); + const file = new Blob([res.body], {type: contentType}); + const filecontent = URL.createObjectURL(file); + this.pentahoUrl = this.sanitizer.bypassSecurityTrustResourceUrl(filecontent); + this.hideOutput = false; + }); + } + +} diff --git a/src/app/reports/run-report/run-report.component.html b/src/app/reports/run-report/run-report.component.html new file mode 100644 index 000000000..27fa3ed18 --- /dev/null +++ b/src/app/reports/run-report/run-report.component.html @@ -0,0 +1,113 @@ +
+ + + +
+ + + +
+ +
+ +
+ + {{param.label}} + + + {{param.label}} is required + + +
+ +
+ + {{param.label}} + + + + + {{param.label}} is required + + +
+ +
+ + {{param.label}} + + + {{option.name}} + + + + {{param.label}} is required + + +
+ +
+ +
+ +
+ + Decimal Places + + 0 + 1 + 2 + 3 + 4 + + +
+ +
+ + Output Type + + Normal format + Excel format + Excel 2007 format + CSV format + PDF format + + +
+ +
+ + + + + + +
+ +
+ +
+ +
+ + + +
+ +
+ +
+ + + +
+ +
+ +
diff --git a/src/app/reports/run-report/run-report.component.scss b/src/app/reports/run-report/run-report.component.scss new file mode 100644 index 000000000..57c04e49c --- /dev/null +++ b/src/app/reports/run-report/run-report.component.scss @@ -0,0 +1,3 @@ +.input { + max-width: 37rem; +} diff --git a/src/app/reports/run-report/run-report.component.spec.ts b/src/app/reports/run-report/run-report.component.spec.ts new file mode 100644 index 000000000..b6d4de470 --- /dev/null +++ b/src/app/reports/run-report/run-report.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RunReportComponent } from './run-report.component'; + +describe('RunReportComponent', () => { + let component: RunReportComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ RunReportComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RunReportComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/reports/run-report/run-report.component.ts b/src/app/reports/run-report/run-report.component.ts new file mode 100644 index 000000000..123627f6e --- /dev/null +++ b/src/app/reports/run-report/run-report.component.ts @@ -0,0 +1,201 @@ +/** Angular Imports */ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { DatePipe } from '@angular/common'; + +/** Custom Services */ +import { ReportsService } from '../reports.service'; + +/** Custom Models */ +import { ReportParameter } from '../common-models/report-parameter.model'; +import { SelectOption } from '../common-models/select-option.model'; + +/** + * Run report component. + */ +@Component({ + selector: 'mifosx-run-report', + templateUrl: './run-report.component.html', + styleUrls: ['./run-report.component.scss'] +}) +export class RunReportComponent implements OnInit { + + /** Minimum date allowed. */ + minDate = new Date(2000, 0, 1); + /** Maximum date allowed. */ + maxDate = new Date(); + /** Contains report specifications i.e: name, type and id */ + report: any = {}; + /** Formatted data post labeling of report parameters fetched from API */ + paramData: any = []; + /** Array of all parent parameters */ + parentParameters: any[] = []; + /** Parameter data to configure pentaho output */ + pentahoReportParameters: any[] = []; + /** Data to be passed on to component selectors */ + dataObject: any; + /** Initializes new form group 'ReportForm */ + ReportForm = new FormGroup({}); + /** Static Form control for decimal places in output */ + decimalChoice = new FormControl(); + /** Toggles Report form */ + isCollapsed = false; + /** Toggles Table output. */ + hideTable = true; + /** Toggles Chart output */ + hideChart = true; + /** Toggles Pentaho output */ + hidePentaho = true; + + /** + * Fetches report specifications from route params and retrieves report parameters data from `resolve`. + * @param {ActivatedRoute} route ActivatedRoute. + * @param {DomSanitizer} sanitizer DomSanitizer. + * @param {ReportsService} reportsService ReportsService + * @param {DatePipe} datePipe Date Pipe + */ + constructor(private route: ActivatedRoute, + private reportsService: ReportsService, + private datePipe: DatePipe) { + this.report.name = this.route.snapshot.params['report-name']; + this.report.type = this.route.snapshot.params['report-type']; + this.report.id = this.route.snapshot.params['report-id']; + this.route.data.subscribe((data: any) => { + this.paramData = data.params; + }); + } + + /** + * Creates and sets the run report form. + */ + ngOnInit() { + this.createRunReportForm(); + } + + /** + * Establishes form controls for Report Parameter's name attribute, + * Fetches dropdown options and builds child dependencies. + */ + createRunReportForm() { + this.paramData.forEach( + (param: any) => { + if (!param.parentParameterName) { + this.ReportForm.addControl(param.name, new FormControl('', Validators.required)); + if (param.displayType === 'select') { + this.fetchSelectOptions(param, param.name); + } + } else { + const parent: ReportParameter = this.paramData + .find((entry: any) => entry.name === param.parentParameterName); + parent.childParameter = param; + this.parentParameters.push(parent); + } + }); + if (this.report.type === 'Pentaho') { + this.ReportForm.addControl('outputType', new FormControl('')); + this.mapPentahoParams(); + } + this.decimalChoice.patchValue('0'); + this.setChildControls(); + } + + /** + * Fetches Select Dropdown options for param type "Select". + * @param {ReportParameter} param Parameter for which dropdown options are required. + * @param {string} inputstring url substring for API call. + */ + fetchSelectOptions(param: ReportParameter, inputstring: string) { + this.reportsService.getSelectOptions(inputstring).subscribe((options: SelectOption[]) => { + param.selectOptions = options; + if (param.selectAll === 'Y') { + param.selectOptions.push({id: '-1', name: 'All'}); + } + }); + } + + /** + * Maps pentaho specific names to form-control names. + */ + mapPentahoParams() { + this.reportsService.getPentahoParams(this.report.id).subscribe((data: any) => { + data.forEach((entry: any) => { + const param: ReportParameter = this.paramData + .find((_entry: any) => _entry.name === entry.parameterName); + param.pentahoName = `R_${entry.reportParameterName}`; + }); + }); + } + + /** + * Subscribes to changes in parent parameters value, builds child parameter vis-a-vis parent's value. + */ + setChildControls() { + this.parentParameters.forEach((param: ReportParameter) => { + this.ReportForm.get(param.name).valueChanges.subscribe((option: any) => { + this.ReportForm.addControl(param.childParameter.name, new FormControl('', Validators.required)); + if (param.childParameter.displayType === 'select') { + const inputstring = `${param.childParameter.name}?${param.inputName}=${option.id}`; + this.fetchSelectOptions(param.childParameter, inputstring); + } + }); + }); + } + + /** + * Formats user response and readies it for utilization by run report function. + * @param {any} response Object containing formcontrol values. + */ + formatUserResponse(response: any) { + const formattedResponse: any = {}; + let newKey: string; + for (const [key, value] of Object.entries(response)) { + if (key === 'outputType') { + formattedResponse['output-type'] = value; + continue; + } + const param: ReportParameter = this.paramData + .find((_entry: any) => _entry.name === key); + newKey = this.report.type === 'Pentaho' ? param.pentahoName : param.inputName; + switch (param.displayType) { + case 'text': + formattedResponse[newKey] = value; + break; + case 'select': + formattedResponse[newKey] = value['id']; + break; + case 'date': + const dateFormat = 'yyyy-MM-dd'; + formattedResponse[newKey] = this.datePipe.transform(value, dateFormat); + break; + } + } + return formattedResponse; + } + + /** + * Core run report functionality. + */ + run() { + this.isCollapsed = true; + const userResponse = this.formatUserResponse(this.ReportForm.value); + this.dataObject = { + formData: userResponse, + reportData: this.report, + decimalChoice: this.decimalChoice.value + }; + switch (this.report.type) { + case 'SMS': + case 'Table': + this.hideTable = false; + break; + case 'Chart': + this.hideChart = false; + break; + case 'Pentaho': + this.hidePentaho = false; + break; + } + } + +} diff --git a/src/app/reports/run-report/run-report.resolver.ts b/src/app/reports/run-report/run-report.resolver.ts new file mode 100644 index 000000000..07a18dd99 --- /dev/null +++ b/src/app/reports/run-report/run-report.resolver.ts @@ -0,0 +1,30 @@ +/** Angular Imports */ +import { Injectable } from '@angular/core'; +import { Resolve, ActivatedRouteSnapshot } from '@angular/router'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** Custom Services */ +import { ReportsService } from '../reports.service'; + +/** + * Reports data resolver. + */ +@Injectable() +export class RunReportResolver implements Resolve { + + /** + * @param {ReportsService} reportsService Reports service. + */ + constructor(private reportsService: ReportsService) {} + + /** + * Returns the reports data. + * @returns {Observable} + */ + resolve(route: ActivatedRouteSnapshot): Observable { + return this.reportsService.getReportParams(route.params['report-name']); + } + +} diff --git a/src/app/reports/run-report/table-and-sms/table-and-sms.component.html b/src/app/reports/run-report/table-and-sms/table-and-sms.component.html new file mode 100644 index 000000000..d1869fb3c --- /dev/null +++ b/src/app/reports/run-report/table-and-sms/table-and-sms.component.html @@ -0,0 +1,26 @@ +
+ +
+ +
+ +
+ +
+ +
+ + + + + +
{{column}} + {{toDecimal(param.row[i])}} + {{param.row[i]}} +
+ +
+ +
diff --git a/src/app/reports/run-report/table-and-sms/table-and-sms.component.scss b/src/app/reports/run-report/table-and-sms/table-and-sms.component.scss new file mode 100644 index 000000000..fb1e08cda --- /dev/null +++ b/src/app/reports/run-report/table-and-sms/table-and-sms.component.scss @@ -0,0 +1,13 @@ +.table-output { + overflow: auto; +} + +.select-row:hover { + cursor: pointer; + } + +.mat-header-cell, .mat-cell { + min-width: 150px; + margin-left: 5px; + margin-right: 5px; +} diff --git a/src/app/reports/run-report/table-and-sms/table-and-sms.component.spec.ts b/src/app/reports/run-report/table-and-sms/table-and-sms.component.spec.ts new file mode 100644 index 000000000..cee38749b --- /dev/null +++ b/src/app/reports/run-report/table-and-sms/table-and-sms.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TableAndSmsComponent } from './table-and-sms.component'; + +describe('TableAndSmsComponent', () => { + let component: TableAndSmsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ TableAndSmsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TableAndSmsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/reports/run-report/table-and-sms/table-and-sms.component.ts b/src/app/reports/run-report/table-and-sms/table-and-sms.component.ts new file mode 100644 index 000000000..312935d10 --- /dev/null +++ b/src/app/reports/run-report/table-and-sms/table-and-sms.component.ts @@ -0,0 +1,110 @@ +/** Angular Imports */ +import { Component, Input, ViewChild, OnChanges } from '@angular/core'; +import { MatPaginator, MatTableDataSource} from '@angular/material'; +import { DecimalPipe } from '@angular/common'; + +/** Custom Servies */ +import { ReportsService } from '../../reports.service'; + +/** + * Table and SMS Component + */ +@Component({ + selector: 'mifosx-table-and-sms', + templateUrl: './table-and-sms.component.html', + styleUrls: ['./table-and-sms.component.scss'] +}) +export class TableAndSmsComponent implements OnChanges { + + /** Run Report Data */ + @Input() dataObject: any; + + /** Columns to be displayed in mat-table */ + displayedColumns: string[] = []; + /** Data source for run-report table. */ + dataSource = new MatTableDataSource(); + /** Maps column name to type */ + columnTypes: any[] = []; + /** substitute for resolver */ + hideOutput = true; + /** Data to be converted into CSV file */ + csvData: any; + + /** Paginator for run-report table. */ + @ViewChild(MatPaginator) paginator: MatPaginator; + + /** + * @param {ReportsService} reportsService Reports Service + * @param {DecimalPipe} decimalPipe Decimal Pipe + */ + constructor(private reportsService: ReportsService, + private decimalPipe: DecimalPipe ) { } + + /** + * Fetches run report data post changes in run report form. + */ + ngOnChanges() { + this.hideOutput = true; + this.columnTypes = []; + this.displayedColumns = []; + this.getRunReportData(); + } + + getRunReportData() { + this.reportsService.getRunReportData(this.dataObject.reportData, this.dataObject.formData) + .subscribe( (res: any) => { + this.csvData = res.data; + this.setOutputTable(res.data); + res.columnHeaders.forEach((header: any) => { + this.columnTypes.push(header.columnDisplayType); + this.displayedColumns.push(header.columnName); + }); + this.hideOutput = false; + }); + } + + /** + * Sets up a dynamic Mat Table. + * @param {any} data Mat Table data + */ + setOutputTable(data: any) { + this.dataSource = new MatTableDataSource(data); + setTimeout(() => { + this.dataSource.paginator = this.paginator; + }); + } + + /** + * Generates the CSV file dynamically for run report data. + */ + downloadCSV() { + const headers = this.displayedColumns; + let csv = this.csvData.map((object: any) => object.row.join()); + csv.unshift(`data:text/csv;charset=utf-8,${headers.join()}`); + csv = csv.join('\r\n'); + const link = document.createElement('a'); + link.setAttribute('href', encodeURI(csv)); + link.setAttribute('download', `${this.dataObject.reportData.name}.csv`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + + /** + * Returns number formatted as per user's decimal choice. + * @param {number} value Value to be formatted as per decimal choice. + */ + toDecimal(value: number) { + const decimalChoice = this.dataObject.decimalChoice; + return this.decimalPipe.transform(value, `1.${decimalChoice}-${decimalChoice}`); + } + + /** + * Checks the weather Mat-Table column has decimal display type. + * @param {number} index Index of column. + */ + isDecimal(index: number) { + return this.columnTypes[index] === 'DECIMAL'; + } + +} diff --git a/src/typings.d.ts b/src/typings.d.ts index 7306c36f1..904adb4e4 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -10,3 +10,5 @@ declare var module: NodeModule; interface NodeModule { id: string; } + +declare module 'chart.js';