WEB-558:feat add loan product import via JSON upload (#2972)

fix:Added optional chaining and fallback defaults
This commit is contained in:
Shubham Kumar
2026-01-08 10:50:34 -06:00
committed by GitHub
parent 8ad47399b8
commit e49309e263
5 changed files with 193 additions and 0 deletions
@@ -0,0 +1,24 @@
<h1 mat-dialog-title>Import Loan Product</h1>
<mat-dialog-content>
<form [formGroup]="importLoanProductForm">
<p>Select a JSON file containing the loan product definition.</p>
<!-- File Upload Component -->
<mifosx-file-upload (change)="onFileSelect($event)" acceptFilter=".json"> </mifosx-file-upload>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-raised-button mat-dialog-close>
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button
mat-raised-button
color="primary"
[disabled]="!importLoanProductForm.valid"
[mat-dialog-close]="importLoanProductForm.value"
>
Import
</button>
</mat-dialog-actions>
@@ -0,0 +1,18 @@
mat-dialog-content {
padding: 20px 24px;
min-height: 150px;
p {
margin-bottom: 20px;
color: rgb(0 0 0 / 60%);
}
}
mat-dialog-actions {
padding: 8px 24px 16px;
margin: 0;
button {
margin-left: 8px;
}
}
@@ -0,0 +1,61 @@
import { Component, OnInit, inject } from '@angular/core';
import {
MatDialogRef,
MAT_DIALOG_DATA,
MatDialogTitle,
MatDialogActions,
MatDialogClose,
MatDialogContent
} from '@angular/material/dialog';
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
import { FileUploadComponent } from '../../../shared/file-upload/file-upload.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-import-loan-product-dialog',
templateUrl: './import-loan-product-dialog.component.html',
styleUrls: ['./import-loan-product-dialog.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDialogTitle,
MatDialogContent,
FileUploadComponent,
MatDialogActions,
MatDialogClose
]
})
export class ImportLoanProductDialogComponent implements OnInit {
dialogRef = inject<MatDialogRef<ImportLoanProductDialogComponent>>(MatDialogRef);
private formBuilder = inject(UntypedFormBuilder);
data = inject(MAT_DIALOG_DATA);
/** Import Loan Product form. */
importLoanProductForm: UntypedFormGroup;
ngOnInit() {
this.createImportLoanProductForm();
}
/**
* Creates the import loan product form.
*/
createImportLoanProductForm() {
this.importLoanProductForm = this.formBuilder.group({
file: [
'',
Validators.required
]
});
}
/**
* Sets file form control value.
* @param {any} $event file change event.
*/
onFileSelect($event: any) {
if ($event.target.files.length > 0) {
const file = $event.target.files[0];
this.importLoanProductForm.get('file').setValue(file);
}
}
}
@@ -5,6 +5,12 @@
{{ 'labels.buttons.Create Loan Product' | translate }}
</button>
</div>
<div class="in-block">
<button mat-raised-button color="primary" (click)="openImportDialog()" *mifosxHasPermission="'CREATE_LOANPRODUCT'">
<fa-icon icon="upload" class="m-r-10"></fa-icon>
Import
</button>
</div>
</div>
<div class="container">
@@ -2,6 +2,7 @@
import { Component, OnInit, TemplateRef, ElementRef, ViewChild, AfterViewInit, inject } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort, MatSortHeader } from '@angular/material/sort';
import { MatDialog } from '@angular/material/dialog';
import {
MatTableDataSource,
MatTable,
@@ -17,9 +18,17 @@ import {
} from '@angular/material/table';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
/** rxjs Imports */
import { switchMap, catchError, finalize } from 'rxjs/operators';
import { EMPTY } from 'rxjs';
/* Custom Services */
import { PopoverService } from '../../configuration-wizard/popover/popover.service';
import { ConfigurationWizardService } from '../../configuration-wizard/configuration-wizard.service';
import { ProductsService } from '../products.service';
import { SettingsService } from 'app/settings/settings.service';
import { ErrorHandlerService } from 'app/core/error-handler/error-handler.service';
import { ImportLoanProductDialogComponent } from './import-loan-product-dialog/import-loan-product-dialog.component';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatTooltip } from '@angular/material/tooltip';
import { StatusLookupPipe } from '../../pipes/status-lookup.pipe';
@@ -56,6 +65,10 @@ export class LoanProductsComponent implements OnInit, AfterViewInit {
private router = inject(Router);
private configurationWizardService = inject(ConfigurationWizardService);
private popoverService = inject(PopoverService);
private dialog = inject(MatDialog);
private productsService = inject(ProductsService);
private settingsService = inject(SettingsService);
private errorHandler = inject(ErrorHandlerService);
loanProductsData: any;
displayedColumns: string[] = [
@@ -157,4 +170,75 @@ export class LoanProductsComponent implements OnInit, AfterViewInit {
this.configurationWizardService.showLoanProducts = true;
this.router.navigate(['/products']);
}
/**
* Opens the import loan product dialog.
*/
openImportDialog(): void {
const importDialogRef = this.dialog.open(ImportLoanProductDialogComponent, {
width: '50rem'
});
importDialogRef.afterClosed().subscribe((response: any) => {
if (response && response.file) {
this.importLoanProduct(response.file);
}
});
}
/**
* Imports a loan product from a JSON file.
* @param {File} file The JSON file containing loan product definition.
*/
importLoanProduct(file: File): void {
const reader = new FileReader();
reader.onload = (e: any) => {
try {
const loanProductData = JSON.parse(e.target.result);
// Remove fields that shouldn't be included in creation
delete loanProductData.id;
delete loanProductData.status;
// Add required fields for API with null safety checks
const locale = this.settingsService.language?.code || 'en';
const dateFormat = this.settingsService.dateFormat || 'dd MMMM yyyy';
const payload = {
...loanProductData,
locale,
dateFormat,
// Add default required fields if not present
currencyCode: loanProductData.currencyCode || 'USD',
digitsAfterDecimal: loanProductData.digitsAfterDecimal ?? 2,
charges: loanProductData.charges || []
};
// Call API to create loan product with proper error handling
this.productsService
.createLoanProduct(payload)
.pipe(
switchMap(() => this.productsService.getLoanProducts()),
catchError((error) => this.errorHandler.handleError(error, 'Loan Product Import'))
)
.subscribe({
next: (data: any) => {
this.loanProductsData = data;
this.dataSource.data = this.loanProductsData;
this.errorHandler.showSuccess('Loan product imported successfully!');
},
error: () => {
// Error already handled by ErrorHandlerService
}
});
} catch (error) {
this.errorHandler.showInfo(
'The selected file is not a valid JSON file. Please check the file format and try again.'
);
}
};
reader.readAsText(file);
}
}