mirror of
https://github.com/openMF/web-app.git
synced 2026-02-06 14:11:48 +00:00
Merge ce985b1390 into b7ef0fcbd9
This commit is contained in:
commit
ac64b54537
@ -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 {}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 ||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user