Skip to content

Commit 0b783cd

Browse files
authored
Merge pull request #2691 from oleksii-novikov-onix/WEB-331/loan-reaging-preview
WEB-331: Introduce loan reaging preview
2 parents a1370e0 + 8514f99 commit 0b783cd

24 files changed

+403
-93
lines changed

src/app/loans/loans-account-stepper/loans-account-schedule-step/loans-account-schedule-step.component.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Component, Input } from '@angular/core';
22
import { ActivatedRoute, RouterLink } from '@angular/router';
33
import { LoansService } from 'app/loans/loans.service';
4+
import { RepaymentSchedule } from 'app/loans/models/loan-account.model';
45
import { SettingsService } from 'app/settings/settings.service';
56
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
67
import { RepaymentScheduleTabComponent } from '../../loans-view/repayment-schedule-tab/repayment-schedule-tab.component';
@@ -23,15 +24,15 @@ export class LoansAccountScheduleStepComponent {
2324
/** Currency Code */
2425
@Input() currencyCode: string;
2526
/** Loans Account Template */
26-
@Input() loansAccountTemplate: any;
27+
@Input() loansAccountTemplate: Record<string, unknown>;
2728
/** Loans Account Product Template */
28-
@Input() loansAccountProductTemplate: any;
29+
@Input() loansAccountProductTemplate: { calendarOptions?: unknown };
2930
/** Loans Account Data */
30-
@Input() loansAccount: any;
31+
@Input() loansAccount: Record<string, unknown>;
3132

32-
repaymentScheduleDetails: any = { periods: [] };
33+
repaymentScheduleDetails: RepaymentSchedule | null = null;
3334

34-
loanId: any = null;
35+
loanId: string | null = null;
3536

3637
constructor(
3738
private loansService: LoansService,
@@ -42,7 +43,7 @@ export class LoansAccountScheduleStepComponent {
4243
}
4344

4445
showRepaymentInfo(): void {
45-
this.repaymentScheduleDetails = { periods: [] };
46+
this.repaymentScheduleDetails = null;
4647
const locale = this.settingsService.language.code;
4748
const dateFormat = this.settingsService.dateFormat;
4849
const payload = this.loansService.buildLoanRequestPayload(
@@ -55,7 +56,7 @@ export class LoansAccountScheduleStepComponent {
5556
delete payload['enableInstallmentLevelDelinquency'];
5657
delete payload['externalId'];
5758

58-
this.loansService.calculateLoanSchedule(payload).subscribe((response: any) => {
59+
this.loansService.calculateLoanSchedule(payload).subscribe((response: RepaymentSchedule) => {
5960
this.repaymentScheduleDetails = response;
6061
});
6162
}

src/app/loans/loans-view/loan-account-actions/edit-repayment-schedule/edit-repayment-schedule.component.ts

Lines changed: 77 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router';
44
import { TranslateService } from '@ngx-translate/core';
55
import { Dates } from 'app/core/utils/dates';
66
import { LoansService } from 'app/loans/loans.service';
7+
import { EditableRepaymentSchedule, EditablePeriod, ScheduleChangeRecord } from 'app/loans/models/loan-account.model';
78
import { SettingsService } from 'app/settings/settings.service';
89
import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component';
910
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
@@ -30,11 +31,9 @@ export class EditRepaymentScheduleComponent implements OnInit {
3031
/** Indicates If the Schedule has been validated */
3132
wasValidated = false;
3233
/** Stores the Repayment Schedule data */
33-
repaymentScheduleDetails: {
34-
periods: { period?: number; dueDate: string; totalDueForPeriod?: number; changed?: boolean }[];
35-
} | null = null;
34+
repaymentScheduleDetails: EditableRepaymentSchedule | null = null;
3635
/** Stores the Installments changed */
37-
repaymentScheduleChanges: any = {};
36+
repaymentScheduleChanges: Record<string, ScheduleChangeRecord> = {};
3837

3938
/**
4039
* @param {LoansService} systemService Loan Service.
@@ -62,14 +61,23 @@ export class EditRepaymentScheduleComponent implements OnInit {
6261
}
6362

6463
getRepaymentSchedule(): void {
65-
this.loansService.getLoanAccountResource(this.loanId, 'repaymentSchedule').subscribe((response: any) => {
66-
this.repaymentScheduleDetails = response.repaymentSchedule;
64+
this.loansService.getLoanAccountResource(this.loanId, 'repaymentSchedule').subscribe({
65+
next: (response: { repaymentSchedule: EditableRepaymentSchedule }) => {
66+
this.repaymentScheduleDetails = response.repaymentSchedule;
67+
},
68+
error: (err) => {
69+
console.error('Failed to load repayment schedule:', err);
70+
}
6771
});
6872
}
6973

7074
applyPattern(): void {
71-
const periods: any = [];
72-
this.repaymentScheduleDetails['periods'].forEach((period: any) => {
75+
if (!this.repaymentScheduleDetails) {
76+
return;
77+
}
78+
79+
const periods: Array<{ idx: number; dueDate: string }> = [];
80+
this.repaymentScheduleDetails.periods.forEach((period: EditablePeriod) => {
7381
if (period.period) {
7482
periods.push({
7583
idx: period.period,
@@ -106,27 +114,29 @@ export class EditRepaymentScheduleComponent implements OnInit {
106114
formfields: formfields
107115
};
108116
const addDialogRef = this.dialog.open(FormDialogComponent, { data });
109-
addDialogRef.afterClosed().subscribe((response: any) => {
110-
if (response.data) {
111-
const fromPeriod = response.data.value.fromPeriod;
112-
const toPeriod = response.data.value.toPeriod;
113-
const amount = response.data.value.amount;
114-
const periodsVariation: any = [];
115-
this.repaymentScheduleDetails['periods'].forEach((period: any) => {
116-
const dueDate = this.dateUtils.formatDate(period.dueDate, this.settingsService.dateFormat);
117-
if (period.period && fromPeriod <= period.period && toPeriod >= period.period) {
118-
if (period.totalDueForPeriod !== amount) {
119-
period.totalDueForPeriod = amount;
120-
this.repaymentScheduleChanges[dueDate] = { dueDate: dueDate, installmentAmount: amount };
121-
this.wasChanged = true;
122-
period['changed'] = true;
117+
addDialogRef
118+
.afterClosed()
119+
.subscribe((response: { data?: { value?: { fromPeriod: number; toPeriod: number; amount: number } } }) => {
120+
if (response.data?.value && this.repaymentScheduleDetails) {
121+
const fromPeriod = response.data.value.fromPeriod;
122+
const toPeriod = response.data.value.toPeriod;
123+
const amount = response.data.value.amount;
124+
const periodsVariation: EditablePeriod[] = [];
125+
this.repaymentScheduleDetails.periods.forEach((period: EditablePeriod) => {
126+
const dueDate = this.dateUtils.formatDate(period.dueDate, this.settingsService.dateFormat);
127+
if (period.period && fromPeriod <= period.period && toPeriod >= period.period) {
128+
if (period.totalDueForPeriod !== amount) {
129+
period.totalDueForPeriod = amount;
130+
this.repaymentScheduleChanges[dueDate] = { dueDate: dueDate, installmentAmount: amount };
131+
this.wasChanged = true;
132+
period.changed = true;
133+
}
123134
}
124-
}
125-
periodsVariation.push(period);
126-
});
127-
this.repaymentScheduleDetails['periods'] = periodsVariation;
128-
}
129-
});
135+
periodsVariation.push(period);
136+
});
137+
this.repaymentScheduleDetails.periods = periodsVariation;
138+
}
139+
});
130140
}
131141

132142
reset(): void {
@@ -138,42 +148,63 @@ export class EditRepaymentScheduleComponent implements OnInit {
138148
)
139149
}
140150
});
141-
recoverScheduleDialogRef.afterClosed().subscribe((responseConfirmation: any) => {
151+
recoverScheduleDialogRef.afterClosed().subscribe((responseConfirmation: { confirm?: boolean }) => {
142152
if (responseConfirmation.confirm) {
143-
this.loansService
144-
.applyCommandLoanScheduleVariations(this.loanId, 'deleteVariations', {})
145-
.subscribe((response: any) => {
153+
this.loansService.applyCommandLoanScheduleVariations(this.loanId, 'deleteVariations', {}).subscribe({
154+
next: () => {
146155
this.getRepaymentSchedule();
147156
this.wasChanged = false;
148157
this.wasValidated = false;
149-
});
158+
},
159+
error: (err) => {
160+
console.error('Failed to delete schedule variations:', err);
161+
}
162+
});
150163
}
151164
});
152165
}
153166

154167
validate(): void {
168+
if (!this.repaymentScheduleDetails) {
169+
return;
170+
}
171+
155172
this.loansService
156173
.applyCommandLoanScheduleVariations(this.loanId, 'calculateLoanSchedule', this.getPayload())
157-
.subscribe((response: any) => {
158-
this.repaymentScheduleDetails['periods'] = [];
159-
response['periods'].forEach((period: any) => {
160-
period['changed'] = true;
161-
this.repaymentScheduleDetails['periods'].push(period);
162-
this.wasValidated = true;
163-
});
174+
.subscribe({
175+
next: (response: EditableRepaymentSchedule) => {
176+
if (this.repaymentScheduleDetails) {
177+
this.repaymentScheduleDetails.periods = [];
178+
response.periods.forEach((period: EditablePeriod) => {
179+
period.changed = true;
180+
this.repaymentScheduleDetails!.periods.push(period);
181+
this.wasValidated = true;
182+
});
183+
}
184+
},
185+
error: (err) => {
186+
console.error('Failed to calculate loan schedule:', err);
187+
}
164188
});
165189
}
166190

167191
submit(): void {
168-
this.loansService
169-
.applyCommandLoanScheduleVariations(this.loanId, 'addVariations', this.getPayload())
170-
.subscribe((response: any) => {
192+
this.loansService.applyCommandLoanScheduleVariations(this.loanId, 'addVariations', this.getPayload()).subscribe({
193+
next: () => {
171194
this.router.navigate(['../../repayment-schedule'], { relativeTo: this.route });
172-
});
195+
},
196+
error: (err) => {
197+
console.error('Failed to add schedule variations:', err);
198+
}
199+
});
173200
}
174201

175-
private getPayload(): any {
176-
const modifiedinstallments: any = [];
202+
private getPayload(): {
203+
exceptions: { modifiedinstallments: ScheduleChangeRecord[] };
204+
dateFormat: string;
205+
locale: string;
206+
} {
207+
const modifiedinstallments: ScheduleChangeRecord[] = [];
177208
Object.keys(this.repaymentScheduleChanges).forEach((key: string) => {
178209
modifiedinstallments.push(this.repaymentScheduleChanges[key]);
179210
});

src/app/loans/loans-view/loan-account-actions/loan-reaging/loan-reaging.component.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,16 @@
8484
>
8585
{{ 'labels.buttons.Submit' | translate }}
8686
</button>
87+
<button
88+
type="button"
89+
mat-raised-button
90+
color="accent"
91+
[disabled]="!reagingLoanForm.valid"
92+
*mifosxHasPermission="'REAGING_LOAN'"
93+
(click)="preview()"
94+
>
95+
{{ 'labels.buttons.Preview' | translate }}
96+
</button>
8797
</mat-card-actions>
8898
</mat-card-content>
8999
</form>

src/app/loans/loans-view/loan-account-actions/loan-reaging/loan-reaging.component.ts

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { Component, Input, OnInit } from '@angular/core';
22
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
33
import { ActivatedRoute, Router } from '@angular/router';
4+
import { MatDialog } from '@angular/material/dialog';
45
import { Dates } from 'app/core/utils/dates';
56
import { LoansService } from 'app/loans/loans.service';
7+
import { RepaymentSchedule } from 'app/loans/models/loan-account.model';
68
import { SettingsService } from 'app/settings/settings.service';
79
import { OptionData } from 'app/shared/models/option-data.model';
810
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
11+
import { ReAgePreviewDialogComponent } from './re-age-preview-dialog/re-age-preview-dialog.component';
912

1013
@Component({
1114
selector: 'mifosx-loan-reaging',
@@ -31,13 +34,16 @@ export class LoanReagingComponent implements OnInit {
3134
/** Maximum Date allowed. */
3235
maxDate = new Date();
3336

37+
currencyCode: string = '';
38+
3439
constructor(
3540
private formBuilder: UntypedFormBuilder,
3641
private route: ActivatedRoute,
3742
private router: Router,
3843
private settingsService: SettingsService,
3944
private loanService: LoansService,
40-
private dateUtils: Dates
45+
private dateUtils: Dates,
46+
private dialog: MatDialog
4147
) {
4248
this.loanId = this.route.snapshot.params['loanId'];
4349
}
@@ -49,6 +55,11 @@ export class LoanReagingComponent implements OnInit {
4955
this.reAgeInterestHandlingOptions = this.dataObject.reAgeInterestHandlingOptions;
5056
this.periodFrequencyOptions = this.dataObject.periodFrequencyOptions;
5157

58+
const loanDetailsData = this.route.parent?.parent?.snapshot.data['loanDetailsData'];
59+
if (loanDetailsData?.currency?.code) {
60+
this.currencyCode = loanDetailsData.currency.code;
61+
}
62+
5263
this.createReagingLoanForm();
5364
}
5465

@@ -79,21 +90,67 @@ export class LoanReagingComponent implements OnInit {
7990
});
8091
}
8192

82-
submit(): void {
83-
const reagingLoanFormData = this.reagingLoanForm.value;
93+
private prepareReagingData() {
94+
const reagingLoanFormData = { ...this.reagingLoanForm.value };
8495
const locale = this.settingsService.language.code;
8596
const dateFormat = this.settingsService.dateFormat;
8697
const startDate: Date = this.reagingLoanForm.value.startDate;
8798
if (reagingLoanFormData.startDate instanceof Date) {
8899
reagingLoanFormData.startDate = this.dateUtils.formatDate(startDate, dateFormat);
89100
}
90-
const data = {
101+
if (reagingLoanFormData.reAgeInterestHandling && typeof reagingLoanFormData.reAgeInterestHandling === 'object') {
102+
reagingLoanFormData.reAgeInterestHandling = reagingLoanFormData.reAgeInterestHandling.id;
103+
}
104+
return {
91105
...reagingLoanFormData,
92106
dateFormat,
93107
locale
94108
};
95-
this.loanService.submitLoanActionButton(this.loanId, data, 'reAge').subscribe((response: any) => {
96-
this.router.navigate(['../../transactions'], { relativeTo: this.route });
109+
}
110+
111+
preview(): void {
112+
if (this.reagingLoanForm.invalid) {
113+
return;
114+
}
115+
const data = this.prepareReagingData();
116+
117+
this.loanService.getReAgePreview(this.loanId, data).subscribe({
118+
next: (response: RepaymentSchedule) => {
119+
const currencyCode = response.currency?.code || this.currencyCode;
120+
121+
if (!currencyCode) {
122+
console.error('Currency code is not available in API response or loan details');
123+
return;
124+
}
125+
126+
this.dialog.open(ReAgePreviewDialogComponent, {
127+
data: {
128+
repaymentSchedule: response,
129+
currencyCode: currencyCode
130+
},
131+
width: '95%',
132+
maxWidth: '1400px',
133+
height: '90vh'
134+
});
135+
},
136+
error: (error) => {
137+
console.error('Error loading re-age preview:', error);
138+
}
139+
});
140+
}
141+
142+
submit(): void {
143+
if (this.reagingLoanForm.invalid) {
144+
return;
145+
}
146+
const data = this.prepareReagingData();
147+
this.loanService.submitLoanActionButton(this.loanId, data, 'reAge').subscribe({
148+
next: (response: any) => {
149+
this.router.navigate(['../../transactions'], { relativeTo: this.route });
150+
},
151+
error: (error) => {
152+
console.error('Error submitting re-age:', error);
153+
}
97154
});
98155
}
99156
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<h1 mat-dialog-title>{{ 'labels.heading.Repayment Schedule Preview' | translate }}</h1>
2+
3+
<mat-dialog-content class="mat-typography">
4+
<mifosx-repayment-schedule-tab
5+
[repaymentScheduleDetails]="repaymentSchedule"
6+
[currencyCode]="currencyCode"
7+
[forEditing]="false"
8+
>
9+
</mifosx-repayment-schedule-tab>
10+
</mat-dialog-content>
11+
12+
<mat-dialog-actions align="end">
13+
<button mat-raised-button type="button" (click)="close()">
14+
{{ 'labels.buttons.Go back' | translate }}
15+
</button>
16+
</mat-dialog-actions>

src/app/loans/loans-view/loan-account-actions/loan-reaging/re-age-preview-dialog/re-age-preview-dialog.component.scss

Whitespace-only changes.

0 commit comments

Comments
 (0)