Export report table to CSV file using defined delimiter (#1798)

Co-authored-by: Jose Alberto Hernandez <alberto@blue-daemon.local>
This commit is contained in:
José Alberto Hernández
2023-07-20 21:14:18 -06:00
committed by GitHub
co-authored by Jose Alberto Hernandez
parent dd4b37c060
commit 3b140e6f34
20 changed files with 230 additions and 90 deletions
+12
View File
@@ -152,6 +152,18 @@ MIFOS_DEFAULT_LANGUAGE=en-US
MIFOS_SUPPORTED_LANGUAGES=en-US,fr-FR
```
Setting for applying the Client preload in the Clients view, Default true
```
MIFOS_PRELOAD_CLIENTS=false
```
Setting for exporting report table to CSV file using this field delimiter
```
MIFOS_DEFAULT_CHAR_DELIMITER=,
```
For more information look the env.sample file in the root directory of the project
## Want to help? [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/openMF/web-app/issues)
+5 -1
View File
@@ -10,9 +10,13 @@ FINERACT_API_VERSION=/v1
FINERACT_PLATFORM_TENANT_IDENTIFIER=default
# Tenant Identifiers list delimited by colon
# Tenant Identifiers list delimited by comma
FINERACT_PLATFORM_TENANTS_IDENTIFIER=default
MIFOS_DEFAULT_LANGUAGE=en-US
MIFOS_SUPPORTED_LANGUAGES=en-US,fr-FR
MIFOS_PRELOAD_CLIENTS=true
MIFOS_DEFAULT_CHAR_DELIMITER=,
@@ -0,0 +1,6 @@
export interface DialogData {
heading?: string;
dialogContext: any;
submitLabel?: string;
type: string;
}
+27
View File
@@ -0,0 +1,27 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class Dialogs {
constructor() { }
public setColor(dataType: string): string {
let color = 'primary';
switch (dataType) {
case 'Basic':
color = 'primary';
break;
case 'Mild':
color = 'primary';
break;
case 'Strong':
color = 'warn';
break;
default:
color = 'warn';
}
return color;
}
}
@@ -155,6 +155,7 @@ export class EditLoansAccountComponent implements OnInit {
}
loansAccountData.principal = loansAccountData.principalAmount;
delete loansAccountData.principalAmount;
delete loansAccountData.multiDisburseLoan;
this.loansService.updateLoansAccount(this.loanId, loansAccountData).subscribe((response: any) => {
this.router.navigate(['../'], { relativeTo: this.route });
});
@@ -8,7 +8,7 @@
<mat-form-field fxFlex>
<mat-label>Note</mat-label>
<textarea matInput required [formControl]="note" cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
<textarea matInput [formControl]="note" cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
</mat-form-field>
</div>
@@ -50,7 +50,7 @@
<ng-container matColumnDef="fromDate">
<th mat-header-cell *matHeaderCellDef matTooltip="Interest effective from date" mat-sort-header> From Date </th>
<td mat-cell *matCellDef="let floatingRatePeriod"> {{ floatingRatePeriod.fromDate | dateFormat : 'dd MMMM yyyy' }} </td>
<td mat-cell *matCellDef="let floatingRatePeriod"> {{ floatingRatePeriod.fromDate | dateFormat }} </td>
</ng-container>
<ng-container matColumnDef="interestRate">
@@ -12,8 +12,8 @@ import { LoanProductAccountingStepComponent } from '../loan-product-stepper/loan
/** Custom Services */
import { ProductsService } from 'app/products/products.service';
import { SettingsService } from 'app/settings/settings.service';
import { GlobalConfiguration } from 'app/system/configurations/global-configurations-tab/configuration.model';
import { LoanProducts } from '../loan-products';
@Component({
selector: 'mifosx-create-loan-product',
@@ -31,34 +31,28 @@ export class CreateLoanProductComponent implements OnInit {
loanProductsTemplate: any;
accountingRuleData = ['None', 'Cash', 'Accrual (periodic)', 'Accrual (upfront)'];
itemsByDefault: any[] = [];
/**
* @param {ActivatedRoute} route Activated Route.
* @param {ProductsService} productsService Product Service.
* @param {SettingsService} settingsService Settings Service
* @param {LoanProducts} loanProducts LoanProducts
* @param {Router} router Router for navigation.
*/
constructor(private route: ActivatedRoute,
private productsService: ProductsService,
private settingsService: SettingsService,
private loanProducts: LoanProducts,
private router: Router) {
this.route.data.subscribe((data: { loanProductsTemplate: any, configurations: any }) => {
this.loanProductsTemplate = data.loanProductsTemplate;
const assetAccountData = this.loanProductsTemplate.accountingMappingOptions.assetAccountOptions || [];
const liabilityAccountData = this.loanProductsTemplate.accountingMappingOptions.liabilityAccountOptions || [];
this.loanProductsTemplate.accountingMappingOptions.assetAndLiabilityAccountOptions = assetAccountData.concat(liabilityAccountData);
const itemsByDefault: string[] = [];
data.configurations.globalConfiguration.forEach((config: GlobalConfiguration) => {
if (config.name === 'days-before-repayment-is-due') {
this.loanProductsTemplate['dueDaysForRepaymentEvent'] = config.value;
itemsByDefault.push('dueDaysForRepaymentEvent');
} else if (config.name === 'days-after-repayment-is-overdue') {
this.loanProductsTemplate['overDueDaysForRepaymentEvent'] = config.value;
itemsByDefault.push('overDueDaysForRepaymentEvent');
}
this.loanProductsTemplate['itemsByDefault'] = itemsByDefault;
});
this.itemsByDefault = loanProducts.setItemsByDefault(data.configurations);
this.loanProductsTemplate['itemsByDefault'] = this.itemsByDefault;
this.loanProductsTemplate = loanProducts.updateLoanProductDefaults(this.loanProductsTemplate);
});
}
@@ -106,15 +100,7 @@ export class CreateLoanProductComponent implements OnInit {
}
submit() {
const dateFormat = this.settingsService.dateFormat;
const loanProduct = {
...this.loanProduct,
charges: this.loanProduct.charges.map((charge: any) => ({ id: charge.id })),
dateFormat,
locale: this.settingsService.language.code
};
delete loanProduct.allowAttributeConfiguration;
delete loanProduct.advancedAccountingRules;
const loanProduct = this.loanProducts.buildPayload(this.loanProduct, this.itemsByDefault);
this.productsService.createLoanProduct(loanProduct)
.subscribe((response: any) => {
@@ -12,8 +12,8 @@ import { LoanProductAccountingStepComponent } from '../loan-product-stepper/loan
/** Custom Services */
import { ProductsService } from 'app/products/products.service';
import { SettingsService } from 'app/settings/settings.service';
import { GlobalConfiguration } from 'app/system/configurations/global-configurations-tab/configuration.model';
import { LoanProducts } from '../loan-products';
@Component({
selector: 'mifosx-edit-loan-product',
@@ -31,38 +31,28 @@ export class EditLoanProductComponent implements OnInit {
loanProductAndTemplate: any;
accountingRuleData = ['None', 'Cash', 'Accrual (periodic)', 'Accrual (upfront)'];
itemsByDefault: GlobalConfiguration[] = [];
/**
* @param {ActivatedRoute} route Activated Route.
* @param {ProductsService} productsService Product Service.
* @param {SettingsService} settingsService Settings Service
* @param {LoanProducts} loanProducts LoanProducts
* @param {Router} router Router for navigation.
*/
constructor(private route: ActivatedRoute,
private productsService: ProductsService,
private settingsService: SettingsService,
private loanProducts: LoanProducts,
private router: Router) {
this.route.data.subscribe((data: { loanProductAndTemplate: any, configurations: any }) => {
this.loanProductAndTemplate = data.loanProductAndTemplate;
const assetAccountData = this.loanProductAndTemplate.accountingMappingOptions.assetAccountOptions || [];
const liabilityAccountData = this.loanProductAndTemplate.accountingMappingOptions.liabilityAccountOptions || [];
this.loanProductAndTemplate.accountingMappingOptions.assetAndLiabilityAccountOptions = assetAccountData.concat(liabilityAccountData);
const itemsByDefault: string[] = [];
data.configurations.globalConfiguration.forEach((config: GlobalConfiguration) => {
if (config.name === 'days-before-repayment-is-due') {
if (this.loanProductAndTemplate['dueDaysForRepaymentEvent'] == null) {
this.loanProductAndTemplate['dueDaysForRepaymentEvent'] = config.value;
itemsByDefault.push('dueDaysForRepaymentEvent');
}
} else if (config.name === 'days-after-repayment-is-overdue') {
if (this.loanProductAndTemplate['overDueDaysForRepaymentEvent'] == null) {
this.loanProductAndTemplate['overDueDaysForRepaymentEvent'] = config.value;
itemsByDefault.push('overDueDaysForRepaymentEvent');
}
}
this.loanProductAndTemplate['itemsByDefault'] = itemsByDefault;
});
this.itemsByDefault = loanProducts.setItemsByDefault(data.configurations);
this.loanProductAndTemplate['itemsByDefault'] = this.itemsByDefault;
this.loanProductAndTemplate = loanProducts.updateLoanProductDefaults(this.loanProductAndTemplate);
});
}
@@ -119,16 +109,8 @@ export class EditLoanProductComponent implements OnInit {
}
submit() {
// TODO: Update once language and date settings are setup
const dateFormat = this.settingsService.dateFormat;
const loanProduct = {
...this.loanProduct,
charges: this.loanProduct.charges.map((charge: any) => ({ id: charge.id })),
dateFormat,
locale: this.settingsService.language.code
};
delete loanProduct.allowAttributeConfiguration;
delete loanProduct.advancedAccountingRules;
const loanProduct = this.loanProducts.buildPayload(this.loanProduct, this.itemsByDefault);
this.productsService.updateLoanProduct(this.loanProductAndTemplate.id, loanProduct)
.subscribe((response: any) => {
this.router.navigate(['../../', response.resourceId], { relativeTo: this.route });
@@ -1,5 +1,6 @@
import { Component, OnInit, Input } from '@angular/core';
import { FormGroup, FormBuilder, Validators, FormControl } from '@angular/forms';
import { LoanProducts } from '../../loan-products';
@Component({
selector: 'mifosx-loan-product-settings-step',
@@ -28,9 +29,8 @@ export class LoanProductSettingsStepComponent implements OnInit {
interestRecalculationOnDayTypeData: any;
delinquencyBucketData: any;
itemsByDefault: string[] = [];
constructor(private formBuilder: FormBuilder) {
constructor(private formBuilder: FormBuilder,
private loanProducts: LoanProducts) {
this.createLoanProductSettingsForm();
this.setConditionalControls();
}
@@ -60,7 +60,6 @@ export class LoanProductSettingsStepComponent implements OnInit {
this.interestRecalculationOnDayTypeData = Array.from({ length: 28 }, (_, index) => index + 1);
this.delinquencyBucketData = this.loanProductsTemplate.delinquencyBucketOptions;
this.itemsByDefault = this.loanProductsTemplate.itemsByDefault || [];
this.loanProductSettingsForm.patchValue({
'amortizationType': this.loanProductsTemplate.amortizationType.id,
'interestType': this.loanProductsTemplate.interestType.id,
@@ -187,8 +186,8 @@ export class LoanProductSettingsStepComponent implements OnInit {
'graceOnArrearsAgeing': [true]
}),
'delinquencyBucketId': ['', Validators.required],
'dueDaysForRepaymentEvent': ['', Validators.required],
'overDueDaysForRepaymentEvent': ['', Validators.required]
'dueDaysForRepaymentEvent': [''],
'overDueDaysForRepaymentEvent': ['']
});
}
@@ -362,7 +361,7 @@ export class LoanProductSettingsStepComponent implements OnInit {
}
styleByDefault(input: string): string {
if (this.itemsByDefault.includes(input)) {
if (this.loanProducts.isItemByDefault(input)) {
return 'by-default';
}
return '';
@@ -0,0 +1,77 @@
import { Injectable } from '@angular/core';
import { SettingsService } from 'app/settings/settings.service';
import { GlobalConfiguration } from 'app/system/configurations/global-configurations-tab/configuration.model';
@Injectable({
providedIn: 'root'
})
export class LoanProducts {
globalConfigurations: string[] = ['days-before-repayment-is-due', 'days-after-repayment-is-overdue'];
propertyNames: string[] = ['dueDaysForRepaymentEvent', 'overDueDaysForRepaymentEvent'];
constructor(private settingsService: SettingsService) { }
public setItemsByDefault(configurations: any) {
const itemsByDefault: GlobalConfiguration[] = [];
configurations.globalConfiguration.forEach((config: GlobalConfiguration) => {
if (this.globalConfigurations.includes(config.name)) {
itemsByDefault.push(config);
}
});
return itemsByDefault;
}
public updateLoanProductDefaults(loanProductTemplate: any) {
const itemsByDefault: GlobalConfiguration[] = loanProductTemplate['itemsByDefault'];
itemsByDefault.forEach((config: GlobalConfiguration) => {
const propertyName = this.resolvePropertyName(config.name);
if (propertyName !== '') {
loanProductTemplate[propertyName] = config.value;
}
});
return loanProductTemplate;
}
public buildPayload(loanProductData: any, itemsByDefault: any) {
const dateFormat: string = this.settingsService.dateFormat;
const locale: string = this.settingsService.language.code;
const loanProduct = {
...loanProductData,
charges: loanProductData.charges.map((charge: any) => ({ id: charge.id })),
dateFormat,
locale
};
// Remove unnecessary properties
delete loanProduct.allowAttributeConfiguration;
delete loanProduct.advancedAccountingRules;
// Set Default values If they were not set
itemsByDefault.forEach((config: GlobalConfiguration) => {
const propertyName = this.resolvePropertyName(config.name);
if (propertyName !== '') {
if (!loanProduct[propertyName] || loanProduct[propertyName] === '') {
loanProduct[propertyName] = config.value;
}
}
});
return loanProduct;
}
public isItemByDefault(propertyName: string): boolean {
return (this.propertyNames.includes(propertyName));
}
private resolvePropertyName(configName: string): string {
if (this.globalConfigurations.includes(configName)) {
const idx = this.globalConfigurations.indexOf(configName);
if (idx > -1) {
return this.propertyNames[idx];
}
}
return '';
}
}
@@ -3,7 +3,7 @@
<div *ngIf="!hideOutput && !toBeExportedToRepo">
<div class="m-b-10" *ngIf="dataObject.report.type === 'Table'" fxLayoutAlign="end">
<button mat-raised-button color="primary" (click)="downloadCSV()" [disabled]="notExistsReportData"
<button mat-raised-button color="primary" (click)="exportFile()" [disabled]="notExistsReportData"
matToolTip="Export the data to CSV file">
Export CSV
</button>
@@ -6,6 +6,13 @@ import { DecimalPipe, NumberSymbol, getLocaleNumberSymbol } from '@angular/commo
/** Custom Servies */
import { ReportsService } from '../../reports.service';
import { MatDialog } from '@angular/material/dialog';
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
import { SelectBase } from 'app/shared/form-dialog/formfield/model/select-base';
import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base';
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
import { Dates } from 'app/core/utils/dates';
import { environment } from 'environments/environment';
/**
* Table and SMS Component
@@ -41,7 +48,9 @@ export class TableAndSmsComponent implements OnChanges {
* @param {DecimalPipe} decimalPipe Decimal Pipe
*/
constructor(private reportsService: ReportsService,
private decimalPipe: DecimalPipe ) { }
public dialog: MatDialog,
private decimalPipe: DecimalPipe,
private dateUtils: Dates) { }
/**
* Fetches run report data post changes in run report form.
@@ -85,19 +94,57 @@ export class TableAndSmsComponent implements OnChanges {
/**
* Generates the CSV file dynamically for run report data.
*/
downloadCSV() {
const userLang = navigator.language;
exportFile() {
const delimiterOptions: any[] = [
{name: 'Comma (,)', char: ','},
{name: 'Colon (:)', char: ':'},
{name: 'SemiColon (;)', char: ';'},
{name: 'Pipe (|)', char: '|'},
{name: 'Space ( )', char: ' '},
];
const fileName = `${this.dataObject.report.name}.csv`;
const formfields: FormfieldBase[] = [
new SelectBase({
controlName: 'delimiter',
label: 'Delimiter',
value: environment.defaultCharDelimiter,
options: { label: 'name', value: 'char', data: delimiterOptions },
required: true,
order: 1
}),
new InputBase({
controlName: 'fileName',
label: 'File Name',
value: fileName,
type: 'text',
required: true,
order: 2
})
];
const data = {
title: 'Export data to File',
layout: { addButtonText: 'Export to File' },
formfields: formfields
};
const exportDialogRef = this.dialog.open(FormDialogComponent, { data });
exportDialogRef.afterClosed().subscribe((response: { data: any }) => {
if (response.data) {
this.downloadCSV(response.data.value.fileName, response.data.value.delimiter);
}
});
}
/**
* Generates the CSV file dynamically for run report data.
*/
downloadCSV(fileName: string, delimiter: string) {
const headers = this.displayedColumns;
let delimiter = ',';
if (getLocaleNumberSymbol(userLang, NumberSymbol.CurrencyDecimal) !== '.') {
delimiter = ';';
}
let csv = this.csvData.map((object: any) => object.row.join(delimiter));
csv.unshift(`data:text/csv;charset=utf-8,${headers.join(delimiter)}`);
csv = csv.join('\r\n');
const link = document.createElement('a');
link.setAttribute('href', encodeURI(csv));
link.setAttribute('download', `${this.dataObject.report.name}.csv`);
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
@@ -1,6 +1,8 @@
/** Angular Imports */
import { Component, OnInit, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { DialogData } from 'app/core/dialogs/dialog-data.model';
import { Dialogs } from 'app/core/dialogs/dialogs';
/**
* Delete dialog component.
@@ -18,27 +20,15 @@ export class ConfirmationDialogComponent implements OnInit {
* @param {any} data Provides a deleteContext.
*/
constructor(public dialogRef: MatDialogRef<ConfirmationDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) { }
private dialogs: Dialogs,
@Inject(MAT_DIALOG_DATA) public data: DialogData) { }
ngOnInit() {
this.setColor();
}
setColor() {
switch (this.data.type) {
case 'Basic':
this.color = 'primary';
break;
case 'Mild':
this.color = 'primary';
break;
case 'Strong':
this.color = 'warn';
break;
default:
this.color = 'warn';
}
this.color = this.dialogs.setColor(this.data.type);
}
}
@@ -10,5 +10,5 @@
<mat-dialog-actions fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center" fxLayoutGap="2%">
<button mat-raised-button mat-dialog-close>{{ layout.cancelButtonText }}</button>
<button mat-raised-button color="primary" [mat-dialog-close]="{ data: form }" [disabled]="!form.valid || form.pristine">{{ layout.addButtonText }}</button>
<button mat-raised-button color="primary" [mat-dialog-close]="{ data: form }" [disabled]="!form.valid">{{ layout.addButtonText }}</button>
</mat-dialog-actions>
@@ -8,7 +8,6 @@ import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.co
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
import { SystemService } from 'app/system/system.service';
import { TouchSequence } from 'selenium-webdriver';
@Component({
selector: 'mifosx-datatable-single-row',
+3
View File
@@ -16,4 +16,7 @@
window["env"]["supportedLanguages"] = '';
window['env']['preloadClients'] = '';
// Char delimiter to Export CSV options: ',' ';' '|' ' '
window['env']['defaultCharDelimiter'] = ',';
})(this);
+3
View File
@@ -16,4 +16,7 @@
window["env"]["supportedLanguages"] = '$MIFOS_SUPPORTED_LANGUAGES';
window['env']['preloadClients'] = '$MIFOS_PRELOAD_CLIENTS';
// Char delimiter to Export CSV options: ',' ';' '|' ' '
window['env']['defaultCharDelimiter'] = '$MIFOS_DEFAULT_CHAR_DELIMITER';
})(this);
+4 -2
View File
@@ -10,7 +10,7 @@ export const environment = {
fineractPlatformTenantIds: window['env']['fineractPlatformTenantIds'] || 'default',
// For connecting to others servers running elsewhere update the base API URL
baseApiUrls: window['env']['fineractApiUrls'] ||
'https://dev.mifos.io,https://demo.mifos.io,https://qa.mifos.io,https://staging.mifos.io,https://mobile.mifos.io,https://demo.fineract.dev,https://localhost:8443',
'https://dev.mifos.io,https://demo.mifos.io,https://qa.mifos.io,https://staging.mifos.io,https://mobile.mifos.io,https://demo.fineract.dev,https://localhost:8443',
// For connecting to server running elsewhere set the base API URL
baseApiUrl: window['env']['fineractApiUrl'] || 'https://demo.fineract.dev',
allowServerSwitch: env.allow_switching_backend_instance,
@@ -23,7 +23,9 @@ export const environment = {
},
defaultLanguage: window['env']['defaultLanguage'] || 'en-US',
supportedLanguages: window['env']['supportedLanguages'] || 'en-US,es-MX,fr-FR',
preloadClients: window['env']['preloadClients'] || true
preloadClients: window['env']['preloadClients'] || true,
defaultCharDelimiter: window['env']['defaultCharDelimiter'] || ','
};
// Server URL
+3 -1
View File
@@ -28,7 +28,9 @@ export const environment = {
},
defaultLanguage: window['env']['defaultLanguage'] || 'en-US',
supportedLanguages: window['env']['supportedLanguages'] || 'en-US,es-MX,fr-FR',
preloadClients: window['env']['preloadClients'] || true
preloadClients: window['env']['preloadClients'] || true,
defaultCharDelimiter: window['env']['defaultCharDelimiter'] || ','
};
// Server URL