WEB-331: Introduce loan reaging preview

This commit is contained in:
Oleksii Novikov
2025-11-12 13:08:39 +02:00
parent 38faa475ba
commit 8514f996b6
24 changed files with 403 additions and 93 deletions
@@ -4,6 +4,7 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Dates } from 'app/core/utils/dates';
import { LoansService } from 'app/loans/loans.service';
import { EditableRepaymentSchedule, EditablePeriod, ScheduleChangeRecord } from 'app/loans/models/loan-account.model';
import { SettingsService } from 'app/settings/settings.service';
import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component';
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
@@ -30,11 +31,9 @@ export class EditRepaymentScheduleComponent implements OnInit {
/** Indicates If the Schedule has been validated */
wasValidated = false;
/** Stores the Repayment Schedule data */
repaymentScheduleDetails: {
periods: { period?: number; dueDate: string; totalDueForPeriod?: number; changed?: boolean }[];
} | null = null;
repaymentScheduleDetails: EditableRepaymentSchedule | null = null;
/** Stores the Installments changed */
repaymentScheduleChanges: any = {};
repaymentScheduleChanges: Record<string, ScheduleChangeRecord> = {};
/**
* @param {LoansService} systemService Loan Service.
@@ -62,14 +61,23 @@ export class EditRepaymentScheduleComponent implements OnInit {
}
getRepaymentSchedule(): void {
this.loansService.getLoanAccountResource(this.loanId, 'repaymentSchedule').subscribe((response: any) => {
this.repaymentScheduleDetails = response.repaymentSchedule;
this.loansService.getLoanAccountResource(this.loanId, 'repaymentSchedule').subscribe({
next: (response: { repaymentSchedule: EditableRepaymentSchedule }) => {
this.repaymentScheduleDetails = response.repaymentSchedule;
},
error: (err) => {
console.error('Failed to load repayment schedule:', err);
}
});
}
applyPattern(): void {
const periods: any = [];
this.repaymentScheduleDetails['periods'].forEach((period: any) => {
if (!this.repaymentScheduleDetails) {
return;
}
const periods: Array<{ idx: number; dueDate: string }> = [];
this.repaymentScheduleDetails.periods.forEach((period: EditablePeriod) => {
if (period.period) {
periods.push({
idx: period.period,
@@ -106,27 +114,29 @@ export class EditRepaymentScheduleComponent implements OnInit {
formfields: formfields
};
const addDialogRef = this.dialog.open(FormDialogComponent, { data });
addDialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
const fromPeriod = response.data.value.fromPeriod;
const toPeriod = response.data.value.toPeriod;
const amount = response.data.value.amount;
const periodsVariation: any = [];
this.repaymentScheduleDetails['periods'].forEach((period: any) => {
const dueDate = this.dateUtils.formatDate(period.dueDate, this.settingsService.dateFormat);
if (period.period && fromPeriod <= period.period && toPeriod >= period.period) {
if (period.totalDueForPeriod !== amount) {
period.totalDueForPeriod = amount;
this.repaymentScheduleChanges[dueDate] = { dueDate: dueDate, installmentAmount: amount };
this.wasChanged = true;
period['changed'] = true;
addDialogRef
.afterClosed()
.subscribe((response: { data?: { value?: { fromPeriod: number; toPeriod: number; amount: number } } }) => {
if (response.data?.value && this.repaymentScheduleDetails) {
const fromPeriod = response.data.value.fromPeriod;
const toPeriod = response.data.value.toPeriod;
const amount = response.data.value.amount;
const periodsVariation: EditablePeriod[] = [];
this.repaymentScheduleDetails.periods.forEach((period: EditablePeriod) => {
const dueDate = this.dateUtils.formatDate(period.dueDate, this.settingsService.dateFormat);
if (period.period && fromPeriod <= period.period && toPeriod >= period.period) {
if (period.totalDueForPeriod !== amount) {
period.totalDueForPeriod = amount;
this.repaymentScheduleChanges[dueDate] = { dueDate: dueDate, installmentAmount: amount };
this.wasChanged = true;
period.changed = true;
}
}
}
periodsVariation.push(period);
});
this.repaymentScheduleDetails['periods'] = periodsVariation;
}
});
periodsVariation.push(period);
});
this.repaymentScheduleDetails.periods = periodsVariation;
}
});
}
reset(): void {
@@ -138,42 +148,63 @@ export class EditRepaymentScheduleComponent implements OnInit {
)
}
});
recoverScheduleDialogRef.afterClosed().subscribe((responseConfirmation: any) => {
recoverScheduleDialogRef.afterClosed().subscribe((responseConfirmation: { confirm?: boolean }) => {
if (responseConfirmation.confirm) {
this.loansService
.applyCommandLoanScheduleVariations(this.loanId, 'deleteVariations', {})
.subscribe((response: any) => {
this.loansService.applyCommandLoanScheduleVariations(this.loanId, 'deleteVariations', {}).subscribe({
next: () => {
this.getRepaymentSchedule();
this.wasChanged = false;
this.wasValidated = false;
});
},
error: (err) => {
console.error('Failed to delete schedule variations:', err);
}
});
}
});
}
validate(): void {
if (!this.repaymentScheduleDetails) {
return;
}
this.loansService
.applyCommandLoanScheduleVariations(this.loanId, 'calculateLoanSchedule', this.getPayload())
.subscribe((response: any) => {
this.repaymentScheduleDetails['periods'] = [];
response['periods'].forEach((period: any) => {
period['changed'] = true;
this.repaymentScheduleDetails['periods'].push(period);
this.wasValidated = true;
});
.subscribe({
next: (response: EditableRepaymentSchedule) => {
if (this.repaymentScheduleDetails) {
this.repaymentScheduleDetails.periods = [];
response.periods.forEach((period: EditablePeriod) => {
period.changed = true;
this.repaymentScheduleDetails!.periods.push(period);
this.wasValidated = true;
});
}
},
error: (err) => {
console.error('Failed to calculate loan schedule:', err);
}
});
}
submit(): void {
this.loansService
.applyCommandLoanScheduleVariations(this.loanId, 'addVariations', this.getPayload())
.subscribe((response: any) => {
this.loansService.applyCommandLoanScheduleVariations(this.loanId, 'addVariations', this.getPayload()).subscribe({
next: () => {
this.router.navigate(['../../repayment-schedule'], { relativeTo: this.route });
});
},
error: (err) => {
console.error('Failed to add schedule variations:', err);
}
});
}
private getPayload(): any {
const modifiedinstallments: any = [];
private getPayload(): {
exceptions: { modifiedinstallments: ScheduleChangeRecord[] };
dateFormat: string;
locale: string;
} {
const modifiedinstallments: ScheduleChangeRecord[] = [];
Object.keys(this.repaymentScheduleChanges).forEach((key: string) => {
modifiedinstallments.push(this.repaymentScheduleChanges[key]);
});
@@ -84,6 +84,16 @@
>
{{ 'labels.buttons.Submit' | translate }}
</button>
<button
type="button"
mat-raised-button
color="accent"
[disabled]="!reagingLoanForm.valid"
*mifosxHasPermission="'REAGING_LOAN'"
(click)="preview()"
>
{{ 'labels.buttons.Preview' | translate }}
</button>
</mat-card-actions>
</mat-card-content>
</form>
@@ -1,11 +1,14 @@
import { Component, Input, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { Dates } from 'app/core/utils/dates';
import { LoansService } from 'app/loans/loans.service';
import { RepaymentSchedule } from 'app/loans/models/loan-account.model';
import { SettingsService } from 'app/settings/settings.service';
import { OptionData } from 'app/shared/models/option-data.model';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { ReAgePreviewDialogComponent } from './re-age-preview-dialog/re-age-preview-dialog.component';
@Component({
selector: 'mifosx-loan-reaging',
@@ -31,13 +34,16 @@ export class LoanReagingComponent implements OnInit {
/** Maximum Date allowed. */
maxDate = new Date();
currencyCode: string = '';
constructor(
private formBuilder: UntypedFormBuilder,
private route: ActivatedRoute,
private router: Router,
private settingsService: SettingsService,
private loanService: LoansService,
private dateUtils: Dates
private dateUtils: Dates,
private dialog: MatDialog
) {
this.loanId = this.route.snapshot.params['loanId'];
}
@@ -49,6 +55,11 @@ export class LoanReagingComponent implements OnInit {
this.reAgeInterestHandlingOptions = this.dataObject.reAgeInterestHandlingOptions;
this.periodFrequencyOptions = this.dataObject.periodFrequencyOptions;
const loanDetailsData = this.route.parent?.parent?.snapshot.data['loanDetailsData'];
if (loanDetailsData?.currency?.code) {
this.currencyCode = loanDetailsData.currency.code;
}
this.createReagingLoanForm();
}
@@ -79,21 +90,67 @@ export class LoanReagingComponent implements OnInit {
});
}
submit(): void {
const reagingLoanFormData = this.reagingLoanForm.value;
private prepareReagingData() {
const reagingLoanFormData = { ...this.reagingLoanForm.value };
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const startDate: Date = this.reagingLoanForm.value.startDate;
if (reagingLoanFormData.startDate instanceof Date) {
reagingLoanFormData.startDate = this.dateUtils.formatDate(startDate, dateFormat);
}
const data = {
if (reagingLoanFormData.reAgeInterestHandling && typeof reagingLoanFormData.reAgeInterestHandling === 'object') {
reagingLoanFormData.reAgeInterestHandling = reagingLoanFormData.reAgeInterestHandling.id;
}
return {
...reagingLoanFormData,
dateFormat,
locale
};
this.loanService.submitLoanActionButton(this.loanId, data, 'reAge').subscribe((response: any) => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
}
preview(): void {
if (this.reagingLoanForm.invalid) {
return;
}
const data = this.prepareReagingData();
this.loanService.getReAgePreview(this.loanId, data).subscribe({
next: (response: RepaymentSchedule) => {
const currencyCode = response.currency?.code || this.currencyCode;
if (!currencyCode) {
console.error('Currency code is not available in API response or loan details');
return;
}
this.dialog.open(ReAgePreviewDialogComponent, {
data: {
repaymentSchedule: response,
currencyCode: currencyCode
},
width: '95%',
maxWidth: '1400px',
height: '90vh'
});
},
error: (error) => {
console.error('Error loading re-age preview:', error);
}
});
}
submit(): void {
if (this.reagingLoanForm.invalid) {
return;
}
const data = this.prepareReagingData();
this.loanService.submitLoanActionButton(this.loanId, data, 'reAge').subscribe({
next: (response: any) => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
},
error: (error) => {
console.error('Error submitting re-age:', error);
}
});
}
}
@@ -0,0 +1,16 @@
<h1 mat-dialog-title>{{ 'labels.heading.Repayment Schedule Preview' | translate }}</h1>
<mat-dialog-content class="mat-typography">
<mifosx-repayment-schedule-tab
[repaymentScheduleDetails]="repaymentSchedule"
[currencyCode]="currencyCode"
[forEditing]="false"
>
</mifosx-repayment-schedule-tab>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-raised-button type="button" (click)="close()">
{{ 'labels.buttons.Go back' | translate }}
</button>
</mat-dialog-actions>
@@ -0,0 +1,47 @@
import { Component, Inject } from '@angular/core';
import {
MAT_DIALOG_DATA,
MatDialogRef,
MatDialogTitle,
MatDialogContent,
MatDialogActions
} from '@angular/material/dialog';
import { MatButton } from '@angular/material/button';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { RepaymentSchedule } from 'app/loans/models/loan-account.model';
import { RepaymentScheduleTabComponent } from '../../../repayment-schedule-tab/repayment-schedule-tab.component';
export interface ReAgePreviewDialogData {
repaymentSchedule: RepaymentSchedule;
currencyCode: string;
}
@Component({
selector: 'mifosx-re-age-preview-dialog',
templateUrl: './re-age-preview-dialog.component.html',
styleUrls: ['./re-age-preview-dialog.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatButton,
RepaymentScheduleTabComponent
]
})
export class ReAgePreviewDialogComponent {
repaymentSchedule: RepaymentSchedule;
currencyCode: string;
constructor(
public dialogRef: MatDialogRef<ReAgePreviewDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: ReAgePreviewDialogData
) {
this.repaymentSchedule = data.repaymentSchedule;
this.currencyCode = data.currencyCode;
}
close(): void {
this.dialogRef.close();
}
}