Skip to content

Commit 0def1ca

Browse files
WEB-813: Working Capital loan delinquency actions
1 parent 35b428f commit 0def1ca

13 files changed

Lines changed: 368 additions & 87 deletions

src/app/core/utils/dates.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ export class Dates {
5252
}
5353
}
5454

55+
public isBefore(date1: Date, date2: Date): boolean {
56+
return date1 < date2;
57+
}
58+
59+
public isAfter(date1: Date, date2: Date): boolean {
60+
return date1 > date2;
61+
}
62+
5563
public parseDatetime(value: any): Date {
5664
return moment(value).toDate();
5765
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Copyright since 2025 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
*/
8+
9+
import { Injectable } from '@angular/core';
10+
import { BehaviorSubject } from 'rxjs';
11+
import { LOAN_PRODUCT_TYPE, LoanProductType } from 'app/products/loan-products/models/loan-product.model';
12+
import { ActivatedRouteSnapshot } from '@angular/router';
13+
14+
@Injectable({
15+
providedIn: 'root'
16+
})
17+
export class LoanBaseResolver {
18+
productType = new BehaviorSubject<LoanProductType>(LOAN_PRODUCT_TYPE.LOAN);
19+
20+
constructor() {}
21+
22+
protected initialize(route: ActivatedRouteSnapshot): void {
23+
const productType = route.queryParams['productType'];
24+
if (productType !== null) {
25+
if (productType === 'loan') {
26+
this.productType.next(LOAN_PRODUCT_TYPE.LOAN);
27+
} else if (productType === 'working-capital') {
28+
this.productType.next(LOAN_PRODUCT_TYPE.WORKING_CAPITAL);
29+
}
30+
}
31+
}
32+
33+
get isWorkingCapital(): boolean {
34+
return LOAN_PRODUCT_TYPE.WORKING_CAPITAL === this.productType.value;
35+
}
36+
37+
get isLoanProduct(): boolean {
38+
return LOAN_PRODUCT_TYPE.LOAN === this.productType.value;
39+
}
40+
41+
get loanProductPath(): string {
42+
return this.isLoanProduct ? 'loanproducts' : 'working-capital-loan-products';
43+
}
44+
45+
get loanAccountPath(): string {
46+
return this.isLoanProduct ? 'loans' : 'working-capital-loans';
47+
}
48+
}

src/app/loans/common-resolvers/loan-delinquency-actions.resolver.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,27 @@ import { Injectable, inject } from '@angular/core';
1010
import { ActivatedRouteSnapshot } from '@angular/router';
1111
import { Observable } from 'rxjs';
1212
import { LoansService } from '../loans.service';
13+
import { LoanBaseResolver } from './loan-base.resolver';
1314

1415
@Injectable({
1516
providedIn: 'root'
1617
})
17-
export class LoanDelinquencyActionsResolver {
18+
export class LoanDelinquencyActionsResolver extends LoanBaseResolver {
1819
private loansService = inject(LoansService);
1920

21+
constructor() {
22+
super();
23+
}
24+
2025
/**
2126
* Returns the Loans with Association data.
2227
* @returns {Observable<any>}
2328
*/
2429
resolve(route: ActivatedRouteSnapshot): Observable<any> {
30+
this.initialize(route);
2531
const loanId = route.paramMap.get('loanId') || route.parent.paramMap.get('loanId');
26-
return this.loansService.getDelinquencyActions(loanId);
32+
if (!isNaN(+loanId)) {
33+
return this.loansService.getDelinquencyActions(this.loanAccountPath, loanId);
34+
}
2735
}
2836
}

src/app/loans/common-resolvers/loan-delinquency-data.resolver.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,29 @@ import { Observable } from 'rxjs';
1515

1616
/** Custom Services */
1717
import { LoansService } from '../loans.service';
18+
import { LoanBaseResolver } from './loan-base.resolver';
1819

1920
/**
2021
* Loan Delinquency data resolver.
2122
*/
2223
@Injectable()
23-
export class LoanDelinquencyDataResolver {
24+
export class LoanDelinquencyDataResolver extends LoanBaseResolver {
2425
private loansService = inject(LoansService);
2526

27+
constructor() {
28+
super();
29+
}
2630
/**
2731
* Returns the Loans with Association data.
2832
* @returns {Observable<any>}
2933
*/
3034
resolve(route: ActivatedRouteSnapshot): Observable<any> {
35+
this.initialize(route);
3136
const loanId = route.paramMap.get('loanId') || route.parent.paramMap.get('loanId');
32-
return this.loansService.getDelinquencyData(loanId);
37+
if (!isNaN(+loanId)) {
38+
return this.isLoanProduct
39+
? this.loansService.getDelinquencyData(loanId)
40+
: this.loansService.getWorkingCapitalLoanDetails(loanId);
41+
}
3342
}
3443
}

src/app/loans/common-resolvers/loan-delinquency-tags.resolver.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,28 @@ import { Observable } from 'rxjs';
1515

1616
/** Custom Services */
1717
import { LoansService } from '../loans.service';
18+
import { LoanBaseResolver } from './loan-base.resolver';
1819

1920
/**
20-
* Clients data resolver.
21+
* Loan Delinquency Tags data resolver.
2122
*/
2223
@Injectable()
23-
export class LoanDelinquencyTagsResolver {
24+
export class LoanDelinquencyTagsResolver extends LoanBaseResolver {
2425
private loansService = inject(LoansService);
2526

27+
constructor() {
28+
super();
29+
}
30+
2631
/**
2732
* Returns the Loans with Association data.
2833
* @returns {Observable<any>}
2934
*/
3035
resolve(route: ActivatedRouteSnapshot): Observable<any> {
36+
this.initialize(route);
3137
const loanId = route.paramMap.get('loanId') || route.parent.paramMap.get('loanId');
32-
return this.loansService.getDelinquencyTags(loanId);
38+
if (!isNaN(+loanId)) {
39+
return this.loansService.getDelinquencyTags(loanId);
40+
}
3341
}
3442
}

src/app/loans/common-resolvers/loan-details.resolver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class LoanDetailsResolver {
4040
if (resolvedProductType === LOAN_PRODUCT_TYPE.LOAN) {
4141
return this.loansService.getLoanAccountAssociationDetails(loanId);
4242
} else {
43-
return this.loansService.getWorkingCapitalLoannDetails(loanId);
43+
return this.loansService.getWorkingCapitalLoanDetails(loanId);
4444
}
4545
}
4646
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<!--
2+
Copyright since 2025 Mifos Initiative
3+
4+
This Source Code Form is subject to the terms of the Mozilla Public
5+
License, v. 2.0. If a copy of the MPL was not distributed with this
6+
file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
-->
8+
9+
<h2 mat-dialog-title>{{ 'labels.heading.Loan Delinquency Actions' | translate }}</h2>
10+
11+
<div mat-dialog-content [formGroup]="delinquencyActionForm" class="layout-column">
12+
<mat-form-field class="flex-28">
13+
<mat-label>{{ 'labels.inputs.Minimum Payment' | translate }}</mat-label>
14+
<input type="number" matInput formControlName="minimumPayment" />
15+
</mat-form-field>
16+
17+
<mat-form-field class="flex-23">
18+
<mat-label>{{ 'labels.inputs.Period Payment Frequency' | translate }}</mat-label>
19+
<input
20+
type="number"
21+
matInput
22+
required
23+
formControlName="frequency"
24+
matTooltip="{{ 'tooltips.Fields are input to calculating the repayment schedule' | translate }}"
25+
/>
26+
@if (delinquencyActionForm.controls.frequency.hasError('required')) {
27+
<mat-error>
28+
{{ 'labels.inputs.Period Payment Frequency' | translate }} {{ 'labels.commons.is' | translate }}
29+
<strong>{{ 'labels.commons.required' | translate }}</strong>
30+
</mat-error>
31+
}
32+
</mat-form-field>
33+
34+
<mat-form-field class="flex-23">
35+
<mat-label>{{ 'labels.inputs.Period Payment Frequency Type' | translate }}</mat-label>
36+
<mat-select formControlName="frequencyType" required>
37+
@for (frequencyType of frequencyTypeOptions; track frequencyType) {
38+
<mat-option [value]="frequencyType.id">
39+
{{ frequencyType.value | translateKey: 'catalogs' }}
40+
</mat-option>
41+
}
42+
</mat-select>
43+
</mat-form-field>
44+
</div>
45+
46+
<mat-dialog-actions class="layout-row layout-xs-column layout-align-center gap-2percent">
47+
<button mat-raised-button mat-dialog-close>{{ 'labels.buttons.Cancel' | translate }}</button>
48+
<button
49+
mat-raised-button
50+
color="primary"
51+
[mat-dialog-close]="{ data: delinquencyActionForm }"
52+
[disabled]="!delinquencyActionForm.valid || delinquencyActionForm.pristine"
53+
>
54+
{{ 'labels.buttons.Reschedule' | translate }}
55+
</button>
56+
</mat-dialog-actions>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* Copyright since 2025 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Copyright since 2025 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
*/
8+
9+
import { Component, inject } from '@angular/core';
10+
import { UntypedFormBuilder, UntypedFormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
11+
import {
12+
MAT_DIALOG_DATA,
13+
MatDialogRef,
14+
MatDialogTitle,
15+
MatDialogContent,
16+
MatDialogActions,
17+
MatDialogClose
18+
} from '@angular/material/dialog';
19+
import { CdkScrollable } from '@angular/cdk/scrolling';
20+
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
21+
import { MatTooltip } from '@angular/material/tooltip';
22+
import { StringEnumOptionData } from 'app/shared/models/option-data.model';
23+
24+
@Component({
25+
selector: 'mifosx-loan-delinquency-action-reschedule-dialog',
26+
templateUrl: './loan-delinquency-action-reschedule-dialog.component.html',
27+
styleUrl: './loan-delinquency-action-reschedule-dialog.component.scss',
28+
imports: [
29+
...STANDALONE_SHARED_IMPORTS,
30+
MatDialogTitle,
31+
CdkScrollable,
32+
MatDialogContent,
33+
MatDialogActions,
34+
MatDialogClose,
35+
MatTooltip
36+
]
37+
})
38+
export class LoanDelinquencyActionRescheduleDialogComponent {
39+
dialogRef = inject<MatDialogRef<LoanDelinquencyActionRescheduleDialogComponent>>(MatDialogRef);
40+
data = inject(MAT_DIALOG_DATA);
41+
private formBuilder = inject(UntypedFormBuilder);
42+
43+
delinquencyActionForm: UntypedFormGroup;
44+
45+
frequencyTypeOptions: StringEnumOptionData[] = [];
46+
47+
constructor() {
48+
this.createDelinquencyActionForm();
49+
this.frequencyTypeOptions = this.data.frequencyTypeOptions;
50+
}
51+
52+
createDelinquencyActionForm() {
53+
this.delinquencyActionForm = this.formBuilder.group({
54+
minimumPayment: [
55+
'',
56+
Validators.required
57+
],
58+
frequency: [
59+
'',
60+
Validators.required
61+
],
62+
frequencyType: [
63+
'',
64+
Validators.required
65+
]
66+
});
67+
}
68+
}

src/app/loans/loans-account-stepper/loans-account-terms-step/loans-account-terms-step.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ <h4 class="mat-h4 flex-98">
5151
/>
5252
@if (loansAccountTermsForm.controls.repaymentEvery.hasError('required')) {
5353
<mat-error>
54-
{{ 'labels.inputs.Repaid every' | translate }} {{ 'labels.commons.is' | translate }}
54+
{{ 'labels.inputs.Period Payment Frequency' | translate }} {{ 'labels.commons.is' | translate }}
5555
<strong>{{ 'labels.commons.required' | translate }}</strong>
5656
</mat-error>
5757
}

0 commit comments

Comments
 (0)