WEB-172 Merging all changes from main branch into dev branch (#2450)

This commit is contained in:
Gopi Kishan
2025-05-13 20:32:32 -06:00
committed by GitHub
parent c8d857f3cb
commit e643c64711
29 changed files with 331 additions and 95 deletions
+5 -2
View File
@@ -5,6 +5,7 @@ import { CommonModule } from '@angular/common';
/** Custom Directives */
import { HasPermissionDirective } from './has-permission/has-permission.directive';
import { FormatAmountDirective } from './format-amount.directive';
import { ValidateOnFocusDirective } from './validate-on-focus.directive';
/**
* Directives Module
@@ -17,11 +18,13 @@ import { FormatAmountDirective } from './format-amount.directive';
],
declarations: [
HasPermissionDirective,
FormatAmountDirective
FormatAmountDirective,
ValidateOnFocusDirective
],
exports: [
HasPermissionDirective,
FormatAmountDirective
FormatAmountDirective,
ValidateOnFocusDirective
]
})
export class DirectivesModule {}
@@ -0,0 +1,18 @@
import { Directive, ElementRef, HostListener } from '@angular/core';
import { NgControl } from '@angular/forms';
@Directive({
selector: '[mifosxValidateOnFocus]'
})
export class ValidateOnFocusDirective {
constructor(
private control: NgControl,
private el: ElementRef
) {}
@HostListener('focus')
onFocus() {
this.control.control?.markAsTouched();
this.control.control?.updateValueAndValidity();
}
}
@@ -42,6 +42,7 @@ export class LoansAccountScheduleStepComponent {
dateFormat
);
delete payload['enableInstallmentLevelDelinquency'];
delete payload['externalId'];
this.loansService.calculateLoanSchedule(payload).subscribe((response: any) => {
this.repaymentScheduleDetails = response;
@@ -58,6 +58,7 @@ export class MakeRepaymentComponent implements OnInit {
* and initialize with the required values
*/
ngOnInit() {
this.command = this.dataObject.type.code.split('.')[1];
this.maxDate = this.settingsService.businessDate;
this.createRepaymentLoanForm();
this.setRepaymentLoanDetails();
@@ -76,14 +77,27 @@ export class MakeRepaymentComponent implements OnInit {
this.settingsService.businessDate,
Validators.required
],
transactionAmount: [
'',
Validators.required
],
externalId: '',
paymentTypeId: '',
note: ''
});
if (this.isCapitalizedIncome()) {
this.repaymentLoanForm.addControl(
'transactionAmount',
new UntypedFormControl('', [
Validators.required,
Validators.min(0.001),
Validators.max(this.dataObject.amount)])
);
} else {
this.repaymentLoanForm.addControl(
'transactionAmount',
new UntypedFormControl('', [
Validators.required,
Validators.min(0.001)])
);
}
}
setRepaymentLoanDetails() {
@@ -114,7 +128,14 @@ export class MakeRepaymentComponent implements OnInit {
}
showDetails(): boolean {
return !['capitalizedIncome'].includes(this.command);
return !this.isCapitalizedIncome();
}
isCapitalizedIncome(): boolean {
return [
'capitalizedIncome',
'capitalizedIncomeAdjustment'
].includes(this.command);
}
/** Submits the repayment form */
@@ -212,7 +212,8 @@ export class LoanTrancheDetailsComponent implements OnInit {
item.expectedDisbursementDate,
this.settingsService.dateFormat
),
principal: item.principal
principal: item.principal,
id: item.id
});
});
@@ -194,6 +194,16 @@
<span>{{ 'labels.menus.Undo Re-Amortize' | translate }}</span>
</button>
</span>
<span *ngIf="isCapitalizedIncome(transaction.type) && !transaction.manuallyReversed">
<button
mat-menu-item
*mifosxHasPermission="'CAPITALIZED_INCOME_LOAN'"
(click)="capitalizedIncomeAdjustmentTransaction(transaction, $event)"
>
<mat-icon><fa-icon icon="coins" size="sm"></fa-icon></mat-icon>
<span>{{ 'labels.menus.Capitalized Income Adjustment' | translate }}</span>
</button>
</span>
<span *ngIf="viewJournalEntry(transaction.type)">
<button
mat-menu-item
@@ -12,6 +12,11 @@ import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/conf
import { TranslateService } from '@ngx-translate/core';
import { LoanTransaction } from 'app/products/loan-products/models/loan-account.model';
import { LoanTransactionType } from 'app/loans/models/loan-transaction-type.model';
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base';
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
import { AlertService } from 'app/core/alert/alert.service';
import { DatepickerBase } from 'app/shared/form-dialog/formfield/model/datepicker-base';
@Component({
selector: 'mifosx-transactions-tab',
@@ -80,7 +85,8 @@ export class TransactionsTabComponent implements OnInit {
private dialog: MatDialog,
private loansService: LoansService,
private translateService: TranslateService,
private settingsService: SettingsService
private settingsService: SettingsService,
private alertService: AlertService
) {
this.route.parent.parent.data.subscribe((data: { loanDetailsData: any }) => {
this.transactionsData = data.loanDetailsData.transactions;
@@ -332,6 +338,10 @@ export class TransactionsTabComponent implements OnInit {
return transactionType.reAmortize || transactionType.code === 'loanTransactionType.reAmortize';
}
private isCapitalizedIncome(transactionType: LoanTransactionType): boolean {
return transactionType.capitalizedIncome || transactionType.code === 'loanTransactionType.capitalizedIncome';
}
private isReAgoeOrReAmortize(transactionType: LoanTransactionType): boolean {
return this.isReAmortize(transactionType) || this.isReAge(transactionType);
}
@@ -354,4 +364,72 @@ export class TransactionsTabComponent implements OnInit {
}
return true;
}
capitalizedIncomeAdjustmentTransaction(transaction: LoanTransaction) {
const accountId = `${this.loanId}`;
this.loansService
.getLoanTransactionActionTemplate(accountId, 'capitalizedIncomeAdjustment', `${transaction.id}`)
.subscribe((response: any) => {
const transactionDate = response.date || transaction.date;
const transactionAmount = response.amount || transaction.amount;
const formfields: FormfieldBase[] = [
new DatepickerBase({
controlName: 'transactionDate',
label: 'Date',
value: this.dateUtils.parseDate(transactionDate),
type: 'datetime-local',
required: true,
minDate: transaction.date,
order: 1
}),
new InputBase({
controlName: 'amount',
label: 'Amount',
value: transactionAmount,
type: 'number',
required: true,
max: transactionAmount,
min: 0.001,
order: 2
})
];
const data = {
title: `Adjustment ${transaction.type.value} Transaction`,
layout: { addButtonText: 'Adjustment' },
formfields: formfields
};
const chargebackDialogRef = this.dialog.open(FormDialogComponent, { data });
chargebackDialogRef.afterClosed().subscribe((response: { data: any }) => {
if (response.data) {
const dateFormat = this.settingsService.dateFormat;
if (response.data.value.amount <= transactionAmount) {
const locale = this.settingsService.language.code;
const payload = {
transactionDate: this.dateUtils.formatDate(response.data.value.transactionDate, dateFormat),
transactionAmount: response.data.value.amount,
locale,
dateFormat
};
this.loansService
.executeLoansAccountTransactionsCommand(
accountId,
'capitalizedIncomeAdjustment',
payload,
transaction.id
)
.subscribe(() => {
this.router.navigate(['../'], { relativeTo: this.route });
});
} else {
this.alertService.alert({
type: 'BusinessRule',
message: 'Chargeback amount must be lower or equal to: ' + transaction.amount
});
}
}
});
});
}
}
+5
View File
@@ -32,6 +32,11 @@ export class LoansService {
return this.http.get(`/loans/${loanId}/transactions/template`, { params: httpParams });
}
getLoanTransactionActionTemplate(loanId: string, command: string, transactionId: string): Observable<any> {
const httpParams = new HttpParams().set('command', command).set('transactionId', transactionId);
return this.http.get(`/loans/${loanId}/transactions/template`, { params: httpParams });
}
getLoanPrepayLoanActionTemplate(loanId: string, transactionDate: string): Observable<any> {
if (!transactionDate) {
transactionDate = this.dateUtils.formatDate(this.settingsService.businessDate, this.settingsService.dateFormat);
@@ -32,4 +32,6 @@ export interface LoanTransactionType {
downPayment: boolean;
reAge: boolean;
reAmortize: boolean;
capitalizedIncome: boolean;
capitalizedIncomeAdjustment: boolean;
}
@@ -200,15 +200,22 @@
-->
<mat-form-field fxFlex="48%" *ngIf="showMinMaxCap()">
<mat-label>{{ 'labels.inputs.Minimum Charge Cap' | translate }}</mat-label>
<input matInput formControlName="minCap" />
<input matInput autofocus formControlName="minCap" mifosxValidateOnFocus />
<mat-error *ngIf="chargeForm.controls.minCap.hasError('maxValue')">
{{ 'errors.validation.msg.loanproduct.maximumGap.not.greater.than.specified.number' | translate }} ({{
chargeForm.controls.maxCap.value
}})
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%" *ngIf="showMinMaxCap()">
<mat-label>{{ 'labels.inputs.Maximum Charge Cap' | translate }}</mat-label>
<input matInput [min]="chargeForm.value.minCap" formControlName="maxCap" />
<mat-error *ngIf="chargeForm.errors?.minGreaterThanMax">
{{ 'labels.inputs.Maximum Charge Cap' | translate }} ({{ repeatEveryLabel }})
<strong>{{ 'labels.commons.must higher than Minimum Charge Cap' | translate }}</strong>
<input matInput autofocus formControlName="maxCap" mifosxValidateOnFocus />
<mat-error *ngIf="chargeForm.controls.maxCap.hasError('minValue')">
{{ 'errors.validation.msg.loanproduct.maximumGap.not.greater.than.specified.number' | translate }} ({{
chargeForm.controls.minCap.value
}})
</mat-error>
</mat-form-field>
@@ -7,7 +7,8 @@ import { Router, ActivatedRoute } from '@angular/router';
import { ProductsService } from '../../products.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
import { minLessThanOrEqualMaxValidator } from 'app/shared/validators/min-max-values.validator';
import { minNumberValueValidator } from 'app/shared/validators/min-number-value.validator';
import { maxNumberValueValidator } from 'app/shared/validators/max-number-value.validator';
/**
* Create charge component.
@@ -80,40 +81,45 @@ export class CreateChargeComponent implements OnInit {
* Creates the charge form.
*/
createChargeForm() {
this.chargeForm = this.formBuilder.group(
{
chargeAppliesTo: [
'',
Validators.required
],
name: [
'',
Validators.required
],
currencyCode: [
'',
Validators.required
],
chargeTimeType: [
'',
Validators.required
],
chargeCalculationType: [
'',
Validators.required
],
amount: [
'',
Validators.required
],
active: [false],
penalty: [false],
taxGroupId: [''],
minCap: [''],
maxCap: ['']
},
{ validators: minLessThanOrEqualMaxValidator('minCap', 'maxCap') }
);
this.chargeForm = this.formBuilder.group({
chargeAppliesTo: [
'',
Validators.required
],
name: [
'',
Validators.required
],
currencyCode: [
'',
Validators.required
],
chargeTimeType: [
'',
Validators.required
],
chargeCalculationType: [
'',
Validators.required
],
amount: [
'',
[
Validators.required,
Validators.pattern('^\\s*(?=.*[1-9])\\d*(?:\\.\\d+)?\\s*$')]
],
active: [false],
penalty: [false],
taxGroupId: [null],
minCap: [
null,
[maxNumberValueValidator('maxCap')]
],
maxCap: [
null,
[minNumberValueValidator('minCap')]
]
});
}
/**
@@ -112,12 +112,11 @@
"
>
<mat-label>{{ 'labels.inputs.Minimum Charge Cap' | translate }}</mat-label>
<input matInput autofocus formControlName="minCap" />
<mat-error
*ngIf="chargeForm.controls.maxCap.hasError('minGreaterThanMax') || chargeForm.errors?.minGreaterThanMax"
>
{{ 'labels.inputs.Minimum Charge Cap' | translate }} ({{ repeatEveryLabel }})
<strong>{{ 'labels.commons.must lower than Maximum Charge Cap' | translate }}</strong>
<input matInput autofocus formControlName="minCap" mifosxValidateOnFocus />
<mat-error *ngIf="chargeForm.controls.minCap.hasError('maxValue')">
{{ 'errors.validation.msg.loanproduct.maximumGap.not.greater.than.specified.number' | translate }} ({{
chargeForm.controls.maxCap.value
}})
</mat-error>
</mat-form-field>
<mat-form-field
@@ -137,10 +136,12 @@
"
>
<mat-label>{{ 'labels.inputs.Maximum Charge Cap' | translate }}</mat-label>
<input matInput [min]="chargeForm.value.minCap" autofocus formControlName="maxCap" />
<mat-error *ngIf="chargeForm.hasError('minGreaterThanMax') || chargeForm.errors?.minGreaterThanMax">
{{ 'labels.inputs.Maximum Charge Cap' | translate }} ({{ repeatEveryLabel }})
<strong>{{ 'labels.commons.must be equal or greater than Minimum Charge Cap' | translate }}</strong>
<input matInput autofocus formControlName="maxCap" mifosxValidateOnFocus />
<mat-error *ngIf="chargeForm.controls.maxCap.hasError('minValue')">
{{ 'errors.validation.msg.loanproduct.maximumGap.not.greater.than.specified.number' | translate }} ({{
chargeForm.controls.minCap.value
}})
</mat-error>
</mat-form-field>
@@ -6,7 +6,8 @@ import { ActivatedRoute, Router } from '@angular/router';
/** Custom Services */
import { ProductsService } from 'app/products/products.service';
import { SettingsService } from 'app/settings/settings.service';
import { minLessThanOrEqualMaxValidator } from 'app/shared/validators/min-max-values.validator';
import { maxNumberValueValidator } from 'app/shared/validators/max-number-value.validator';
import { minNumberValueValidator } from 'app/shared/validators/min-number-value.validator';
/**
* Edit Charge component.
@@ -74,39 +75,43 @@ export class EditChargeComponent implements OnInit {
editChargeForm() {
this.showFeeOptions = this.chargeData.feeInterval && this.chargeData.feeInterval > 0;
this.chargeForm = this.formBuilder.group(
{
name: [
this.chargeData.name,
Validators.required
],
chargeAppliesTo: [
{ value: this.chargeData.chargeAppliesTo.id, disabled: true },
Validators.required
],
currencyCode: [
this.chargeData.currency.code,
Validators.required
],
amount: [
this.chargeData.amount,
Validators.required
],
active: [this.chargeData.active],
penalty: [this.chargeData.penalty],
minCap: [this.chargeData.minCap],
maxCap: [this.chargeData.maxCap],
chargeTimeType: [
this.chargeData.chargeTimeType.id,
Validators.required
],
chargeCalculationType: [
this.chargeData.chargeCalculationType.id,
Validators.required
]
},
{ validators: minLessThanOrEqualMaxValidator('minCap', 'maxCap') }
);
this.chargeForm = this.formBuilder.group({
name: [
this.chargeData.name,
Validators.required
],
chargeAppliesTo: [
{ value: this.chargeData.chargeAppliesTo.id, disabled: true },
Validators.required
],
currencyCode: [
this.chargeData.currency.code,
Validators.required
],
amount: [
this.chargeData.amount,
Validators.required
],
active: [this.chargeData.active],
penalty: [this.chargeData.penalty],
minCap: [
this.chargeData.minCap || null,
,
[maxNumberValueValidator('maxCap')]
],
maxCap: [
this.chargeData.maxCap || null,
[minNumberValueValidator('minCap')]
],
chargeTimeType: [
this.chargeData.chargeTimeType.id,
Validators.required
],
chargeCalculationType: [
this.chargeData.chargeCalculationType.id,
Validators.required
]
});
switch (this.chargeData.chargeAppliesTo.value) {
case 'Loan': {
this.chargeTimeTypeOptions = this.chargeData.loanChargeTimeTypeOptions;
@@ -0,0 +1,26 @@
import { AbstractControl, ValidatorFn } from '@angular/forms';
export function maxNumberValueValidator(maxControlName: string): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } | null => {
const maxControl = control.parent?.get(maxControlName);
if (!maxControl || !control.value) {
return null;
}
let maxValue = maxControl.value;
if (typeof maxValue === 'undefined') {
return null;
}
if (typeof maxValue !== 'number') {
maxValue = maxValue * 1;
}
let controlValue = control.value;
if (typeof controlValue !== 'number') {
controlValue = controlValue * 1;
}
if (controlValue > maxValue) {
return { maxValue: { requiredMax: maxValue, actual: controlValue } };
}
return null;
};
}
@@ -0,0 +1,26 @@
import { AbstractControl, ValidatorFn } from '@angular/forms';
export function minNumberValueValidator(minControlName: string): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } | null => {
const minControl = control.parent?.get(minControlName);
if (!minControl || !control.value) {
return null;
}
let minValue = minControl.value;
if (typeof minValue === 'undefined') {
return null;
}
if (typeof minValue !== 'number') {
minValue = minValue * 1;
}
let controlValue = control.value;
if (typeof controlValue !== 'number') {
controlValue = controlValue * 1;
}
if (controlValue < minValue) {
return { minValue: { requiredMin: minValue, actual: controlValue } };
}
return null;
};
}