WEB-95: WEB-95 Fix problems with GLIM application: saved total, "Moratorium", "Overdue Charges""

This commit is contained in:
Soni Jay 2025-11-28 14:19:15 +05:30 committed by JaySoni1
parent b7ef0fcbd9
commit ce985b1390
7 changed files with 120 additions and 117 deletions

View File

@ -9,6 +9,7 @@
/** Angular Imports */
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { I18nService } from './core/i18n/i18n.service';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {
HttpBackend,
@ -139,7 +140,8 @@ export function HttpLoaderFactory(http: HttpClient) {
provide: HTTP_INTERCEPTORS,
useClass: !environment.OIDC.oidcServerEnabled ? TokenInterceptor : ZitadelTokenInterceptor,
multi: true
}
},
I18nService
]
})
export class AppModule {}

View File

@ -185,58 +185,42 @@ export class CreateGlimAccountComponent {
};
}
setData(client: any, totalLoan: number): any {
setData(applicationId: number, client: any, totalLoan: number, isFirst: boolean, isLast: boolean): any {
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
// const monthDayFormat = 'dd MMMM';
const data = {
const data: any = {
...this.loansAccount,
charges: (this.loansAccount.charges ?? [])
.map((charge: any) => {
const chargeId = charge.chargeId ?? charge.id;
if (chargeId == null) {
return null;
}
const mappedCharge: any = {
chargeId,
amount: charge.amount
};
if (charge.id && charge.id !== chargeId) {
mappedCharge.id = charge.id;
}
if (charge.dueDate) {
mappedCharge.dueDate = this.dateUtils.formatDate(charge.dueDate, dateFormat);
}
if (charge.feeInterval !== undefined) {
mappedCharge.feeInterval = charge.feeInterval;
}
if (charge.feeOnMonthDay !== undefined) {
mappedCharge.feeOnMonthDay = charge.feeOnMonthDay;
}
return mappedCharge;
})
.filter(Boolean),
charges: this.loansAccount.charges.map((charge: any) => ({
chargeId: charge.id,
amount: charge.amount,
currency: charge.currency
})),
clientId: client.id,
totalLoan: totalLoan,
loanType: 'glim',
applicationId: applicationId,
amortizationType: 1,
isParentAccount: true,
principal: client.principal,
syncDisbursementWithMeeting: false,
expectedDisbursementDate: this.dateUtils.formatDate(this.loansAccount.expectedDisbursementDate, dateFormat),
submittedOnDate: this.dateUtils.formatDate(this.loansAccount.submittedOnDate, dateFormat),
dateFormat,
// monthDayFormat,
locale
dateFormat: dateFormat,
locale: locale,
groupId: this.loansAccountTemplate.group.id
};
data.groupId = this.loansAccountTemplate.group.id;
if (isFirst) {
data.isParentAccount = true;
}
if (isLast) {
data.lastApplication = true;
}
delete data.principalAmount;
// TODO: 2025-03-17: Apparently (?!) unsupported for GLIM
delete data.allowPartialPeriodInterestCalculation;
delete data.multiDisburseLoan;
delete data.isFloatingInterestRate;
delete data.moratoriumPrincipal;
delete data.moratoriumInterest;
return JSON.stringify(data);
}
@ -245,12 +229,17 @@ export class CreateGlimAccountComponent {
const requestData = [];
const memberSelected = this.selectedMembers?.selectedMembers ?? [];
const totalLoan = this.totalLoanAmount();
const applicationId = Math.floor(1000000000 + Math.random() * 9000000000);
for (let index = 0; index < memberSelected.length; index++) {
const isFirst = index === 0;
const isLast = index === memberSelected.length - 1;
requestData.push({
requestId: index.toString(),
reference: index === 0 ? null : (index - 1).toString(),
method: 'POST',
relativeUrl: 'loans',
body: this.setData(memberSelected[index], totalLoan)
body: this.setData(applicationId, memberSelected[index], totalLoan, isFirst, isLast)
});
}
return requestData;
@ -265,57 +254,10 @@ export class CreateGlimAccountComponent {
return total;
}
/**
* Creates a new GLIM account.
*/
submit() {
this.selectedMembers = this.loansActiveClientMembers?.selectedClientMembers;
const memberSelected = this.loansActiveClientMembers?.selectedClientMembers?.selectedMembers ?? [];
if (!memberSelected.length) return;
const gsimMemberIds = new Set(this.dataSource.map((m: any) => Number(m.id)));
for (const member of memberSelected) {
const memberId = Number(member.id);
// Validate savings account ownership
const ownerId = Number(member.linkAccountOwnerId);
if (member.linkAccountId && member.linkAccountOwnerId && ownerId !== memberId) {
this.i18nService.translate('errors.linkedSavingsAccountOwnership').subscribe((msg: string) => {
this.notify({ defaultUserMessage: msg, errors: [] }, { memberId });
});
return;
}
// Validate GSIM membership
if (!gsimMemberIds.has(memberId)) {
this.i18nService.translate('errors.clientNotInGSIM', { id: memberId }).subscribe((msg: string) => {
this.notify({ defaultUserMessage: msg, errors: [] }, { memberId });
});
return;
}
}
// Use date format from settingsService for interestChargedFromDate
const data = this.buildRequestData();
this.loansService.createGlimAccount(data).subscribe((response: any) => {
const body = JSON.parse(response[0].body);
if (body.glimId) {
this.router.navigate(
[
'../',
body.glimId
],
{ relativeTo: this.route }
);
} else {
this.notify(body, { batchSize: data.length });
}
});
}
notify(body: any, context?: { [k: string]: unknown }) {
const parts: string[] = [String(body?.defaultUserMessage ?? '')];
if (Array.isArray(body?.errors)) {
for (const e of body.errors) parts.push(String(e?.developerMessage ?? ''));
}
if (context) parts.push(`Context: ${JSON.stringify(context)}`);
console.error(parts.join(' ').trim());
notify(body: any, data: any) {
let message = body.defaultUserMessage + ' ';
body.errors?.forEach((error: any) => (message += error.developerMessage + ' '));
message += 'Data: ' + JSON.stringify(data);
console.error(message);
}
}

View File

@ -34,7 +34,7 @@
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.name' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.name + ', ' + (charge.currency?.displaySymbol || '') }}
{{ charge.name }}<span *ngIf="charge.currency">, {{ charge.currency.displaySymbol }}</span>
</td>
</ng-container>
@ -49,7 +49,13 @@
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.amount }}
<button mat-icon-button color="primary" (click)="editChargeAmount(charge)">
<button
mat-icon-button
color="primary"
(click)="editChargeAmount(charge)"
type="button"
aria-label="Edit charge amount"
>
<fa-icon icon="pen"></fa-icon>
</button>
</td>
@ -102,7 +108,7 @@
<ng-container matColumnDef="action">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let charge">
<button mat-icon-button color="warn" (click)="deleteCharge(charge)">
<button mat-icon-button color="warn" (click)="deleteCharge(charge)" type="button" aria-label="Delete charge">
<fa-icon icon="trash"></fa-icon>
</button>
</td>
@ -119,7 +125,9 @@
<table mat-table class="mat-elevation-z1" [dataSource]="overDueChargesDataSource">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.name' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.name + ', ' + (charge.currency?.displaySymbol || '') }}</td>
<td mat-cell *matCellDef="let charge">
{{ charge.name }}<span *ngIf="charge.currency">, {{ charge.currency.displaySymbol }}</span>
</td>
</ng-container>
<ng-container matColumnDef="type">
@ -129,7 +137,18 @@
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.amount | formatNumber }}</td>
<td mat-cell *matCellDef="let charge">
{{ charge.amount | formatNumber }}
<button
mat-icon-button
color="primary"
(click)="editOverdueChargeAmount(charge)"
type="button"
aria-label="Edit overdue charge amount"
>
<fa-icon icon="pen"></fa-icon>
</button>
</td>
</ng-container>
<ng-container matColumnDef="collectedon">
@ -143,17 +162,15 @@
</div>
<div class="layout-row responsive-column align-center gap-2px margin-t">
<button mat-raised-button matStepperPrevious>
<button mat-raised-button matStepperPrevious type="button">
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
<button mat-raised-button matStepperNext type="button">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
@if (loanId) {
<button mat-raised-button [routerLink]="['../', 'general']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
}
<button mat-raised-button *ngIf="loanId" [routerLink]="['../', 'general']" type="button">
{{ 'labels.buttons.Cancel' | translate }}
</button>
</div>

View File

@ -331,6 +331,32 @@ export class LoansAccountChargesStepComponent implements OnInit, OnChanges {
});
}
editOverdueChargeAmount(charge: { amount: number; [key: string]: any }) {
const formfields: FormfieldBase[] = [
new InputBase({
controlName: 'amount',
label: 'Amount',
value: charge.amount,
type: 'number',
required: false
})
];
const data = {
title: 'Edit Overdue Charge Amount',
layout: { addButtonText: 'Confirm' },
formfields: formfields
};
const editNoteDialogRef = this.dialog.open(FormDialogComponent, { data });
editNoteDialogRef.afterClosed().subscribe((response?: { data?: { value: { amount: number } } }) => {
if (response?.data) {
const newCharge = { ...charge, amount: response.data.value.amount };
this.overDueChargesDataSource.splice(this.overDueChargesDataSource.indexOf(charge), 1, newCharge);
this.overDueChargesDataSource = this.overDueChargesDataSource.concat([]);
this.pristine = false;
}
});
}
get isValid() {
return true;
// !this.activeClientMembers ||

View File

@ -344,10 +344,10 @@
}
@if (
!(
charge.chargeTimeType.value === 'Monthly Fee' ||
charge.chargeTimeType.value === 'Annual Fee' ||
charge.chargeTimeType.value === 'Specified due date' ||
charge.chargeTimeType.value === 'Weekly Fee'
charge.chargeTimeType?.value === 'Monthly Fee' ||
charge.chargeTimeType?.value === 'Annual Fee' ||
charge.chargeTimeType?.value === 'Specified due date' ||
charge.chargeTimeType?.value === 'Weekly Fee'
)
) {
<span>

View File

@ -73,6 +73,7 @@
matInput
formControlName="numberOfRepayments"
matTooltip="{{ 'tooltips.Enter the total count of repayments' | translate }}"
aria-label="Number of repayments"
/>
@if (loansAccountTermsForm.controls.numberOfRepayments.hasError('required')) {
<mat-error>
@ -98,6 +99,7 @@
matTooltip="{{ 'tooltips.May be entered to override' | translate }}"
[matDatepicker]="repaymentsPicker"
formControlName="repaymentsStartingFromDate"
aria-label="First repayment on"
/>
<mat-datepicker-toggle matSuffix [for]="repaymentsPicker"></mat-datepicker-toggle>
<mat-datepicker #repaymentsPicker></mat-datepicker>
@ -112,6 +114,7 @@
[matDatepicker]="interestPicker"
matTooltip="{{ 'tooltips.May be entered to override the date' | translate }}"
formControlName="interestChargedFromDate"
aria-label="Interest charged from"
/>
<mat-datepicker-toggle matSuffix [for]="interestPicker"></mat-datepicker-toggle>
<mat-datepicker #interestPicker></mat-datepicker>
@ -133,6 +136,7 @@
required
formControlName="repaymentEvery"
matTooltip="{{ 'tooltips.Fields are input to calculating the repayment schedule' | translate }}"
aria-label="Repaid every"
/>
@if (loansAccountTermsForm.controls.repaymentEvery.hasError('required')) {
<mat-error>
@ -198,7 +202,7 @@
@if (!loansAccountTermsData?.isLoanProductLinkedToFloatingRate) {
<mat-form-field class="flex-fill flex-23">
<mat-label>{{ 'labels.inputs.Nominal interest rate' | translate }} %</mat-label>
<input type="number" matInput formControlName="interestRatePerPeriod" />
<input type="number" matInput formControlName="interestRatePerPeriod" aria-label="Nominal interest rate" />
</mat-form-field>
<mat-form-field class="flex-fill flex-23">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
@ -375,6 +379,7 @@
type="number"
formControlName="inArrearsTolerance"
matTooltip="{{ 'tooltips.With Arrears tolerance' | translate }}"
aria-label="Arrears tolerance"
/>
</mat-form-field>
@ -384,6 +389,7 @@
matInput
formControlName="graceOnInterestCharged"
matTooltip="{{ 'tooltips.If the Interest Free Period' | translate }}"
aria-label="Interest free period"
/>
</mat-form-field>
@ -394,17 +400,17 @@
<mat-form-field class="flex-fill flex-23">
<mat-label>{{ 'labels.inputs.Grace on principal payment' | translate }}</mat-label>
<input type="number" matInput formControlName="graceOnPrincipalPayment" />
<input type="number" matInput formControlName="graceOnPrincipalPayment" aria-label="Grace on principal payment" />
</mat-form-field>
<mat-form-field class="flex-fill flex-23">
<mat-label>{{ 'labels.inputs.Grace on interest payment' | translate }}</mat-label>
<input type="number" matInput formControlName="graceOnInterestPayment" />
<input type="number" matInput formControlName="graceOnInterestPayment" aria-label="Grace on interest payment" />
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.On arrears ageing' | translate }}</mat-label>
<input type="number" matInput formControlName="graceOnArrearsAgeing" />
<input type="number" matInput formControlName="graceOnArrearsAgeing" aria-label="On arrears ageing" />
</mat-form-field>
@if (isDelinquencyEnabled()) {
@ -575,6 +581,7 @@
type="button"
mat-icon-button
color="primary"
aria-label="Add Disbursement Data Entry"
required
(click)="addDisbursementDataEntry(disbursementData)"
[disabled]="isMultiDisbursedCompleted"
@ -610,6 +617,7 @@
type="button"
mat-icon-button
color="warn"
aria-label="Remove Disbursement Data Entry"
(click)="removeDisbursementDataEntry(rowIndex)"
matTooltip="{{ 'tooltips.Delete' | translate }}"
matTooltipPosition="left"
@ -685,7 +693,7 @@
<div class="layout-column flex-50">
<div class="layout-row align-flex-end">
<button mat-raised-button color="primary" (click)="addCollateral()">
<button mat-raised-button color="primary" (click)="addCollateral()" type="button">
<fa-icon icon="plus" class="m-r-10"></fa-icon>{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@ -715,7 +723,13 @@
<ng-container matColumnDef="action">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let ele; let collateralIndex = index">
<button mat-icon-button color="warn" (click)="deleteCollateral(collateralIndex)">
<button
mat-icon-button
color="warn"
(click)="deleteCollateral(collateralIndex)"
aria-label="Delete Collateral"
type="button"
>
<fa-icon icon="trash"></fa-icon>
</button>
</td>
@ -727,11 +741,11 @@
</div>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<button mat-raised-button matStepperPrevious type="button" aria-label="Previous">
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
<button mat-raised-button matStepperNext type="button" aria-label="Next">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>

View File

@ -16,8 +16,9 @@ import { finalize } from 'rxjs/operators';
/** Custom Services */
import { AuthenticationService } from '../../core/authentication/authentication.service';
import { MatPrefix } from '@angular/material/form-field';
import { M3IconComponent } from '../../shared/m3-ui/m3-icon/m3-icon.component';
import { M3ButtonComponent } from '../../shared/m3-ui/m3-button/m3-button.component';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatIconButton, MatButton } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatProgressBar } from '@angular/material/progress-bar';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@ -34,8 +35,9 @@ import { environment } from '../../../environments/environment';
imports: [
...STANDALONE_SHARED_IMPORTS,
MatPrefix,
M3IconComponent,
M3ButtonComponent,
FaIconComponent,
MatIconButton,
MatCheckboxModule,
MatProgressBar,
MatProgressSpinner
]