diff --git a/package-lock.json b/package-lock.json
index 42a72a9224..bd6185273a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -32,7 +32,7 @@
"@ngx-translate/http-loader": "^16.0.1",
"@swimlane/ngx-graph": "^11.0.0",
"@tailwindcss/forms": "^0.5.4",
- "chart.js": "3.0.0-alpha",
+ "chart.js": "^4.5.1",
"d3": "^7.9.0",
"exceljs": "^4.4.0",
"html2canvas": "^1.4.1",
@@ -5756,9 +5756,9 @@
"license": "MIT"
},
"node_modules/@kurkle/color": {
- "version": "0.1.9",
- "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.1.9.tgz",
- "integrity": "sha512-K3Aul4Ct6O48yWw0/az5rqk2K76oNXXX3Su32Xkh4SfMFvPt0QEkq0Q6+3icE5S3U2c88WAuq3Vh1Iaz4aUH+w==",
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
"node_modules/@leichtgewicht/ip-codec": {
@@ -10345,12 +10345,15 @@
"license": "MIT"
},
"node_modules/chart.js": {
- "version": "3.0.0-alpha",
- "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.0.0-alpha.tgz",
- "integrity": "sha512-9jbL1IsG9+1oq01GMKbeJ/257O46nVjQuYWEanpo0MuCr18cXMfkrzBTwdbl0VTOkIvWLJJcU5Cmhdc546k2bA==",
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
+ "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
"license": "MIT",
"dependencies": {
- "@kurkle/color": "^0.1.6"
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
}
},
"node_modules/check-more-types": {
@@ -12612,21 +12615,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/errno": {
- "version": "0.1.8",
- "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
- "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "prr": "~1.0.1"
- },
- "bin": {
- "errno": "cli.js"
- }
- },
"node_modules/error-ex": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
@@ -14813,21 +14801,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/image-size": {
- "version": "0.5.5",
- "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
- "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "bin": {
- "image-size": "bin/image-size.js"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
@@ -16812,58 +16785,6 @@
}
}
},
- "node_modules/less/node_modules/make-dir": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
- "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "pify": "^4.0.1",
- "semver": "^5.6.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/less/node_modules/pify": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
- "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/less/node_modules/semver": {
- "version": "5.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
- "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
- "dev": true,
- "license": "ISC",
- "optional": true,
- "peer": true,
- "bin": {
- "semver": "bin/semver"
- }
- },
- "node_modules/less/node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "license": "BSD-3-Clause",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -18350,40 +18271,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/needle": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz",
- "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "iconv-lite": "^0.6.3",
- "sax": "^1.2.4"
- },
- "bin": {
- "needle": "bin/needle"
- },
- "engines": {
- "node": ">= 4.4.x"
- }
- },
- "node_modules/needle/node_modules/iconv-lite": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
- "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
@@ -20377,15 +20264,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/prr": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
- "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
@@ -21176,15 +21054,6 @@
}
}
},
- "node_modules/sax": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
- "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
- "dev": true,
- "license": "ISC",
- "optional": true,
- "peer": true
- },
"node_modules/saxes": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
diff --git a/package.json b/package.json
index e689872a1e..3d33f51cac 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,7 @@
"@ngx-translate/http-loader": "^16.0.1",
"@swimlane/ngx-graph": "^11.0.0",
"@tailwindcss/forms": "^0.5.4",
- "chart.js": "3.0.0-alpha",
+ "chart.js": "^4.5.1",
"d3": "^7.9.0",
"exceljs": "^4.4.0",
"html2canvas": "^1.4.1",
diff --git a/src/app/account-transfers/make-account-transfers/make-account-transfers.component.scss b/src/app/account-transfers/make-account-transfers/make-account-transfers.component.scss
index 4b619b0d9b..a3b5aff57d 100644
--- a/src/app/account-transfers/make-account-transfers/make-account-transfers.component.scss
+++ b/src/app/account-transfers/make-account-transfers/make-account-transfers.component.scss
@@ -4,11 +4,7 @@
padding: 1rem;
}
-#search-button {
- background-color: #1074b9;
- color: white;
-}
-
+// Card styling
.transfer-card {
border-radius: 8px;
box-shadow: 0 2px 10px rgb(0 0 0 / 8%);
@@ -26,7 +22,8 @@
font-weight: 600;
color: #333;
margin: 0 0 1rem;
- padding-bottom: 0;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid rgb(0 0 0 / 12%);
&.transfer-heading {
font-weight: 700;
@@ -49,9 +46,7 @@
align-items: center;
padding: 0.5rem;
background-color: rgb(0 0 0 / 2%);
- box-shadow: 0 0.5px 1px rgb(0 0 0 / 10%);
- border: 1px solid var(--border-color, #ddd);
- border-radius: 8px;
+ border-radius: 4px;
&:hover {
background-color: rgb(0 0 0 / 4%);
@@ -90,29 +85,55 @@ mat-divider {
gap: 1rem;
@media (width >= 768px) {
- grid-template-columns: repeat(2, 50%);
+ grid-template-columns: repeat(2, 1fr); /* stylelint-disable-line unit-allowed-list */
+ gap: 1.5rem;
}
}
+.form-row {
+ display: contents;
+}
+
.form-field {
width: 100%;
+ margin-bottom: 0.5rem;
+
+ &:nth-child(odd) {
+ @media (width >= 768px) {
+ margin-right: 0.5rem;
+ }
+ }
+
+ &:nth-child(even) {
+ @media (width >= 768px) {
+ margin-left: 0.5rem;
+ }
+ }
&.description-field {
grid-column: 1 / -1;
+
+ textarea {
+ min-height: 80px;
+ resize: vertical;
+ }
}
}
+::ng-deep .mat-form-field {
+ width: 100%;
+}
+
// Button styling
.action-buttons {
display: flex;
- justify-content: center;
+ justify-content: flex-end;
gap: 1rem;
padding: 1rem 1.5rem;
- margin-top: 0;
+ margin-top: 1rem;
@media (width <= 576px) {
flex-direction: column;
- align-items: center;
}
button {
@@ -120,7 +141,6 @@ mat-divider {
@media (width <= 576px) {
width: 100%;
- max-width: 300px;
margin-bottom: 0.5rem;
}
}
@@ -186,16 +206,12 @@ mat-divider {
// Dark theme
// Lower specificity selectors first
.dark-theme {
- .info-row {
- color: #fff !important;
- }
-
.info-label {
- color: rgb(255 255 255 / 70%) !important;
+ color: rgb(255 255 255 / 70%);
}
.info-value {
- color: rgb(255 255 255 / 87%) !important;
+ color: rgb(255 255 255 / 87%);
}
.section-title {
@@ -233,18 +249,6 @@ mat-divider {
}
// Higher specificity selectors
-:host-context(.dark-theme) .info-row {
- color: #fff !important;
-}
-
-:host-context(.dark-theme) .info-label {
- color: rgb(255 255 255 / 70%) !important;
-}
-
-:host-context(.dark-theme) .info-value {
- color: rgb(255 255 255 / 87%) !important;
-}
-
:host-context(.dark-theme) .transfer-heading {
color: #fff;
font-weight: 700;
@@ -266,18 +270,6 @@ mat-divider {
// Medium specificity selectors
body.dark-theme {
.container {
- .info-row {
- color: #fff !important;
- }
-
- .info-label {
- color: rgb(255 255 255 / 70%) !important;
- }
-
- .info-value {
- color: rgb(255 255 255 / 87%) !important;
- }
-
.section-title {
color: #fff;
}
@@ -320,3 +312,20 @@ body.dark-theme {
::ng-deep mat-form-field.error-warn.mat-form-field-invalid .mat-error {
color: #0009 !important;
}
+
+::ng-deep .mat-form-field-flex {
+ align-items: center;
+}
+
+.transfer-form .form-field {
+ margin-bottom: 1rem;
+}
+
+.readonly-field {
+ background-color: rgb(0 0 0 / 2%);
+ cursor: not-allowed;
+}
+
+::ng-deep .mat-input-element {
+ font-size: 14px;
+}
diff --git a/src/app/account-transfers/make-account-transfers/make-account-transfers.component.ts b/src/app/account-transfers/make-account-transfers/make-account-transfers.component.ts
index 264cd4490f..bfde75b96a 100644
--- a/src/app/account-transfers/make-account-transfers/make-account-transfers.component.ts
+++ b/src/app/account-transfers/make-account-transfers/make-account-transfers.component.ts
@@ -3,12 +3,10 @@ import { Component, OnInit, AfterViewInit, ViewChild, ElementRef, inject } from
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import {
AbstractControl,
- FormGroup,
UntypedFormBuilder,
UntypedFormGroup,
ValidationErrors,
Validators,
- ReactiveFormsModule,
FormsModule
} from '@angular/forms';
@@ -21,11 +19,10 @@ import { Dates } from 'app/core/utils/dates';
/** Environment Configuration */
import { environment } from '../../../environments/environment';
import { MatDivider } from '@angular/material/divider';
-import { MatFormField, MatLabel, MatHint, MatSuffix, MatError } from '@angular/material/form-field';
-import { MatOption, MatAutocompleteTrigger, MatAutocomplete } from '@angular/material/autocomplete';
+import { MatFormField, MatLabel, MatHint, MatError } from '@angular/material/form-field';
+import { MatAutocompleteTrigger, MatAutocomplete } from '@angular/material/autocomplete';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
-import { MakeAccountInterbankTransfersComponent } from '../make-account-interbank-transfers/make-account-interbank-transfers.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
@@ -43,8 +40,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatAutocompleteTrigger,
MatAutocomplete,
FaIconComponent,
- CdkTextareaAutosize,
- MakeAccountInterbankTransfersComponent
+ CdkTextareaAutosize
]
})
export class MakeAccountTransfersComponent implements OnInit, AfterViewInit {
@@ -64,7 +60,6 @@ export class MakeAccountTransfersComponent implements OnInit, AfterViewInit {
maxDate = new Date(2100, 0, 1);
/** Edit Standing Instructions form. */
makeAccountTransferForm: UntypedFormGroup;
- //makeAccountInterbankTransferForm: FormGroup;
/** To Office Type Data */
toOfficeTypeData: any;
/** To Client Type Data */
@@ -85,6 +80,7 @@ export class MakeAccountTransfersComponent implements OnInit, AfterViewInit {
interbank: boolean = false;
/** Reference of phoneAccount search */
phoneAccount = '';
+ /** Interbank transfer form flag */
interbankTransferForm: boolean = false;
balance: number = 0;
isLoading: boolean = false;
@@ -109,6 +105,8 @@ export class MakeAccountTransfersComponent implements OnInit, AfterViewInit {
this.setOptions();
});
}
+
+ /** Sets the value from the URL */
/** Sets the value from the URL */
setParams() {
this.accountType = this.route.snapshot.queryParams['accountType'];
@@ -122,7 +120,13 @@ export class MakeAccountTransfersComponent implements OnInit, AfterViewInit {
this.accountTypeId = '2';
this.id = this.route.snapshot.queryParams['savingsId'];
this.interbank = this.route.snapshot.queryParams['interbank'] === 'true';
- this.balance = this.router.currentNavigation().extras.state.balance;
+ const navigationBalance = this.router.getCurrentNavigation()?.extras?.state?.balance;
+ const templateBalance =
+ this.accountTransferTemplateData?.fromAccount?.availableBalance ??
+ this.accountTransferTemplateData?.fromAccount?.summary?.accountBalance ??
+ this.accountTransferTemplateData?.fromAccount?.balance ??
+ 0;
+ this.balance = typeof navigationBalance === 'number' ? navigationBalance : templateBalance;
break;
default:
this.accountTypeId = '0';
@@ -136,9 +140,50 @@ export class MakeAccountTransfersComponent implements OnInit, AfterViewInit {
this.maxDate = this.settingsService.businessDate;
if (!this.interbank) {
this.createMakeAccountTransferForm();
+ } else {
+ this.createEmptyInterbankForm();
}
}
+ /**
+ * Crea un formulario vacío para interbank mientras se carga
+ */
+ createEmptyInterbankForm() {
+ this.makeAccountTransferForm = this.formBuilder.group({
+ toBank: [
+ '',
+ Validators.required
+ ],
+ toClientId: [
+ '',
+ Validators.required
+ ],
+ toAccountType: [
+ '',
+ Validators.required
+ ],
+ toAccountId: [
+ '',
+ Validators.required
+ ],
+ transferAmount: [
+ 0,
+ [
+ Validators.required,
+ Validators.min(0.01),
+ this.amountExceedsBalanceValidator.bind(this)]
+ ],
+ transferDate: [
+ this.settingsService.businessDate,
+ Validators.required
+ ],
+ transferDescription: [
+ '',
+ Validators.required
+ ]
+ });
+ }
+
/**
* Creates the standing instruction form.
*/
@@ -179,25 +224,34 @@ export class MakeAccountTransfersComponent implements OnInit, AfterViewInit {
}
createMakeAccountInterbankTransferForm(account: any) {
- /* --> */ this.makeAccountTransferForm = this.formBuilder.group({
+ if (!account) {
+ console.error('Account data is undefined');
+ this.isLoading = false;
+ return;
+ }
+
+ const defaultAmount =
+ this.accountTransferTemplateData?.transferAmount > 0 ? this.accountTransferTemplateData.transferAmount : 1;
+
+ this.makeAccountTransferForm = this.formBuilder.group({
toBank: [
- { value: account.sourceFspId, disabled: true },
+ account.destinationFspId || '',
Validators.required
],
toClientId: [
- { value: account.firsName + ' ' + account.lastName, disabled: true },
+ (account.firstName || account.firsName || '') + ' ' + (account.lastName || ''),
Validators.required
],
toAccountType: [
- { value: 'Saving Account', disabled: true },
+ 'Saving Account',
Validators.required
],
toAccountId: [
- { value: account.partyId, disabled: true },
+ account.partyId || '',
Validators.required
],
transferAmount: [
- this.accountTransferTemplateData.transferAmount,
+ defaultAmount,
[
Validators.required,
Validators.min(0.01),
@@ -208,7 +262,7 @@ export class MakeAccountTransfersComponent implements OnInit, AfterViewInit {
Validators.required
],
transferDescription: [
- '',
+ 'Transferencia interbancaria',
Validators.required
]
});
@@ -246,7 +300,7 @@ export class MakeAccountTransfersComponent implements OnInit, AfterViewInit {
delete dataObj.transferAmount;
delete dataObj.transferDate;
delete dataObj.transferDescription;
- if (dataObj.toClientId) {
+ if (dataObj.toClientId && typeof dataObj.toClientId === 'object') {
dataObj.toClientId = dataObj.toClientId.id;
}
const propNames = Object.getOwnPropertyNames(dataObj);
@@ -263,9 +317,9 @@ export class MakeAccountTransfersComponent implements OnInit, AfterViewInit {
* Subscribes to Clients search filter:
*/
ngAfterViewInit() {
- if (!this.interbank) {
- this.makeAccountTransferForm.controls.toClientId.valueChanges.subscribe((value: string) => {
- if (value.length >= 2) {
+ if (!this.interbank && this.makeAccountTransferForm) {
+ this.makeAccountTransferForm.controls.toClientId.valueChanges.subscribe((value: any) => {
+ if (typeof value === 'string' && value.length >= 2) {
this.clientsService.getFilteredClients('displayName', 'ASC', true, value).subscribe((data: any) => {
this.clientsData = data.pageItems;
});
@@ -295,17 +349,26 @@ export class MakeAccountTransfersComponent implements OnInit, AfterViewInit {
this.isLoading = true;
const dateFormat = this.settingsService.dateFormat;
const locale = this.settingsService.language.code;
+
+ let toClientIdValue: any;
+ if (typeof this.makeAccountTransferForm.controls.toClientId.value === 'object') {
+ toClientIdValue = this.makeAccountTransferForm.controls.toClientId.value.id;
+ } else {
+ toClientIdValue = this.makeAccountTransferForm.controls.toClientId.value;
+ }
+
const makeAccountTransferData = {
...this.makeAccountTransferForm.value,
transferDate: this.dateUtils.formatDate(this.makeAccountTransferForm.value.transferDate, dateFormat),
dateFormat,
locale,
- toClientId: this.makeAccountTransferForm.controls.toClientId.value.id,
+ toClientId: toClientIdValue,
fromAccountId: this.id,
fromAccountType: this.accountTypeId,
fromClientId: this.accountTransferTemplateData.fromClient.id,
fromOfficeId: this.accountTransferTemplateData.fromClient.officeId
};
+
this.accountTransfersService.createAccountTransfer(makeAccountTransferData).subscribe(() => {
this.isLoading = false;
this.router.navigate(['../../transactions'], { relativeTo: this.route });
@@ -314,12 +377,19 @@ export class MakeAccountTransfersComponent implements OnInit, AfterViewInit {
makeInterbankTransfer() {
this.isLoading = true;
+
+ if (!this.makeAccountTransferForm.valid) {
+ console.error('Interbank form is not valid');
+ this.isLoading = false;
+ return;
+ }
+
const payload = {
homeTransactionId: crypto.randomUUID(),
from: {
fspId: environment.fineractPlatformTenantId,
idType: 'MSISDN',
- idValue: this.accountTransferTemplateData.fromAccount.externalId.trim()
+ idValue: this.accountTransferTemplateData.fromAccount.externalId?.trim() || ''
},
to: {
fspId: this.makeAccountTransferForm.controls.toBank.value,
@@ -339,6 +409,7 @@ export class MakeAccountTransfersComponent implements OnInit, AfterViewInit {
},
note: this.makeAccountTransferForm.controls.transferDescription.value
};
+
this.accountTransfersService.sendInterbankTransfer(JSON.stringify(payload)).subscribe(
(trnsfr) => {
if (trnsfr.systemMessage) {
@@ -347,12 +418,17 @@ export class MakeAccountTransfersComponent implements OnInit, AfterViewInit {
}
},
(error) => {
+ console.error('Interbank transfer error:', error);
this.isLoading = false;
}
);
}
searchAccountByNumber() {
+ if (!this.phoneAccount || this.phoneAccount.length !== 10) {
+ return;
+ }
+
this.isLoading = true;
this.accountTransfersService
.getAccountByNumber(this.phoneAccount, this.accountTransferTemplateData.currency.code)
@@ -362,6 +438,7 @@ export class MakeAccountTransfersComponent implements OnInit, AfterViewInit {
this.createMakeAccountInterbankTransferForm(acc);
},
(error) => {
+ console.error('searching account error:', error);
this.isLoading = false;
}
);
diff --git a/src/app/accounting/accounting-rules/edit-rule/edit-rule.component.html b/src/app/accounting/accounting-rules/edit-rule/edit-rule.component.html
index 70ea8009cc..1d2d6a763a 100644
--- a/src/app/accounting/accounting-rules/edit-rule/edit-rule.component.html
+++ b/src/app/accounting/accounting-rules/edit-rule/edit-rule.component.html
@@ -135,7 +135,7 @@
-
diff --git a/src/app/loans/loans-view/loan-account-actions/loan-reaging/re-age-preview-dialog/re-age-preview-dialog.component.html b/src/app/loans/loans-view/loan-account-actions/loan-reaging/re-age-preview-dialog/re-age-preview-dialog.component.html
new file mode 100644
index 0000000000..e33efb2365
--- /dev/null
+++ b/src/app/loans/loans-view/loan-account-actions/loan-reaging/re-age-preview-dialog/re-age-preview-dialog.component.html
@@ -0,0 +1,16 @@
+
{{ 'labels.heading.Repayment Schedule Preview' | translate }}
+
+
+
+
+
+
+
+
+ {{ 'labels.buttons.Go back' | translate }}
+
+
diff --git a/src/app/loans/loans-view/loan-account-actions/loan-reaging/re-age-preview-dialog/re-age-preview-dialog.component.scss b/src/app/loans/loans-view/loan-account-actions/loan-reaging/re-age-preview-dialog/re-age-preview-dialog.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/app/loans/loans-view/loan-account-actions/loan-reaging/re-age-preview-dialog/re-age-preview-dialog.component.ts b/src/app/loans/loans-view/loan-account-actions/loan-reaging/re-age-preview-dialog/re-age-preview-dialog.component.ts
new file mode 100644
index 0000000000..aa5a63de35
--- /dev/null
+++ b/src/app/loans/loans-view/loan-account-actions/loan-reaging/re-age-preview-dialog/re-age-preview-dialog.component.ts
@@ -0,0 +1,47 @@
+import { Component, Inject } from '@angular/core';
+import {
+ MAT_DIALOG_DATA,
+ MatDialogRef,
+ MatDialogTitle,
+ MatDialogContent,
+ MatDialogActions
+} from '@angular/material/dialog';
+import { MatButton } from '@angular/material/button';
+import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
+import { RepaymentSchedule } from 'app/loans/models/loan-account.model';
+import { RepaymentScheduleTabComponent } from '../../../repayment-schedule-tab/repayment-schedule-tab.component';
+
+export interface ReAgePreviewDialogData {
+ repaymentSchedule: RepaymentSchedule;
+ currencyCode: string;
+}
+
+@Component({
+ selector: 'mifosx-re-age-preview-dialog',
+ templateUrl: './re-age-preview-dialog.component.html',
+ styleUrls: ['./re-age-preview-dialog.component.scss'],
+ imports: [
+ ...STANDALONE_SHARED_IMPORTS,
+ MatDialogTitle,
+ MatDialogContent,
+ MatDialogActions,
+ MatButton,
+ RepaymentScheduleTabComponent
+ ]
+})
+export class ReAgePreviewDialogComponent {
+ repaymentSchedule: RepaymentSchedule;
+ currencyCode: string;
+
+ constructor(
+ public dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) public data: ReAgePreviewDialogData
+ ) {
+ this.repaymentSchedule = data.repaymentSchedule;
+ this.currencyCode = data.currencyCode;
+ }
+
+ close(): void {
+ this.dialogRef.close();
+ }
+}
diff --git a/src/app/loans/loans-view/repayment-schedule-tab/repayment-schedule-tab.component.html b/src/app/loans/loans-view/repayment-schedule-tab/repayment-schedule-tab.component.html
index 754a324b40..17c57e0538 100644
--- a/src/app/loans/loans-view/repayment-schedule-tab/repayment-schedule-tab.component.html
+++ b/src/app/loans/loans-view/repayment-schedule-tab/repayment-schedule-tab.component.html
@@ -8,12 +8,13 @@
}
@if (!forEditing) {
-
+
| {{ item.period }} |
|
+
@@ -21,6 +22,7 @@
|
Total |
+
@@ -28,6 +30,7 @@
|
|
+
|
+
|
@@ -46,6 +50,7 @@
|
|
+
|
+
{{
- repaymentScheduleDetails.totalPrincipalExpected | currency: currencyCode : 'symbol-narrow' : '1.2-2'
+ repaymentScheduleDetails?.totalPrincipalExpected | currency: currencyCode : 'symbol-narrow' : '1.2-2'
}}
|
+
- {{ repaymentScheduleDetails.totalInterestCharged | currency: currencyCode : 'symbol-narrow' : '1.2-2' }}
+ {{ repaymentScheduleDetails?.totalInterestCharged | currency: currencyCode : 'symbol-narrow' : '1.2-2' }}
|
+
- {{ repaymentScheduleDetails.totalFeeChargesCharged | currency: currencyCode : 'symbol-narrow' : '1.2-2' }}
+ {{ repaymentScheduleDetails?.totalFeeChargesCharged | currency: currencyCode : 'symbol-narrow' : '1.2-2' }}
|
+
{{
- repaymentScheduleDetails.totalPenaltyChargesCharged | currency: currencyCode : 'symbol-narrow' : '1.2-2'
+ repaymentScheduleDetails?.totalPenaltyChargesCharged | currency: currencyCode : 'symbol-narrow' : '1.2-2'
}}
|
+
- {{ repaymentScheduleDetails.totalRepaymentExpected | currency: currencyCode : 'symbol-narrow' : '1.2-2' }}
+ {{ repaymentScheduleDetails?.totalRepaymentExpected | currency: currencyCode : 'symbol-narrow' : '1.2-2' }}
|
+
- {{ repaymentScheduleDetails.totalRepayment | currency: currencyCode : 'symbol-narrow' : '1.2-2' }}
+ {{ repaymentScheduleDetails?.totalRepayment | currency: currencyCode : 'symbol-narrow' : '1.2-2' }}
|
+
- {{ repaymentScheduleDetails.totalPaidInAdvance | currency: currencyCode : 'symbol-narrow' : '1.2-2' }}
+ {{ repaymentScheduleDetails?.totalPaidInAdvance | currency: currencyCode : 'symbol-narrow' : '1.2-2' }}
|
+
- {{ repaymentScheduleDetails.totalPaidLate | currency: currencyCode : 'symbol-narrow' : '1.2-2' }}
+ {{ repaymentScheduleDetails?.totalPaidLate | currency: currencyCode : 'symbol-narrow' : '1.2-2' }}
|
+
@if (isWaived) {
- {{ repaymentScheduleDetails.totalWaived | currency: currencyCode : 'symbol-narrow' : '1.2-2' }}
+ {{ repaymentScheduleDetails?.totalWaived | currency: currencyCode : 'symbol-narrow' : '1.2-2' }}
|
}
@@ -179,33 +193,39 @@
|
}
+
{{ item.totalOutstandingForPeriod | formatNumber }} |
- {{ repaymentScheduleDetails.totalOutstanding | currency: currencyCode : 'symbol-narrow' : '1.2-2' }}
+ {{ repaymentScheduleDetails?.totalOutstanding | currency: currencyCode : 'symbol-narrow' : '1.2-2' }}
|
+
|
+
+
+
+
- @if (forEditing && repaymentScheduleDetails.periods.length > 0) {
-
+ @if (forEditing && repaymentScheduleDetails?.periods?.length > 0) {
+
| {{ item.period }} |
+
@@ -231,6 +252,7 @@
|
+
diff --git a/src/app/loans/loans.service.ts b/src/app/loans/loans.service.ts
index d5c82107af..d4162c3f4c 100644
--- a/src/app/loans/loans.service.ts
+++ b/src/app/loans/loans.service.ts
@@ -244,6 +244,24 @@ export class LoansService {
return this.http.post(`/loans/${loanId}/transactions`, data, { params: httpParams });
}
+ /**
+ * Get Re-Age preview with repayment schedule
+ * @param loanId Loan Id
+ * @param data Re-Age data
+ * @returns Observable with repayment schedule preview
+ */
+ getReAgePreview(loanId: string, data: any): Observable {
+ let httpParams = new HttpParams();
+
+ Object.keys(data).forEach((key) => {
+ if (data[key] !== null && data[key] !== undefined && data[key] !== '') {
+ httpParams = httpParams.set(key, data[key].toString());
+ }
+ });
+
+ return this.http.get(`/loans/${loanId}/transactions/reage-preview`, { params: httpParams });
+ }
+
getLoanScreenReportsData(): Observable {
const httpParams = new HttpParams().set('entityId', '1').set('typeId', '0');
return this.http.get(`/templates`, { params: httpParams });
diff --git a/src/app/loans/models/loan-account.model.ts b/src/app/loans/models/loan-account.model.ts
index 76aaa84754..0b1c4dd276 100644
--- a/src/app/loans/models/loan-account.model.ts
+++ b/src/app/loans/models/loan-account.model.ts
@@ -146,3 +146,21 @@ export interface BuyDownFeeAmortizationDetails {
adjustedAmount: number;
chargedOffAmount: number;
}
+
+export interface EditablePeriod extends RepaymentSchedulePeriod {
+ changed?: boolean;
+}
+
+export interface EditableRepaymentSchedule extends RepaymentSchedule {
+ periods: EditablePeriod[];
+}
+
+export interface RepaymentScheduleEditCache {
+ edit: boolean;
+ data: RepaymentSchedulePeriod;
+}
+
+export interface ScheduleChangeRecord {
+ dueDate: string;
+ installmentAmount: number;
+}
diff --git a/src/app/navigation/navigation.component.scss b/src/app/navigation/navigation.component.scss
index e69de29bb2..1252090d09 100644
--- a/src/app/navigation/navigation.component.scss
+++ b/src/app/navigation/navigation.component.scss
@@ -0,0 +1,53 @@
+// Component-specific styles - minimal overrides
+// Leverages global layout classes from main.scss
+
+:host {
+ display: block;
+}
+
+.container {
+ width: 100%;
+}
+
+// Using existing .layout-row-wrap.responsive-column from global styles
+// Only adding specific alignment needed for this component
+.layout-row-wrap.responsive-column {
+ align-items: flex-start;
+}
+
+// Extending global .flex-48 with min-width constraint for this component
+.flex-48 {
+ min-width: 20rem;
+
+ @media (width >= 1200px) {
+ flex-basis: 48%;
+ }
+}
+
+mat-card {
+ padding: 1rem;
+ border-radius: 0.5rem;
+ overflow: hidden;
+}
+
+mat-card-content {
+ display: grid;
+ grid-template-columns: 100%;
+
+ @media (width >= 768px) {
+ grid-template-columns: 50% 50%;
+ }
+
+ @media (width >= 1200px) {
+ grid-template-columns: 50% 50%;
+ gap: 1rem;
+ }
+}
+
+mat-form-field {
+ width: 100%;
+}
+
+mat-label {
+ letter-spacing: 0.0125rem;
+}
diff --git a/src/app/navigation/office-navigation/office-navigation.component.scss b/src/app/navigation/office-navigation/office-navigation.component.scss
index 2fccc29335..7de4c3a1b0 100644
--- a/src/app/navigation/office-navigation/office-navigation.component.scss
+++ b/src/app/navigation/office-navigation/office-navigation.component.scss
@@ -1,14 +1,55 @@
-h2 {
- font-weight: 500;
+// Component-specific styles - minimal overrides to global styles
+// Follows existing patterns from general-tab components
+
+mat-card-header {
+ padding: 1.5rem 1.5rem 1rem;
+
+ h2 {
+ font-weight: 500;
+ font-size: 1.5rem;
+ margin: 0;
+ line-height: 1.4;
+ }
+
+ @media (width <= 480px) {
+ padding: 1rem;
+
+ h2 {
+ font-size: 1.25rem;
+ }
+ }
}
-.content {
- div {
- margin: 1rem 0;
+mat-card-content {
+ padding: 1.5rem;
+
+ .layout-row-wrap {
+ display: grid;
+ grid-template-columns: 50% 50%;
+
+ @media (width <= 768px) {
+ grid-template-columns: 100%;
+ }
+ }
+
+ .flex-50 {
+ padding: 0.625rem 0;
+ display: flex;
+ align-items: center;
+ font-size: 0.875rem;
word-wrap: break-word;
+ line-height: 1.6;
+
+ &.mat-body-strong {
+ font-weight: 600;
+ }
+
+ &:not(:last-child, :nth-last-child(2)) {
+ border-bottom: 1px solid #ddd;
+ }
}
-}
-.main-icon {
- margin: 7px 0 0;
+ @media (width <= 480px) {
+ padding: 1rem;
+ }
}
diff --git a/src/app/navigation/office-navigation/office-navigation.component.ts b/src/app/navigation/office-navigation/office-navigation.component.ts
index f580508c13..2cb4226fef 100644
--- a/src/app/navigation/office-navigation/office-navigation.component.ts
+++ b/src/app/navigation/office-navigation/office-navigation.component.ts
@@ -23,6 +23,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatCardTitleGroup,
MatCardTitle,
MatCardSubtitle,
+ MatCardContent,
ExternalIdentifierComponent,
DateFormatPipe
]
diff --git a/src/app/organization/bulk-import/view-bulk-import/view-bulk-import.component.scss b/src/app/organization/bulk-import/view-bulk-import/view-bulk-import.component.scss
index 9ea17984bf..4705237f32 100644
--- a/src/app/organization/bulk-import/view-bulk-import/view-bulk-import.component.scss
+++ b/src/app/organization/bulk-import/view-bulk-import/view-bulk-import.component.scss
@@ -20,6 +20,7 @@
display: flex;
flex-direction: column;
border-radius: 20px;
+ min-height: 400px;
h3 {
margin: 0 0 20px;
@@ -36,7 +37,6 @@
mat-card-content {
padding: 0;
margin-bottom: 20px;
- flex-grow: 1;
mat-form-field {
width: 100%;
@@ -55,6 +55,11 @@
}
}
+ .flex-spacer {
+ flex: 1;
+ min-height: 20px;
+ }
+
mifosx-file-upload {
margin: 16px 0;
flex-grow: 1;
diff --git a/src/app/organization/bulk-loan-reassignmnet/bulk-loan-reassignmnet.component.ts b/src/app/organization/bulk-loan-reassignmnet/bulk-loan-reassignmnet.component.ts
index 71e475b33b..da75a2aa7a 100644
--- a/src/app/organization/bulk-loan-reassignmnet/bulk-loan-reassignmnet.component.ts
+++ b/src/app/organization/bulk-loan-reassignmnet/bulk-loan-reassignmnet.component.ts
@@ -82,8 +82,12 @@ export class BulkLoanReassignmnetComponent implements OnInit {
*/
setBulkLoanForm() {
this.bulkLoanForm = this.formBuilder.group({
+ officeId: [
+ '',
+ Validators.required
+ ],
assignmentDate: [
- new Date(),
+ this.settingsService.businessDate,
Validators.required
],
toLoanOfficerId: [
@@ -110,10 +114,14 @@ export class BulkLoanReassignmnetComponent implements OnInit {
* @param officerId Office Id.
*/
getFromOfficers(officerId: any) {
- this.toLoanOfficers = this.fromLoanOfficers.filter((officer: any) => officer.id !== officerId);
- this.organizationSevice.getOfficerTemplate(officerId, this.officeTemplate.id).subscribe((response: any) => {
- this.officerTemplate = response;
- });
+ this.toLoanOfficers = this.fromLoanOfficers?.filter((officer: any) => officer.id !== officerId) || [];
+ if (officerId && this.officeTemplate && this.officeTemplate.id) {
+ this.organizationSevice.getOfficerTemplate(officerId, this.officeTemplate.id).subscribe((response: any) => {
+ this.officerTemplate = response;
+ });
+ } else {
+ this.officerTemplate = undefined;
+ }
}
/**
diff --git a/src/app/organization/entity-data-table-checks/entity-data-table-checks.component.ts b/src/app/organization/entity-data-table-checks/entity-data-table-checks.component.ts
index d001e2abee..a649d05896 100644
--- a/src/app/organization/entity-data-table-checks/entity-data-table-checks.component.ts
+++ b/src/app/organization/entity-data-table-checks/entity-data-table-checks.component.ts
@@ -127,17 +127,18 @@ export class EntityDataTableChecksComponent implements OnInit {
this.setEntity();
}
- /**
- * Sets Entity to its corresponding values
- */
setEntity() {
- for (let i = 0; i < this.dataSource.data.length; i++) {
- for (let j = 0; j < this.entityValues.length; j++) {
- if (this.entityValues[j].code === this.dataSource.data[i].entity) {
- this.dataSource.data[i].entity = this.entityValues[j].value;
- }
+ const entityMap = new Map();
+ this.entityValues.forEach((entity: any) => {
+ entityMap.set(entity.code, entity.value);
+ });
+
+ this.dataSource.data.forEach((item: any) => {
+ const entityValue = entityMap.get(item.entity);
+ if (entityValue) {
+ item.entity = entityValue;
}
- }
+ });
}
/**
diff --git a/src/app/organization/fund-mapping/fund-mapping.component.ts b/src/app/organization/fund-mapping/fund-mapping.component.ts
index a4ef7a1cf6..25064faf92 100644
--- a/src/app/organization/fund-mapping/fund-mapping.component.ts
+++ b/src/app/organization/fund-mapping/fund-mapping.component.ts
@@ -20,7 +20,9 @@ import {
UntypedFormBuilder,
UntypedFormControl,
Validators,
- ReactiveFormsModule
+ ReactiveFormsModule,
+ AbstractControl,
+ ValidationErrors
} from '@angular/forms';
import { ActivatedRoute, RouterLink } from '@angular/router';
@@ -115,14 +117,36 @@ export class FundMappingComponent implements OnInit {
this.buildDependencies();
}
+ /**
+ * Custom validator to ensure array fields are not empty.
+ * @param {AbstractControl} control - the form control to validate.
+ * @returns {ValidationErrors | null} - validation errors or null if valid.
+ */
+ private nonEmptyArrayValidator(control: AbstractControl): ValidationErrors | null {
+ const value = control.value;
+ if (!value || !Array.isArray(value) || value.length === 0) {
+ return { required: true };
+ }
+ if (value.every((item: any) => item === '' || item === null || item === undefined)) {
+ return { required: true };
+ }
+ return null;
+ }
+
/**
* Creates the Fund Mapping Form
*/
createFundMappingForm() {
this.fundMappingForm = this.formBuilder.group({
- loanStatus: [''],
- loanProducts: [''],
- offices: [''],
+ loanStatus: [
+ [],
+ this.nonEmptyArrayValidator.bind(this)],
+ loanProducts: [
+ [],
+ this.nonEmptyArrayValidator.bind(this)],
+ offices: [
+ [],
+ this.nonEmptyArrayValidator.bind(this)],
loanDateOption: [
'',
Validators.required
@@ -227,6 +251,10 @@ export class FundMappingComponent implements OnInit {
if (fundMappingFormData.loanFromDate instanceof Date) {
fundMappingFormData.loanFromDate = this.dateUtils.formatDate(prevLoanFromDate, dateFormat);
}
+ if (this.fundMappingForm.invalid) {
+ this.fundMappingForm.markAllAsTouched();
+ return;
+ }
if (fundMappingFormData.loanToDate instanceof Date) {
fundMappingFormData.loanToDate = this.dateUtils.formatDate(prevLoanToDate, dateFormat);
}
diff --git a/src/app/organization/holidays/create-holiday/create-holiday.component.ts b/src/app/organization/holidays/create-holiday/create-holiday.component.ts
index 0fa68ac702..5cadef5996 100644
--- a/src/app/organization/holidays/create-holiday/create-holiday.component.ts
+++ b/src/app/organization/holidays/create-holiday/create-holiday.component.ts
@@ -337,11 +337,14 @@ export class CreateHolidayComponent implements OnInit {
const locale = this.settings.language.code;
const prevFromDate: Date = this.holidayForm.value.fromDate;
const prevToDate: Date = this.holidayForm.value.toDate;
- holidayFormData.fromDate = this.dateUtils.formatDate(prevFromDate, dateFormat);
- holidayFormData.toDate = this.dateUtils.formatDate(prevToDate, dateFormat);
+ holidayFormData.fromDate = this.dateUtils.formatDateAsString(prevFromDate, dateFormat);
+ holidayFormData.toDate = this.dateUtils.formatDateAsString(prevToDate, dateFormat);
if (this.holidayForm.contains('repaymentsRescheduledTo')) {
const prevRepaymentsRescheduledTo: Date = this.holidayForm.value.repaymentsRescheduledTo;
- holidayFormData.repaymentsRescheduledTo = this.dateUtils.formatDate(prevRepaymentsRescheduledTo, dateFormat);
+ holidayFormData.repaymentsRescheduledTo = this.dateUtils.formatDateAsString(
+ prevRepaymentsRescheduledTo,
+ dateFormat
+ );
}
const offices = this.holidayForm.value.offices.map((office: string) => {
return { officeId: Number.parseInt(office, 10) };
diff --git a/src/app/organization/holidays/edit-holiday/edit-holiday.component.ts b/src/app/organization/holidays/edit-holiday/edit-holiday.component.ts
index cdb8888618..64613d3950 100644
--- a/src/app/organization/holidays/edit-holiday/edit-holiday.component.ts
+++ b/src/app/organization/holidays/edit-holiday/edit-holiday.component.ts
@@ -137,17 +137,20 @@ export class EditHolidayComponent implements OnInit {
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
if (!this.isActiveHoliday) {
- if (this.reSchedulingType === 2) {
- const repaymentScheduledTo: Date = this.holidayForm.value.repaymentsRescheduledTo;
- holidayFormData.repaymentsRescheduledTo = this.dateUtils.formatDate(repaymentScheduledTo, dateFormat);
+ const prevFromDate = this.holidayForm.value.fromDate;
+ const prevToDate = this.holidayForm.value.toDate;
+
+ if (prevFromDate instanceof Date) {
+ holidayFormData.fromDate = this.dateUtils.formatDateAsString(prevFromDate, dateFormat);
}
- const prevFromDate: Date = this.holidayForm.value.fromDate;
- const prevToDate: Date = this.holidayForm.value.toDate;
- if (holidayFormData.closureDate instanceof Date) {
- holidayFormData.fromDate = this.dateUtils.formatDate(prevFromDate, dateFormat);
+ if (prevToDate instanceof Date) {
+ holidayFormData.toDate = this.dateUtils.formatDateAsString(prevToDate, dateFormat);
}
- if (holidayFormData.closureDate instanceof Date) {
- holidayFormData.toDate = this.dateUtils.formatDate(prevToDate, dateFormat);
+ if (this.reSchedulingType === 2) {
+ const repaymentScheduledTo = this.holidayForm.value.repaymentsRescheduledTo;
+ if (repaymentScheduledTo instanceof Date) {
+ holidayFormData.repaymentsRescheduledTo = this.dateUtils.formatDateAsString(repaymentScheduledTo, dateFormat);
+ }
}
}
const data = {
diff --git a/src/app/organization/loan-provisioning-criteria/create-loan-provisioning-criteria/create-loan-provisioning-criteria.component.ts b/src/app/organization/loan-provisioning-criteria/create-loan-provisioning-criteria/create-loan-provisioning-criteria.component.ts
index 2a8cc9df25..132fef3ace 100644
--- a/src/app/organization/loan-provisioning-criteria/create-loan-provisioning-criteria/create-loan-provisioning-criteria.component.ts
+++ b/src/app/organization/loan-provisioning-criteria/create-loan-provisioning-criteria/create-loan-provisioning-criteria.component.ts
@@ -134,7 +134,10 @@ export class CreateLoanProvisioningCriteriaComponent implements OnInit {
'',
Validators.required
],
- loanProducts: ['']
+ loanProducts: [
+ [],
+ Validators.required
+ ]
});
}
@@ -237,9 +240,10 @@ export class CreateLoanProvisioningCriteriaComponent implements OnInit {
*/
submit() {
const locale = this.settingsService.language.code;
+ const products = this.provisioningCriteriaForm.get('loanProducts').value;
const loanProvisioningCriteria = {
...this.provisioningCriteriaForm.value,
- loanProducts: this.provisioningCriteriaForm.get('loanProducts').value.map((product: any) => ({
+ loanProducts: products.map((product: any) => ({
id: product.id,
name: product.name,
includeInBorrowerCycle: product.includeInBorrowerCycle
diff --git a/src/app/organization/loan-provisioning-criteria/view-loan-provisioning-criteria/view-loan-provisioning-criteria.component.html b/src/app/organization/loan-provisioning-criteria/view-loan-provisioning-criteria/view-loan-provisioning-criteria.component.html
index e125620e78..f2a38a0c19 100644
--- a/src/app/organization/loan-provisioning-criteria/view-loan-provisioning-criteria/view-loan-provisioning-criteria.component.html
+++ b/src/app/organization/loan-provisioning-criteria/view-loan-provisioning-criteria/view-loan-provisioning-criteria.component.html
@@ -12,15 +12,16 @@
-
-
{{ provisioningData.criteriaName }}
-
-
-
- {{ 'labels.inputs.Loan Product' | translate }}:
- {{ loanProducts }}
-
+
+
+ {{ provisioningData.criteriaName }}
+
+
+ {{ 'labels.inputs.Loan Product' | translate }}:
+ {{ loanProducts }}
+
+
diff --git a/src/app/organization/loan-provisioning-criteria/view-loan-provisioning-criteria/view-loan-provisioning-criteria.component.scss b/src/app/organization/loan-provisioning-criteria/view-loan-provisioning-criteria/view-loan-provisioning-criteria.component.scss
index edd3222222..9b6a49f879 100644
--- a/src/app/organization/loan-provisioning-criteria/view-loan-provisioning-criteria/view-loan-provisioning-criteria.component.scss
+++ b/src/app/organization/loan-provisioning-criteria/view-loan-provisioning-criteria/view-loan-provisioning-criteria.component.scss
@@ -1,3 +1,15 @@
+.criteria-title {
+ margin-bottom: 0;
+ display: inline;
+ vertical-align: middle;
+}
+
+.loan-product-label {
+ margin-left: 24px;
+ font-size: 1.1em;
+ vertical-align: middle;
+}
+
table {
width: 100%;
}
diff --git a/src/app/organization/loan-provisioning-criteria/view-loan-provisioning-criteria/view-loan-provisioning-criteria.component.ts b/src/app/organization/loan-provisioning-criteria/view-loan-provisioning-criteria/view-loan-provisioning-criteria.component.ts
index e6f902b49a..81e4fb8055 100644
--- a/src/app/organization/loan-provisioning-criteria/view-loan-provisioning-criteria/view-loan-provisioning-criteria.component.ts
+++ b/src/app/organization/loan-provisioning-criteria/view-loan-provisioning-criteria/view-loan-provisioning-criteria.component.ts
@@ -25,6 +25,8 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatDivider } from '@angular/material/divider';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
+import { LoanProduct } from 'app/products/loan-products/models/loan-product.model';
+
/**
* View Loan Provisioning
*/
@@ -96,9 +98,14 @@ export class ViewLoanProvisioningCriteriaComponent implements OnInit {
setLoanProvisioningSelectedCriteria() {
this.dataSource = new MatTableDataSource(this.provisioningData.definitions);
- /** Get load products as a string. */
- for (let _id = 0; _id < this.provisioningData.loanProducts.length; _id++) {
- this.loanProducts += this.provisioningData.loanProducts[_id].name + ',';
+ // Get loan products as a comma-separated string, no trailing comma, with type safety
+ if (this.provisioningData.loanProducts && this.provisioningData.loanProducts.length > 0) {
+ this.loanProducts = (this.provisioningData.loanProducts as LoanProduct[])
+ .filter((p) => p && p.name)
+ .map((p) => p.name)
+ .join(', ');
+ } else {
+ this.loanProducts = '';
}
}
@@ -111,9 +118,14 @@ export class ViewLoanProvisioningCriteriaComponent implements OnInit {
});
deleteCriteriaDialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
- this.organizationService.deleteProvisioningCriteria(this.provisioningData.criteriaId).subscribe(() => {
- this.router.navigate(['/organization/provisioningcriteria']);
- });
+ this.organizationService.deleteProvisioningCriteria(this.provisioningData.criteriaId).subscribe(
+ () => {
+ this.router.navigate(['/organization/provisioning-criteria']);
+ },
+ (error) => {
+ console.error('Failed to delete provisioning criteria:', error);
+ }
+ );
}
});
}
diff --git a/src/app/organization/payment-types/payment-types.component.scss b/src/app/organization/payment-types/payment-types.component.scss
index db8feaf39c..86400c0206 100644
--- a/src/app/organization/payment-types/payment-types.component.scss
+++ b/src/app/organization/payment-types/payment-types.component.scss
@@ -1,3 +1,8 @@
+.table-container {
+ border-radius: 10px;
+ overflow: hidden;
+}
+
table {
width: 100%;
}
@@ -9,13 +14,3 @@ table {
.false {
color: #f44366;
}
-
-.text-center {
- text-align: center;
-}
-
-.nowrap {
- display: inline-flex;
- align-items: center;
- white-space: nowrap;
-}
diff --git a/src/app/pipes/find.pipe.ts b/src/app/pipes/find.pipe.ts
index 81d35d879b..669a050beb 100644
--- a/src/app/pipes/find.pipe.ts
+++ b/src/app/pipes/find.pipe.ts
@@ -1,12 +1,53 @@
-import { Pipe, PipeTransform } from '@angular/core';
+import { Pipe, PipeTransform, SecurityContext } from '@angular/core';
+import { DomSanitizer } from '@angular/platform-browser';
+
+/**
+ * Cache for storing lookup maps per options array reference.
+ * Uses WeakMap to allow garbage collection when options arrays are no longer referenced.
+ * Structure: WeakMap>>
+ */
+const lookupCache = new WeakMap>>();
@Pipe({ name: 'find' })
export class FindPipe implements PipeTransform {
- transform(value: any, options: any, key: string, property: string): any {
- let optionFound;
- if (options) {
- optionFound = options.find((option: any) => option[key] === value);
+ constructor(private sanitizer: DomSanitizer) {}
+
+ transform(value: any, options: any, key: string, property: string): string {
+ if (!options || !key || value === null || value === undefined) {
+ return '';
+ }
+
+ // Get or create the cache for this options array
+ let keyMap = lookupCache.get(options);
+ if (!keyMap) {
+ keyMap = new Map>();
+ lookupCache.set(options, keyMap);
+ }
+
+ // Get or create the lookup map for this specific key
+ let valueMap = keyMap.get(key);
+ if (!valueMap) {
+ // Build the lookup map: O(n) - but only once per options array + key combination
+ valueMap = new Map();
+ if (Array.isArray(options)) {
+ for (const option of options) {
+ if (option && option[key] !== undefined && option[key] !== null) {
+ valueMap.set(option[key], option);
+ }
+ }
+ }
+ keyMap.set(key, valueMap);
}
- return optionFound ? optionFound[property] : '';
+
+ // O(1) lookup
+ const optionFound = valueMap.get(value);
+ const result = optionFound ? (optionFound[property] ?? '') : '';
+
+ // Sanitize string results to prevent XSS
+ if (typeof result === 'string') {
+ return this.sanitizer.sanitize(SecurityContext.HTML, result) || '';
+ }
+
+ return String(result || '');
}
}
diff --git a/src/app/pipes/url-to-string.pipe.spec.ts b/src/app/pipes/url-to-string.pipe.spec.ts
new file mode 100644
index 0000000000..e2f6a889a5
--- /dev/null
+++ b/src/app/pipes/url-to-string.pipe.spec.ts
@@ -0,0 +1,111 @@
+import { UrlToStringPipe } from './url-to-string.pipe';
+
+describe('UrlToStringPipe', () => {
+ let pipe: UrlToStringPipe;
+
+ beforeEach(() => {
+ pipe = new UrlToStringPipe();
+ });
+
+ it('should create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ describe('URL Transformations', () => {
+ it('should transform single-segment URL', () => {
+ expect(pipe.transform('/users')).toBe('Users');
+ });
+
+ it('should transform multi-segment URL with pipe separators', () => {
+ expect(pipe.transform('/admin/system/configuration')).toBe('Admin | System | Configuration');
+ });
+
+ it('should capitalize first letter of each word', () => {
+ expect(pipe.transform('/dashboard')).toBe('Dashboard');
+ });
+
+ it('should transform hyphenated segments into space-separated words', () => {
+ expect(pipe.transform('/user-management')).toBe('User Management');
+ });
+
+ it('should handle multiple hyphens in single segment', () => {
+ expect(pipe.transform('/loan-product-details')).toBe('Loan Product Details');
+ });
+
+ it('should handle hyphens in multiple segments', () => {
+ expect(pipe.transform('/client-accounts/savings-accounts')).toBe('Client Accounts | Savings Accounts');
+ });
+
+ it('should remove query parameters from URL', () => {
+ expect(pipe.transform('/users?id=123')).toBe('Users');
+ });
+
+ it('should handle query parameters in multi-segment URLs', () => {
+ expect(pipe.transform('/clients/details?clientId=456')).toBe('Clients | Details');
+ });
+
+ it('should handle multiple query parameters', () => {
+ expect(pipe.transform('/reports/view?type=loan&status=active')).toBe('Reports | View');
+ });
+
+ it('should decode URL-encoded spaces', () => {
+ expect(pipe.transform('/users%20management')).toBe('Users management');
+ });
+
+ it('should decode URL-encoded special characters', () => {
+ expect(pipe.transform('/client%20%26%20accounts')).toBe('Client & accounts');
+ });
+
+ it('should handle URL with only leading slash', () => {
+ expect(pipe.transform('/')).toBe('');
+ });
+
+ it('should handle URL with trailing slash', () => {
+ expect(pipe.transform('/users/')).toBe('Users | ');
+ });
+
+ it('should handle empty segments', () => {
+ expect(pipe.transform('/users//accounts')).toBe('Users | | Accounts');
+ });
+
+ it('should handle single character segments', () => {
+ expect(pipe.transform('/a/b/c')).toBe('A | B | C');
+ });
+
+ it('should handle numeric segments', () => {
+ expect(pipe.transform('/client/123/accounts')).toBe('Client | 123 | Accounts');
+ });
+
+ it('should handle mixed case input', () => {
+ expect(pipe.transform('/UserManagement/ClientAccounts')).toBe('UserManagement | ClientAccounts');
+ });
+
+ it('should handle uppercase input', () => {
+ expect(pipe.transform('/API/Settings')).toBe('API | Settings');
+ });
+ });
+
+ describe('Real-World Usage', () => {
+ it('should transform navigation breadcrumb URL', () => {
+ expect(pipe.transform('/clients/view-client/general')).toBe('Clients | View Client | General');
+ });
+
+ it('should transform settings page URL', () => {
+ expect(pipe.transform('/system/manage-data-tables')).toBe('System | Manage Data Tables');
+ });
+
+ it('should transform report URL with query params', () => {
+ expect(pipe.transform('/reports/run-report?reportId=5')).toBe('Reports | Run Report');
+ });
+
+ it('should transform user profile edit URL', () => {
+ expect(pipe.transform('/users/edit-user/123')).toBe('Users | Edit User | 123');
+ });
+
+ it('should transform deep nested URL', () => {
+ expect(pipe.transform('/products/loan-products/edit-loan-product/details')).toBe(
+ 'Products | Loan Products | Edit Loan Product | Details'
+ );
+ });
+ });
+});
diff --git a/src/app/products/charges/create-charge/create-charge.component.ts b/src/app/products/charges/create-charge/create-charge.component.ts
index a23c21465c..e29b88aee9 100644
--- a/src/app/products/charges/create-charge/create-charge.component.ts
+++ b/src/app/products/charges/create-charge/create-charge.component.ts
@@ -78,13 +78,12 @@ export class CreateChargeComponent implements OnInit {
constructor() {
this.route.data.subscribe((data: { chargesTemplate: any }) => {
this.chargesTemplateData = data.chargesTemplate;
- if (data.chargesTemplate.incomeOrLiabilityAccountOptions.liabilityAccountOptions) {
- this.incomeAndLiabilityAccountData =
- data.chargesTemplate.incomeOrLiabilityAccountOptions.incomeAccountOptions.concat(
- data.chargesTemplate.incomeOrLiabilityAccountOptions.liabilityAccountOptions
- );
+ const incomeOptions = data.chargesTemplate.incomeOrLiabilityAccountOptions.incomeAccountOptions || [];
+ const liabilityOptions = data.chargesTemplate.incomeOrLiabilityAccountOptions.liabilityAccountOptions || [];
+ if (liabilityOptions.length > 0) {
+ this.incomeAndLiabilityAccountData = incomeOptions.concat(liabilityOptions);
} else {
- this.incomeAndLiabilityAccountData = data.chargesTemplate.incomeOrLiabilityAccountOptions.incomeAccountOptions;
+ this.incomeAndLiabilityAccountData = incomeOptions;
}
});
}
diff --git a/src/app/products/collaterals/view-collateral/view-collateral.component.spec.ts b/src/app/products/collaterals/view-collateral/view-collateral.component.spec.ts
new file mode 100644
index 0000000000..27bf9f5318
--- /dev/null
+++ b/src/app/products/collaterals/view-collateral/view-collateral.component.spec.ts
@@ -0,0 +1,929 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ActivatedRoute, Router } from '@angular/router';
+import { MatDialog, MatDialogRef } from '@angular/material/dialog';
+import { of, throwError, BehaviorSubject } from 'rxjs';
+import { ViewCollateralComponent } from './view-collateral.component';
+import { ProductsService } from 'app/products/products.service';
+import { TranslateService, TranslateModule } from '@ngx-translate/core';
+import { DeleteDialogComponent } from '../../../shared/delete-dialog/delete-dialog.component';
+import { provideNativeDateAdapter } from '@angular/material/core';
+import { FaIconLibrary } from '@fortawesome/angular-fontawesome';
+import * as solidIcons from '@fortawesome/free-solid-svg-icons';
+import { describe, it, expect, jest, beforeEach } from '@jest/globals';
+import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
+import { AuthenticationService } from 'app/core/authentication/authentication.service';
+import { SettingsService } from 'app/settings/settings.service';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+
+describe('ViewCollateralComponent - Integration Tests', () => {
+ let component: ViewCollateralComponent;
+ let fixture: ComponentFixture;
+ let mockRouter: jest.Mocked;
+ let mockDialog: jest.Mocked;
+ let mockProductsService: jest.Mocked;
+ let mockTranslateService: any;
+ let routeDataSubject: BehaviorSubject;
+
+ const mockCollateralData: any = {
+ id: 1,
+ name: 'Gold Collateral',
+ quality: 'High Quality',
+ basePrice: 50000,
+ pctToBase: 80,
+ currency: {
+ code: 'USD',
+ name: 'US Dollar',
+ displaySymbol: '$'
+ },
+ unitType: 'kg'
+ };
+
+ beforeEach(async () => {
+ mockRouter = {
+ navigate: jest.fn()
+ } as any;
+
+ mockDialog = {
+ open: jest.fn()
+ } as any;
+
+ mockProductsService = {
+ deleteCollateral: jest.fn()
+ } as any;
+
+ mockTranslateService = {
+ instant: jest.fn((key: string) => key),
+ get: jest.fn((key: string) => of(key)),
+ onLangChange: of({ lang: 'en' }),
+ onTranslationChange: of({}),
+ onDefaultLangChange: of({ lang: 'en' })
+ };
+
+ const mockAuthenticationService = {
+ isAuthenticated: jest.fn<() => boolean>().mockReturnValue(true),
+ getCredentials: jest.fn().mockReturnValue({
+ username: 'testuser',
+ accessToken: 'test-token',
+ permissions: ['ALL_FUNCTIONS']
+ })
+ };
+
+ const mockSettingsService = {
+ language: { code: 'en' },
+ dateFormat: 'dd MMMM yyyy'
+ };
+
+ routeDataSubject = new BehaviorSubject({
+ collateral: { ...mockCollateralData }
+ });
+
+ await TestBed.configureTestingModule({
+ imports: [
+ ViewCollateralComponent,
+ TranslateModule.forRoot()
+ ],
+ providers: [
+ { provide: Router, useValue: mockRouter },
+ { provide: MatDialog, useValue: mockDialog },
+ { provide: ProductsService, useValue: mockProductsService },
+ { provide: TranslateService, useValue: mockTranslateService },
+ { provide: AuthenticationService, useValue: mockAuthenticationService },
+ { provide: SettingsService, useValue: mockSettingsService },
+ {
+ provide: ActivatedRoute,
+ useValue: {
+ data: routeDataSubject.asObservable(),
+ snapshot: { data: routeDataSubject.value }
+ }
+ },
+ provideNativeDateAdapter(),
+ provideAnimationsAsync()
+
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
+ }).compileComponents();
+
+ // Add all FontAwesome solid icons to handle all child component icon requirements
+ const faIconLibrary = TestBed.inject(FaIconLibrary);
+ const iconList = Object.keys(solidIcons)
+ .filter((key) => key !== 'fas' && key !== 'prefix' && key.startsWith('fa'))
+ .map((icon) => (solidIcons as any)[icon]);
+ faIconLibrary.addIcons(...iconList);
+
+ fixture = TestBed.createComponent(ViewCollateralComponent);
+ component = fixture.componentInstance;
+ });
+
+ describe('Component Initialization', () => {
+ it('should create the component', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should load collateral data from route resolver', () => {
+ fixture.detectChanges();
+ expect(component.collateralData).toEqual(mockCollateralData);
+ });
+
+ it('should subscribe to route data on initialization', () => {
+ expect(component.collateralData).toBeDefined();
+ expect(component.collateralData.id).toBe(1);
+ expect(component.collateralData.name).toBe('Gold Collateral');
+ });
+
+ it('should handle collateral data with all properties', () => {
+ fixture.detectChanges();
+ expect(component.collateralData.basePrice).toBe(50000);
+ expect(component.collateralData.pctToBase).toBe(80);
+ expect(component.collateralData.unitType).toBe('kg');
+ });
+ });
+
+ describe('Route Data Subscription', () => {
+ it('should update collateral data when route data changes', () => {
+ const updatedCollateralData = {
+ id: 2,
+ name: 'Silver Collateral',
+ quality: 'Medium Quality',
+ basePrice: 30000
+ };
+
+ routeDataSubject.next({ collateral: updatedCollateralData });
+
+ expect(component.collateralData).toEqual(updatedCollateralData);
+ expect(component.collateralData.name).toBe('Silver Collateral');
+ expect(component.collateralData.basePrice).toBe(30000);
+ });
+
+ it('should handle multiple route data updates', () => {
+ const firstUpdate = {
+ id: 2,
+ name: 'First Collateral',
+ basePrice: 20000
+ };
+
+ const secondUpdate = {
+ id: 3,
+ name: 'Second Collateral',
+ basePrice: 30000
+ };
+
+ routeDataSubject.next({ collateral: firstUpdate });
+ expect(component.collateralData.name).toBe('First Collateral');
+
+ routeDataSubject.next({ collateral: secondUpdate });
+ expect(component.collateralData.name).toBe('Second Collateral');
+ });
+
+ it('should maintain subscription throughout component lifecycle', () => {
+ fixture.detectChanges();
+ const initialData = component.collateralData;
+ expect(initialData).toBeDefined();
+
+ const newData = {
+ id: 99,
+ name: 'Updated Collateral',
+ basePrice: 75000
+ };
+
+ routeDataSubject.next({ collateral: newData });
+ expect(component.collateralData).toEqual(newData);
+ expect(component.collateralData).not.toEqual(initialData);
+ });
+ });
+
+ describe('Collateral Data Properties', () => {
+ it('should correctly handle collateral with id', () => {
+ fixture.detectChanges();
+ expect(component.collateralData.id).toBe(1);
+ });
+
+ it('should correctly handle collateral name', () => {
+ fixture.detectChanges();
+ expect(component.collateralData.name).toBe('Gold Collateral');
+ expect(typeof component.collateralData.name).toBe('string');
+ });
+
+ it('should correctly handle collateral quality', () => {
+ fixture.detectChanges();
+ expect(component.collateralData.quality).toBe('High Quality');
+ });
+
+ it('should correctly handle base price', () => {
+ fixture.detectChanges();
+ expect(component.collateralData.basePrice).toBe(50000);
+ expect(typeof component.collateralData.basePrice).toBe('number');
+ });
+
+ it('should correctly handle percentage to base', () => {
+ fixture.detectChanges();
+ expect(component.collateralData.pctToBase).toBe(80);
+ });
+
+ it('should correctly handle currency information', () => {
+ fixture.detectChanges();
+ expect(component.collateralData.currency).toBeDefined();
+ expect(component.collateralData.currency.code).toBe('USD');
+ expect(component.collateralData.currency.name).toBe('US Dollar');
+ expect(component.collateralData.currency.displaySymbol).toBe('$');
+ });
+
+ it('should correctly handle unit type', () => {
+ fixture.detectChanges();
+ expect(component.collateralData.unitType).toBe('kg');
+ });
+
+ it('should correctly handle nested currency object', () => {
+ fixture.detectChanges();
+ expect(component.collateralData.currency).toBeDefined();
+ expect(component.collateralData.currency.code).toBe('USD');
+ });
+ });
+
+ describe('Delete Collateral Functionality', () => {
+ let mockDialogRef: jest.Mocked>;
+
+ beforeEach(() => {
+ mockDialogRef = {
+ afterClosed: jest.fn()
+ } as any;
+
+ mockDialog.open.mockReturnValue(mockDialogRef);
+ mockDialogRef.afterClosed.mockReturnValue(of({ delete: false }));
+ fixture.detectChanges();
+ });
+
+ it('should open delete dialog when deleteCollateral is called', () => {
+ component.deleteCollateral();
+
+ expect(mockDialog.open).toHaveBeenCalledWith(
+ DeleteDialogComponent,
+ expect.objectContaining({
+ data: expect.objectContaining({
+ deleteContext: expect.any(String)
+ })
+ })
+ );
+ });
+
+ it('should pass correct delete context to dialog', () => {
+ mockTranslateService.instant.mockReturnValue('Collateral');
+ component.deleteCollateral();
+
+ expect(mockDialog.open).toHaveBeenCalledWith(
+ DeleteDialogComponent,
+ expect.objectContaining({
+ data: {
+ deleteContext: 'Collateral 1'
+ }
+ })
+ );
+ });
+
+ it('should call translateService.instant with correct key', () => {
+ component.deleteCollateral();
+
+ expect(mockTranslateService.instant).toHaveBeenCalledWith('labels.text.Collateral');
+ });
+
+ it('should delete collateral when user confirms', () => {
+ mockDialogRef.afterClosed.mockReturnValue(of({ delete: true }));
+ mockProductsService.deleteCollateral.mockReturnValue(of({}));
+
+ component.deleteCollateral();
+
+ expect(mockProductsService.deleteCollateral).toHaveBeenCalledWith(1);
+ });
+
+ it('should NOT delete collateral when user cancels', () => {
+ mockDialogRef.afterClosed.mockReturnValue(of({ delete: false }));
+
+ component.deleteCollateral();
+
+ expect(mockProductsService.deleteCollateral).not.toHaveBeenCalled();
+ });
+
+ it('should navigate to collaterals list after successful deletion', () => {
+ mockDialogRef.afterClosed.mockReturnValue(of({ delete: true }));
+ mockProductsService.deleteCollateral.mockReturnValue(of({}));
+
+ component.deleteCollateral();
+
+ expect(mockRouter.navigate).toHaveBeenCalledWith(['/products/collaterals']);
+ });
+
+ it('should call deleteCollateral with correct collateral id', () => {
+ mockDialogRef.afterClosed.mockReturnValue(of({ delete: true }));
+ mockProductsService.deleteCollateral.mockReturnValue(of({}));
+
+ component.deleteCollateral();
+
+ expect(mockProductsService.deleteCollateral).toHaveBeenCalledWith(mockCollateralData.id);
+ });
+
+ it('should handle dialog close without response', () => {
+ mockDialogRef.afterClosed.mockReturnValue(of({}));
+
+ expect(() => {
+ component.deleteCollateral();
+ }).not.toThrow();
+ });
+
+ it('should handle undefined dialog response', () => {
+ mockDialogRef.afterClosed.mockReturnValue(of(undefined));
+
+ component.deleteCollateral();
+
+ expect(mockProductsService.deleteCollateral).not.toHaveBeenCalled();
+ });
+
+ it('should handle null dialog response', () => {
+ mockDialogRef.afterClosed.mockReturnValue(of(null));
+
+ component.deleteCollateral();
+
+ expect(mockProductsService.deleteCollateral).not.toHaveBeenCalled();
+ });
+
+ it('should handle delete response with false flag', () => {
+ mockDialogRef.afterClosed.mockReturnValue(of({ delete: false }));
+
+ component.deleteCollateral();
+
+ expect(mockProductsService.deleteCollateral).not.toHaveBeenCalled();
+ expect(mockRouter.navigate).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('Delete Error Handling', () => {
+ let mockDialogRef: jest.Mocked>;
+
+ beforeEach(() => {
+ mockDialogRef = {
+ afterClosed: jest.fn()
+ } as any;
+
+ mockDialog.open.mockReturnValue(mockDialogRef);
+ fixture.detectChanges();
+ });
+
+ it('should handle API error during deletion', () => {
+ mockDialogRef.afterClosed.mockReturnValue(of({ delete: true }));
+ mockProductsService.deleteCollateral.mockReturnValue(throwError(() => new Error('API Error')));
+
+ expect(() => {
+ component.deleteCollateral();
+ }).not.toThrow();
+ });
+
+ it('should handle network error during deletion', () => {
+ mockDialogRef.afterClosed.mockReturnValue(of({ delete: true }));
+ mockProductsService.deleteCollateral.mockReturnValue(throwError(() => ({ status: 0, message: 'Network Error' })));
+
+ component.deleteCollateral();
+
+ expect(mockRouter.navigate).not.toHaveBeenCalled();
+ });
+
+ it('should handle 404 error during deletion', () => {
+ mockDialogRef.afterClosed.mockReturnValue(of({ delete: true }));
+ mockProductsService.deleteCollateral.mockReturnValue(throwError(() => ({ status: 404, message: 'Not Found' })));
+
+ component.deleteCollateral();
+
+ expect(mockRouter.navigate).not.toHaveBeenCalled();
+ });
+
+ it('should handle 500 error during deletion', () => {
+ mockDialogRef.afterClosed.mockReturnValue(of({ delete: true }));
+ mockProductsService.deleteCollateral.mockReturnValue(
+ throwError(() => ({ status: 500, message: 'Server Error' }))
+ );
+
+ component.deleteCollateral();
+
+ expect(mockRouter.navigate).not.toHaveBeenCalled();
+ });
+
+ it('should handle deletion service unavailable', () => {
+ mockDialogRef.afterClosed.mockReturnValue(of({ delete: true }));
+ mockProductsService.deleteCollateral.mockReturnValue(
+ throwError(() => ({ status: 503, message: 'Service Unavailable' }))
+ );
+
+ expect(() => {
+ component.deleteCollateral();
+ }).not.toThrow();
+ });
+ });
+
+ describe('Translation Service Integration', () => {
+ let mockDialogRef: jest.Mocked>;
+
+ beforeEach(() => {
+ mockDialogRef = {
+ afterClosed: jest.fn().mockReturnValue(of({ delete: false }))
+ } as any;
+
+ mockDialog.open.mockReturnValue(mockDialogRef);
+ fixture.detectChanges();
+ });
+
+ it('should translate delete context label', () => {
+ mockTranslateService.instant.mockReturnValue('Garantía');
+ component.deleteCollateral();
+
+ const callArgs = mockDialog.open.mock.calls[0][1] as any;
+ expect(callArgs.data.deleteContext).toContain('Garantía');
+ });
+
+ it('should handle translation service returning empty string', () => {
+ mockTranslateService.instant.mockReturnValue('');
+ component.deleteCollateral();
+
+ const callArgs = mockDialog.open.mock.calls[0][1] as any;
+ expect(callArgs.data.deleteContext).toBe(' 1');
+ });
+
+ it('should handle translation service returning undefined', () => {
+ mockTranslateService.instant.mockReturnValue(undefined as any);
+ component.deleteCollateral();
+
+ const callArgs = mockDialog.open.mock.calls[0][1] as any;
+ expect(callArgs.data.deleteContext).toContain('1');
+ });
+
+ it('should concatenate translation with collateral id', () => {
+ mockTranslateService.instant.mockReturnValue('Collateral');
+ component.deleteCollateral();
+
+ const callArgs = mockDialog.open.mock.calls[0][1] as any;
+ expect(callArgs.data.deleteContext).toBe('Collateral 1');
+ });
+
+ it('should handle different language translations', () => {
+ const translations = [
+ 'Collateral',
+ 'Garantía',
+ 'Sicherheit',
+ '担保'
+ ];
+
+ translations.forEach((translation) => {
+ mockTranslateService.instant.mockReturnValue(translation);
+ component.deleteCollateral();
+
+ const callArgs = mockDialog.open.mock.calls[mockDialog.open.mock.calls.length - 1][1] as any;
+ expect(callArgs.data.deleteContext).toContain(translation);
+ });
+ });
+ });
+
+ describe('Router Navigation', () => {
+ let mockDialogRef: jest.Mocked>;
+
+ beforeEach(() => {
+ mockDialogRef = {
+ afterClosed: jest.fn()
+ } as any;
+
+ mockDialog.open.mockReturnValue(mockDialogRef);
+ fixture.detectChanges();
+ });
+
+ it('should navigate to correct route after deletion', () => {
+ mockDialogRef.afterClosed.mockReturnValue(of({ delete: true }));
+ mockProductsService.deleteCollateral.mockReturnValue(of({}));
+
+ component.deleteCollateral();
+
+ expect(mockRouter.navigate).toHaveBeenCalledWith(['/products/collaterals']);
+ });
+
+ it('should navigate with correct path array', () => {
+ mockDialogRef.afterClosed.mockReturnValue(of({ delete: true }));
+ mockProductsService.deleteCollateral.mockReturnValue(of({}));
+
+ component.deleteCollateral();
+
+ const navArgs = mockRouter.navigate.mock.calls[0][0];
+ expect(Array.isArray(navArgs)).toBe(true);
+ expect(navArgs).toEqual(['/products/collaterals']);
+ });
+
+ it('should only navigate after successful deletion', () => {
+ mockDialogRef.afterClosed.mockReturnValue(of({ delete: true }));
+ mockProductsService.deleteCollateral.mockReturnValue(of({}));
+
+ component.deleteCollateral();
+
+ expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
+ });
+
+ it('should not navigate if deletion fails', () => {
+ mockDialogRef.afterClosed.mockReturnValue(of({ delete: true }));
+ mockProductsService.deleteCollateral.mockReturnValue(throwError(() => new Error('Delete failed')));
+
+ component.deleteCollateral();
+
+ expect(mockRouter.navigate).not.toHaveBeenCalled();
+ });
+
+ it('should not navigate if user cancels', () => {
+ mockDialogRef.afterClosed.mockReturnValue(of({ delete: false }));
+
+ component.deleteCollateral();
+
+ expect(mockRouter.navigate).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('Edge Cases and Error Handling', () => {
+ it('should handle collateral data with minimal properties', () => {
+ const minimalCollateral = {
+ id: 1,
+ name: 'Simple Collateral'
+ };
+
+ routeDataSubject.next({ collateral: minimalCollateral });
+ expect(component.collateralData).toEqual(minimalCollateral);
+ expect(component.collateralData.name).toBe('Simple Collateral');
+ });
+
+ it('should handle collateral data with zero base price', () => {
+ const zeroPrice = {
+ id: 1,
+ name: 'Zero Price',
+ basePrice: 0
+ };
+
+ routeDataSubject.next({ collateral: zeroPrice });
+ expect(component.collateralData.basePrice).toBe(0);
+ });
+
+ it('should handle collateral data with high base price', () => {
+ const highPrice = {
+ id: 1,
+ name: 'High Price',
+ basePrice: 999999999
+ };
+
+ routeDataSubject.next({ collateral: highPrice });
+ expect(component.collateralData.basePrice).toBe(999999999);
+ });
+
+ it('should handle collateral with null currency', () => {
+ const nullCurrency: any = {
+ id: 1,
+ name: 'No Currency',
+ basePrice: 10000,
+ currency: null
+ };
+
+ routeDataSubject.next({ collateral: nullCurrency });
+ expect(component.collateralData.currency).toBeNull();
+ });
+
+ it('should handle collateral with undefined properties', () => {
+ const undefinedProps: any = {
+ id: 1,
+ name: 'Collateral',
+ basePrice: undefined,
+ quality: undefined
+ };
+
+ routeDataSubject.next({ collateral: undefinedProps });
+ expect(component.collateralData.basePrice).toBeUndefined();
+ expect(component.collateralData.quality).toBeUndefined();
+ });
+
+ it('should handle empty collateral data object', () => {
+ const emptyData = {};
+
+ routeDataSubject.next({ collateral: emptyData });
+ expect(component.collateralData).toEqual(emptyData);
+ });
+
+ it('should handle collateral with special characters in name', () => {
+ const specialChars = {
+ id: 1,
+ name: 'Collateral & Assets @ 100%',
+ basePrice: 15000
+ };
+
+ routeDataSubject.next({ collateral: specialChars });
+ expect(component.collateralData.name).toBe('Collateral & Assets @ 100%');
+ });
+
+ it('should handle collateral with very long name', () => {
+ const longName = 'A'.repeat(200);
+ const longNameCollateral = {
+ id: 1,
+ name: longName,
+ basePrice: 10000
+ };
+
+ routeDataSubject.next({ collateral: longNameCollateral });
+ expect(component.collateralData.name).toBe(longName);
+ expect(component.collateralData.name.length).toBe(200);
+ });
+
+ it('should handle collateral with negative id', () => {
+ const negativeId = {
+ id: -1,
+ name: 'Invalid Collateral',
+ basePrice: 10000
+ };
+
+ routeDataSubject.next({ collateral: negativeId });
+ expect(component.collateralData.id).toBe(-1);
+ });
+
+ it('should handle collateral with negative base price', () => {
+ const negativePrice = {
+ id: 1,
+ name: 'Negative Price',
+ basePrice: -5000
+ };
+
+ routeDataSubject.next({ collateral: negativePrice });
+ expect(component.collateralData.basePrice).toBe(-5000);
+ });
+ });
+
+ describe('Component Rendering', () => {
+ it('should render component without errors when data is provided', () => {
+ expect(() => {
+ fixture.detectChanges();
+ }).not.toThrow();
+ });
+
+ it('should update view when collateral data changes', () => {
+ fixture.detectChanges();
+ const initialName = component.collateralData.name;
+
+ const newData = {
+ id: 2,
+ name: 'Updated Collateral Name',
+ basePrice: 25000
+ };
+
+ routeDataSubject.next({ collateral: newData });
+ fixture.detectChanges();
+
+ expect(component.collateralData.name).not.toBe(initialName);
+ expect(component.collateralData.name).toBe('Updated Collateral Name');
+ });
+
+ it('should handle component destruction gracefully', () => {
+ fixture.detectChanges();
+ expect(() => {
+ fixture.destroy();
+ }).not.toThrow();
+ });
+ });
+
+ describe('Integration with ActivatedRoute', () => {
+ it('should correctly inject ActivatedRoute', () => {
+ const activatedRoute = TestBed.inject(ActivatedRoute);
+ expect(activatedRoute).toBeDefined();
+ });
+
+ it('should access route data through subscription', () => {
+ let dataEmitted = false;
+ const activatedRoute = TestBed.inject(ActivatedRoute);
+
+ activatedRoute.data.subscribe((data) => {
+ dataEmitted = true;
+ expect(data.collateral).toBeDefined();
+ });
+
+ expect(dataEmitted).toBe(true);
+ });
+
+ it('should handle route snapshot data', () => {
+ const activatedRoute = TestBed.inject(ActivatedRoute);
+ expect(activatedRoute.snapshot.data.collateral).toBeDefined();
+ expect(activatedRoute.snapshot.data.collateral).toEqual(mockCollateralData);
+ });
+ });
+
+ describe('Complete Collateral View Workflow', () => {
+ it('should complete full view workflow', () => {
+ // 1. Component initializes
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+
+ // 2. Collateral data is loaded
+ expect(component.collateralData).toBeDefined();
+ expect(component.collateralData.id).toBe(1);
+
+ // 3. Data is correctly populated
+ expect(component.collateralData.name).toBe('Gold Collateral');
+ expect(component.collateralData.basePrice).toBe(50000);
+ expect(component.collateralData.pctToBase).toBe(80);
+
+ // 4. Component can handle data updates
+ const updatedData = {
+ id: 1,
+ name: 'Updated Gold',
+ basePrice: 55000
+ };
+
+ routeDataSubject.next({ collateral: updatedData });
+ expect(component.collateralData.name).toBe('Updated Gold');
+ expect(component.collateralData.basePrice).toBe(55000);
+ });
+
+ it('should complete full delete workflow', () => {
+ const mockDialogRef = {
+ afterClosed: jest.fn().mockReturnValue(of({ delete: true }))
+ } as any;
+
+ mockDialog.open.mockReturnValue(mockDialogRef);
+ mockProductsService.deleteCollateral.mockReturnValue(of({}));
+
+ fixture.detectChanges();
+
+ // Execute delete
+ component.deleteCollateral();
+
+ // Verify workflow
+ expect(mockDialog.open).toHaveBeenCalled();
+ expect(mockProductsService.deleteCollateral).toHaveBeenCalledWith(1);
+ expect(mockRouter.navigate).toHaveBeenCalledWith(['/products/collaterals']);
+ });
+
+ it('should maintain data integrity throughout component lifecycle', () => {
+ // Initial state
+ fixture.detectChanges();
+ const initialId = component.collateralData.id;
+
+ // Multiple updates
+ for (let i = 1; i <= 5; i++) {
+ const newData = {
+ id: initialId,
+ name: `Collateral Update ${i}`,
+ basePrice: 10000 + i * 1000
+ };
+ routeDataSubject.next({ collateral: newData });
+ expect(component.collateralData.name).toBe(`Collateral Update ${i}`);
+ expect(component.collateralData.basePrice).toBe(10000 + i * 1000);
+ }
+
+ // Final state verification
+ expect(component.collateralData.id).toBe(initialId);
+ });
+ });
+
+ describe('Data Type Validation', () => {
+ it('should handle collateral with all string values', () => {
+ const stringValues = {
+ id: '1',
+ name: 'String Collateral',
+ basePrice: '50000',
+ pctToBase: '80'
+ };
+
+ routeDataSubject.next({ collateral: stringValues });
+ expect(component.collateralData.id).toBe('1');
+ expect(component.collateralData.basePrice).toBe('50000');
+ });
+
+ it('should handle collateral with mixed data types', () => {
+ const mixedTypes = {
+ id: 1,
+ name: 'Mixed Collateral',
+ basePrice: '50000',
+ isActive: true,
+ pctToBase: 80
+ };
+
+ routeDataSubject.next({ collateral: mixedTypes });
+ expect(component.collateralData.isActive).toBe(true);
+ expect(typeof component.collateralData.id).toBe('number');
+ });
+
+ it('should handle collateral with nested objects', () => {
+ const nestedObjects = {
+ id: 1,
+ name: 'Nested Collateral',
+ currency: {
+ code: 'USD',
+ name: 'US Dollar',
+ details: {
+ symbol: '$',
+ locale: 'en-US'
+ }
+ }
+ };
+
+ routeDataSubject.next({ collateral: nestedObjects });
+ expect(component.collateralData.currency.details).toBeDefined();
+ expect(component.collateralData.currency.details.symbol).toBe('$');
+ });
+ });
+
+ describe('Memory and Performance', () => {
+ it('should not create memory leaks with multiple updates', () => {
+ fixture.detectChanges();
+
+ for (let i = 0; i < 100; i++) {
+ const newData = {
+ id: i,
+ name: `Collateral ${i}`,
+ basePrice: i * 1000
+ };
+ routeDataSubject.next({ collateral: newData });
+ }
+
+ expect(component.collateralData.id).toBe(99);
+ expect(component.collateralData.name).toBe('Collateral 99');
+ });
+
+ it('should handle rapid consecutive updates', () => {
+ fixture.detectChanges();
+
+ const updates = [
+ { id: 1, name: 'Collateral 1' },
+ { id: 2, name: 'Collateral 2' },
+ { id: 3, name: 'Collateral 3' }
+ ];
+
+ updates.forEach((update) => {
+ routeDataSubject.next({ collateral: update });
+ });
+
+ expect(component.collateralData.name).toBe('Collateral 3');
+ });
+
+ it('should handle component with large collateral data', () => {
+ const largeData = {
+ ...mockCollateralData,
+ additionalData: new Array(1000).fill({ key: 'value' })
+ };
+
+ routeDataSubject.next({ collateral: largeData });
+ fixture.detectChanges();
+
+ expect(component.collateralData.additionalData.length).toBe(1000);
+ });
+ });
+
+ describe('Dialog Management', () => {
+ it('should open MatDialog with DeleteDialogComponent', () => {
+ const mockDialogRef = {
+ afterClosed: jest.fn().mockReturnValue(of({ delete: false }))
+ } as any;
+
+ mockDialog.open.mockReturnValue(mockDialogRef);
+ fixture.detectChanges();
+
+ component.deleteCollateral();
+
+ expect(mockDialog.open).toHaveBeenCalledWith(DeleteDialogComponent, expect.any(Object));
+ });
+
+ it('should pass dialog configuration', () => {
+ const mockDialogRef = {
+ afterClosed: jest.fn().mockReturnValue(of({ delete: false }))
+ } as any;
+
+ mockDialog.open.mockReturnValue(mockDialogRef);
+ fixture.detectChanges();
+
+ component.deleteCollateral();
+
+ const config = mockDialog.open.mock.calls[0][1];
+ expect(config).toBeDefined();
+ expect(config.data).toBeDefined();
+ });
+
+ it('should subscribe to dialog afterClosed', () => {
+ const mockDialogRef = {
+ afterClosed: jest.fn().mockReturnValue(of({ delete: false }))
+ } as any;
+
+ mockDialog.open.mockReturnValue(mockDialogRef);
+ fixture.detectChanges();
+
+ component.deleteCollateral();
+
+ expect(mockDialogRef.afterClosed).toHaveBeenCalled();
+ });
+
+ it('should handle dialog opening multiple times', () => {
+ const mockDialogRef = {
+ afterClosed: jest.fn().mockReturnValue(of({ delete: false }))
+ } as any;
+
+ mockDialog.open.mockReturnValue(mockDialogRef);
+ fixture.detectChanges();
+
+ component.deleteCollateral();
+ component.deleteCollateral();
+ component.deleteCollateral();
+
+ expect(mockDialog.open).toHaveBeenCalledTimes(3);
+ });
+ });
+});
diff --git a/src/app/products/manage-tax-components/view-tax-component/view-tax-component.component.spec.ts b/src/app/products/manage-tax-components/view-tax-component/view-tax-component.component.spec.ts
new file mode 100644
index 0000000000..6747bb3565
--- /dev/null
+++ b/src/app/products/manage-tax-components/view-tax-component/view-tax-component.component.spec.ts
@@ -0,0 +1,560 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ActivatedRoute, Router } from '@angular/router';
+import { of, BehaviorSubject } from 'rxjs';
+import { ViewTaxComponentComponent } from './view-tax-component.component';
+import { TranslateModule } from '@ngx-translate/core';
+import { provideNativeDateAdapter } from '@angular/material/core';
+import { FaIconLibrary } from '@fortawesome/angular-fontawesome';
+import * as solidIcons from '@fortawesome/free-solid-svg-icons';
+import { describe, it, expect, jest, beforeEach } from '@jest/globals';
+import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
+import { AuthenticationService } from 'app/core/authentication/authentication.service';
+import { SettingsService } from 'app/settings/settings.service';
+import { DecimalPipe, DatePipe } from '@angular/common';
+
+describe('ViewTaxComponentComponent - Integration Tests', () => {
+ let component: ViewTaxComponentComponent;
+ let fixture: ComponentFixture;
+ let mockRouter: jest.Mocked;
+ let routeDataSubject: BehaviorSubject;
+
+ const mockTaxComponentData: any = {
+ id: 1,
+ name: 'VAT',
+ percentage: 15.5,
+ startDate: [
+ 2024,
+ 1,
+ 1
+ ],
+ creditAccount: {
+ id: 101,
+ name: 'Tax Payable',
+ glCode: '2001'
+ },
+ creditAccountId: 101,
+ creditAccountName: 'Tax Payable',
+ creditAccountType: {
+ id: 2,
+ code: 'LIABILITY',
+ value: 'Liability'
+ }
+ };
+
+ beforeEach(async () => {
+ mockRouter = {
+ navigate: jest.fn()
+ } as any;
+
+ const mockAuthenticationService = {
+ isAuthenticated: jest.fn<() => boolean>().mockReturnValue(true),
+ getCredentials: jest.fn().mockReturnValue({
+ username: 'testuser',
+ accessToken: 'test-token',
+ permissions: ['ALL_FUNCTIONS']
+ })
+ };
+
+ const mockSettingsService = {
+ language: { code: 'en' },
+ dateFormat: 'dd MMMM yyyy',
+ decimals: '2'
+ };
+
+ routeDataSubject = new BehaviorSubject({
+ taxComponent: { ...mockTaxComponentData }
+ });
+
+ await TestBed.configureTestingModule({
+ imports: [
+ ViewTaxComponentComponent,
+ TranslateModule.forRoot()
+ ],
+ providers: [
+ { provide: Router, useValue: mockRouter },
+ { provide: AuthenticationService, useValue: mockAuthenticationService },
+ { provide: SettingsService, useValue: mockSettingsService },
+ {
+ provide: ActivatedRoute,
+ useValue: {
+ data: routeDataSubject.asObservable(),
+ snapshot: { data: routeDataSubject.value }
+ }
+ },
+ DatePipe,
+ DecimalPipe,
+ provideNativeDateAdapter(),
+ provideAnimationsAsync()
+
+ ]
+ }).compileComponents();
+
+ // Add all FontAwesome solid icons to handle all child component icon requirements
+ const faIconLibrary = TestBed.inject(FaIconLibrary);
+ const iconList = Object.keys(solidIcons)
+ .filter((key) => key !== 'fas' && key !== 'prefix' && key.startsWith('fa'))
+ .map((icon) => (solidIcons as any)[icon]);
+ faIconLibrary.addIcons(...iconList);
+
+ fixture = TestBed.createComponent(ViewTaxComponentComponent);
+ component = fixture.componentInstance;
+ });
+
+ describe('Component Initialization', () => {
+ it('should create the component', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should load tax component data from route resolver', () => {
+ fixture.detectChanges();
+ expect(component.taxComponentData).toEqual(mockTaxComponentData);
+ });
+
+ it('should subscribe to route data on initialization', () => {
+ expect(component.taxComponentData).toBeDefined();
+ expect(component.taxComponentData.id).toBe(1);
+ expect(component.taxComponentData.name).toBe('VAT');
+ });
+
+ it('should handle tax component data with all properties', () => {
+ fixture.detectChanges();
+ expect(component.taxComponentData.percentage).toBe(15.5);
+ expect(component.taxComponentData.creditAccountId).toBe(101);
+ expect(component.taxComponentData.creditAccountName).toBe('Tax Payable');
+ });
+ });
+
+ describe('Route Data Subscription', () => {
+ it('should update tax component data when route data changes', () => {
+ const updatedTaxData = {
+ id: 2,
+ name: 'GST',
+ percentage: 18.0,
+ startDate: [
+ 2024,
+ 6,
+ 1
+ ],
+ creditAccountId: 102,
+ creditAccountName: 'GST Payable'
+ };
+
+ routeDataSubject.next({
+ taxComponent: updatedTaxData
+ });
+
+ expect(component.taxComponentData).toEqual(updatedTaxData);
+ expect(component.taxComponentData.name).toBe('GST');
+ expect(component.taxComponentData.percentage).toBe(18.0);
+ });
+
+ it('should handle multiple route data updates', () => {
+ const firstUpdate = {
+ id: 2,
+ name: 'Sales Tax',
+ percentage: 10.0
+ };
+
+ const secondUpdate = {
+ id: 3,
+ name: 'Service Tax',
+ percentage: 12.0
+ };
+
+ routeDataSubject.next({ taxComponent: firstUpdate });
+ expect(component.taxComponentData.name).toBe('Sales Tax');
+
+ routeDataSubject.next({ taxComponent: secondUpdate });
+ expect(component.taxComponentData.name).toBe('Service Tax');
+ });
+
+ it('should maintain subscription throughout component lifecycle', () => {
+ fixture.detectChanges();
+ const initialData = component.taxComponentData;
+ expect(initialData).toBeDefined();
+
+ const newData = {
+ id: 99,
+ name: 'Updated Tax',
+ percentage: 20.0
+ };
+
+ routeDataSubject.next({ taxComponent: newData });
+ expect(component.taxComponentData).toEqual(newData);
+ expect(component.taxComponentData).not.toEqual(initialData);
+ });
+ });
+
+ describe('Tax Component Data Properties', () => {
+ it('should correctly handle tax component with id', () => {
+ fixture.detectChanges();
+ expect(component.taxComponentData.id).toBe(1);
+ });
+
+ it('should correctly handle tax component name', () => {
+ fixture.detectChanges();
+ expect(component.taxComponentData.name).toBe('VAT');
+ expect(typeof component.taxComponentData.name).toBe('string');
+ });
+
+ it('should correctly handle tax component percentage', () => {
+ fixture.detectChanges();
+ expect(component.taxComponentData.percentage).toBe(15.5);
+ expect(typeof component.taxComponentData.percentage).toBe('number');
+ });
+
+ it('should correctly handle tax component start date', () => {
+ fixture.detectChanges();
+ expect(component.taxComponentData.startDate).toEqual([
+ 2024,
+ 1,
+ 1
+ ]);
+ expect(Array.isArray(component.taxComponentData.startDate)).toBe(true);
+ });
+
+ it('should correctly handle credit account information', () => {
+ fixture.detectChanges();
+ expect(component.taxComponentData.creditAccountId).toBe(101);
+ expect(component.taxComponentData.creditAccountName).toBe('Tax Payable');
+ });
+
+ it('should correctly handle nested credit account object', () => {
+ fixture.detectChanges();
+ expect(component.taxComponentData.creditAccount).toBeDefined();
+ expect(component.taxComponentData.creditAccount.id).toBe(101);
+ expect(component.taxComponentData.creditAccount.name).toBe('Tax Payable');
+ expect(component.taxComponentData.creditAccount.glCode).toBe('2001');
+ });
+
+ it('should correctly handle credit account type', () => {
+ fixture.detectChanges();
+ expect(component.taxComponentData.creditAccountType).toBeDefined();
+ expect(component.taxComponentData.creditAccountType.code).toBe('LIABILITY');
+ expect(component.taxComponentData.creditAccountType.value).toBe('Liability');
+ });
+ });
+
+ describe('Edge Cases and Error Handling', () => {
+ it('should handle tax component data with minimal properties', () => {
+ const minimalTaxData = {
+ id: 1,
+ name: 'Simple Tax'
+ };
+
+ routeDataSubject.next({ taxComponent: minimalTaxData });
+ expect(component.taxComponentData).toEqual(minimalTaxData);
+ expect(component.taxComponentData.name).toBe('Simple Tax');
+ });
+
+ it('should handle tax component data with zero percentage', () => {
+ const zeroPercentageTax = {
+ id: 1,
+ name: 'Zero Tax',
+ percentage: 0
+ };
+
+ routeDataSubject.next({ taxComponent: zeroPercentageTax });
+ expect(component.taxComponentData.percentage).toBe(0);
+ });
+
+ it('should handle tax component data with high percentage', () => {
+ const highPercentageTax = {
+ id: 1,
+ name: 'High Tax',
+ percentage: 99.99
+ };
+
+ routeDataSubject.next({ taxComponent: highPercentageTax });
+ expect(component.taxComponentData.percentage).toBe(99.99);
+ });
+
+ it('should handle tax component with null credit account', () => {
+ const taxWithNullAccount: any = {
+ id: 1,
+ name: 'Tax Without Account',
+ percentage: 10,
+ creditAccount: null
+ };
+
+ routeDataSubject.next({ taxComponent: taxWithNullAccount });
+ expect(component.taxComponentData.creditAccount).toBeNull();
+ });
+
+ it('should handle tax component with undefined properties', () => {
+ const taxWithUndefined: any = {
+ id: 1,
+ name: 'Tax',
+ percentage: undefined,
+ startDate: undefined
+ };
+
+ routeDataSubject.next({ taxComponent: taxWithUndefined });
+ expect(component.taxComponentData.percentage).toBeUndefined();
+ expect(component.taxComponentData.startDate).toBeUndefined();
+ });
+
+ it('should handle empty tax component data object', () => {
+ const emptyTaxData = {};
+
+ routeDataSubject.next({ taxComponent: emptyTaxData });
+ expect(component.taxComponentData).toEqual(emptyTaxData);
+ });
+
+ it('should handle tax component with different date formats', () => {
+ const taxWithDateString = {
+ id: 1,
+ name: 'Tax',
+ startDate: '2024-01-01'
+ };
+
+ routeDataSubject.next({ taxComponent: taxWithDateString });
+ expect(component.taxComponentData.startDate).toBe('2024-01-01');
+ });
+
+ it('should handle tax component with special characters in name', () => {
+ const taxWithSpecialChars = {
+ id: 1,
+ name: 'Tax & Service @ 15%',
+ percentage: 15
+ };
+
+ routeDataSubject.next({ taxComponent: taxWithSpecialChars });
+ expect(component.taxComponentData.name).toBe('Tax & Service @ 15%');
+ });
+
+ it('should handle tax component with very long name', () => {
+ const longName = 'A'.repeat(200);
+ const taxWithLongName = {
+ id: 1,
+ name: longName,
+ percentage: 10
+ };
+
+ routeDataSubject.next({ taxComponent: taxWithLongName });
+ expect(component.taxComponentData.name).toBe(longName);
+ expect(component.taxComponentData.name.length).toBe(200);
+ });
+
+ it('should handle tax component with negative id', () => {
+ const taxWithNegativeId = {
+ id: -1,
+ name: 'Invalid Tax',
+ percentage: 10
+ };
+
+ routeDataSubject.next({ taxComponent: taxWithNegativeId });
+ expect(component.taxComponentData.id).toBe(-1);
+ });
+ });
+
+ describe('Component Rendering', () => {
+ it('should render component without errors when data is provided', () => {
+ expect(() => {
+ fixture.detectChanges();
+ }).not.toThrow();
+ });
+
+ it('should update view when tax component data changes', () => {
+ fixture.detectChanges();
+ const initialName = component.taxComponentData.name;
+
+ const newTaxData = {
+ id: 2,
+ name: 'Updated Tax Name',
+ percentage: 20
+ };
+
+ routeDataSubject.next({ taxComponent: newTaxData });
+ fixture.detectChanges();
+
+ expect(component.taxComponentData.name).not.toBe(initialName);
+ expect(component.taxComponentData.name).toBe('Updated Tax Name');
+ });
+
+ it('should handle component destruction gracefully', () => {
+ fixture.detectChanges();
+ expect(() => {
+ fixture.destroy();
+ }).not.toThrow();
+ });
+ });
+
+ describe('Integration with ActivatedRoute', () => {
+ it('should correctly inject ActivatedRoute', () => {
+ const activatedRoute = TestBed.inject(ActivatedRoute);
+ expect(activatedRoute).toBeDefined();
+ });
+
+ it('should access route data through subscription', () => {
+ let dataEmitted = false;
+ const activatedRoute = TestBed.inject(ActivatedRoute);
+
+ activatedRoute.data.subscribe((data) => {
+ dataEmitted = true;
+ expect(data.taxComponent).toBeDefined();
+ });
+
+ expect(dataEmitted).toBe(true);
+ });
+
+ it('should handle route snapshot data', () => {
+ const activatedRoute = TestBed.inject(ActivatedRoute);
+ expect(activatedRoute.snapshot.data.taxComponent).toBeDefined();
+ expect(activatedRoute.snapshot.data.taxComponent).toEqual(mockTaxComponentData);
+ });
+ });
+
+ describe('Complete Tax Component View Workflow', () => {
+ it('should complete full view workflow', () => {
+ // 1. Component initializes
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+
+ // 2. Tax component data is loaded
+ expect(component.taxComponentData).toBeDefined();
+ expect(component.taxComponentData.id).toBe(1);
+
+ // 3. Data is correctly populated
+ expect(component.taxComponentData.name).toBe('VAT');
+ expect(component.taxComponentData.percentage).toBe(15.5);
+ expect(component.taxComponentData.creditAccountId).toBe(101);
+
+ // 4. Component can handle data updates
+ const updatedData = {
+ id: 1,
+ name: 'Updated VAT',
+ percentage: 16.0
+ };
+
+ routeDataSubject.next({ taxComponent: updatedData });
+ expect(component.taxComponentData.name).toBe('Updated VAT');
+ expect(component.taxComponentData.percentage).toBe(16.0);
+ });
+
+ it('should maintain data integrity throughout component lifecycle', () => {
+ // Initial state
+ fixture.detectChanges();
+ const initialId = component.taxComponentData.id;
+
+ // Multiple updates
+ for (let i = 1; i <= 5; i++) {
+ const newData = {
+ id: initialId,
+ name: `Tax Update ${i}`,
+ percentage: 10 + i
+ };
+ routeDataSubject.next({ taxComponent: newData });
+ expect(component.taxComponentData.name).toBe(`Tax Update ${i}`);
+ expect(component.taxComponentData.percentage).toBe(10 + i);
+ }
+
+ // Final state verification
+ expect(component.taxComponentData.id).toBe(initialId);
+ });
+ });
+
+ describe('Data Type Validation', () => {
+ it('should handle tax component with all string values', () => {
+ const taxWithStrings = {
+ id: '1',
+ name: 'String Tax',
+ percentage: '15.5',
+ creditAccountId: '101'
+ };
+
+ routeDataSubject.next({ taxComponent: taxWithStrings });
+ expect(component.taxComponentData.id).toBe('1');
+ expect(component.taxComponentData.percentage).toBe('15.5');
+ });
+
+ it('should handle tax component with mixed data types', () => {
+ const taxWithMixedTypes = {
+ id: 1,
+ name: 'Mixed Tax',
+ percentage: '15.5',
+ isActive: true,
+ creditAccountId: 101
+ };
+
+ routeDataSubject.next({ taxComponent: taxWithMixedTypes });
+ expect(component.taxComponentData.isActive).toBe(true);
+ expect(typeof component.taxComponentData.id).toBe('number');
+ });
+
+ it('should handle tax component with array properties', () => {
+ const taxWithArrays = {
+ id: 1,
+ name: 'Array Tax',
+ startDate: [
+ 2024,
+ 1,
+ 1
+ ],
+ applicableRegions: [
+ 'North',
+ 'South',
+ 'East'
+ ]
+ };
+
+ routeDataSubject.next({ taxComponent: taxWithArrays });
+ expect(Array.isArray(component.taxComponentData.startDate)).toBe(true);
+ expect(Array.isArray(component.taxComponentData.applicableRegions)).toBe(true);
+ expect(component.taxComponentData.applicableRegions.length).toBe(3);
+ });
+
+ it('should handle tax component with nested objects', () => {
+ const taxWithNestedObjects = {
+ id: 1,
+ name: 'Nested Tax',
+ creditAccount: {
+ id: 101,
+ name: 'Tax Account',
+ details: {
+ glCode: '2001',
+ type: 'LIABILITY'
+ }
+ }
+ };
+
+ routeDataSubject.next({ taxComponent: taxWithNestedObjects });
+ expect(component.taxComponentData.creditAccount.details).toBeDefined();
+ expect(component.taxComponentData.creditAccount.details.glCode).toBe('2001');
+ });
+ });
+
+ describe('Memory and Performance', () => {
+ it('should not create memory leaks with multiple updates', () => {
+ fixture.detectChanges();
+
+ for (let i = 0; i < 100; i++) {
+ const newData = {
+ id: i,
+ name: `Tax ${i}`,
+ percentage: i * 0.1
+ };
+ routeDataSubject.next({ taxComponent: newData });
+ }
+
+ expect(component.taxComponentData.id).toBe(99);
+ expect(component.taxComponentData.name).toBe('Tax 99');
+ });
+
+ it('should handle rapid consecutive updates', () => {
+ fixture.detectChanges();
+
+ const updates = [
+ { id: 1, name: 'Tax 1' },
+ { id: 2, name: 'Tax 2' },
+ { id: 3, name: 'Tax 3' }
+ ];
+
+ updates.forEach((update) => {
+ routeDataSubject.next({ taxComponent: update });
+ });
+
+ expect(component.taxComponentData.name).toBe('Tax 3');
+ });
+ });
+});
diff --git a/src/app/reports/run-report/chart/chart.component.ts b/src/app/reports/run-report/chart/chart.component.ts
index ae09c89010..1db84a7901 100644
--- a/src/app/reports/run-report/chart/chart.component.ts
+++ b/src/app/reports/run-report/chart/chart.component.ts
@@ -8,11 +8,14 @@ import { ReportsService } from '../../reports.service';
import { ChartData } from '../../common-models/chart-data.model';
/** Charting Imports */
-import Chart from 'chart.js';
+import { Chart, registerables } from 'chart.js';
import { MatButtonToggleGroup, MatButtonToggle } from '@angular/material/button-toggle';
import { NgStyle } from '@angular/common';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
+// Register Chart.js components
+Chart.register(...registerables);
+
/**
* Chart Component
*/
@@ -86,9 +89,11 @@ export class ChartComponent implements OnChanges {
]
},
options: {
- title: {
- display: true,
- text: inputData.keysLabel
+ plugins: {
+ title: {
+ display: true,
+ text: inputData.keysLabel
+ }
}
}
});
@@ -115,19 +120,19 @@ export class ChartComponent implements OnChanges {
]
},
options: {
- legend: { display: false },
+ plugins: {
+ legend: { display: false }
+ },
scales: {
- xAxes: [
- {
- scaleLabel: {
- display: true,
- labelString: inputData.keysLabel
- },
- ticks: {
- beginAtZero: true
- }
+ x: {
+ title: {
+ display: true,
+ text: inputData.keysLabel
}
- ]
+ },
+ y: {
+ min: 0
+ }
}
}
});
diff --git a/src/app/system/audit-trails/view-audit/view-audit.component.ts b/src/app/system/audit-trails/view-audit/view-audit.component.ts
index c303156f47..5e1d75436e 100644
--- a/src/app/system/audit-trails/view-audit/view-audit.component.ts
+++ b/src/app/system/audit-trails/view-audit/view-audit.component.ts
@@ -85,12 +85,25 @@ export class ViewAuditComponent implements OnInit {
* Initalizes Audit Trail Commands Data.
*/
get auditTrailCommandsData() {
- return Object.entries(JSON.parse(this.auditTrailData.commandAsJson)).map(
- ([
- key,
- value
- ]) => ({ command: key, commandValue: value })
- );
+ if (!this.auditTrailData || !this.auditTrailData.commandAsJson) {
+ return [];
+ }
+
+ try {
+ const parsed = JSON.parse(this.auditTrailData.commandAsJson);
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
+ return Object.entries(parsed).map(
+ ([
+ key,
+ value
+ ]) => ({ command: key, commandValue: value })
+ );
+ }
+ return [];
+ } catch (err) {
+ console.error('Invalid commandAsJson in audit trail:', err);
+ return [];
+ }
}
/**
diff --git a/src/app/system/manage-data-tables/edit-data-table/edit-data-table.component.ts b/src/app/system/manage-data-tables/edit-data-table/edit-data-table.component.ts
index 9318b75833..1673d9e8af 100644
--- a/src/app/system/manage-data-tables/edit-data-table/edit-data-table.component.ts
+++ b/src/app/system/manage-data-tables/edit-data-table/edit-data-table.component.ts
@@ -157,11 +157,17 @@ export class EditDataTableComponent implements OnInit {
constructor() {
this.route.data.subscribe((data: { dataTable: any; columnCodes: any }) => {
this.dataTableData = data.dataTable;
+
+ // Get the relationship column name based on application table
+ const relationshipColumnName = this.getRelationshipColumnName(this.dataTableData.applicationTableName);
+
this.dataTableData.columnHeaderData.forEach((item: any) => {
+ // Mark system columns (id, created_at, updated_at) and relationship column as system
item.system = [
- 'created_at',
- 'updated_at'
- ].includes(item.columnName);
+ 'id',
+ 'created_at',
+ 'updated_at'
+ ].includes(item.columnName) || item.columnName === relationshipColumnName;
});
this.columnData = this.dataTableData.columnHeaderData;
this.dataForDialog.columnCodes = data.columnCodes;
@@ -169,7 +175,30 @@ export class EditDataTableComponent implements OnInit {
}
/**
- * Creates and sets data table form and columns table.
+ * Gets the relationship column name.
+ * @param {string} appTableName Application table name.
+ * @returns {string} Relationship column name.
+ */
+ getRelationshipColumnName(appTableName: string): string {
+ // Map application table names to their relationship column names
+ const tableToColumnMap: { [key: string]: string } = {
+ m_client: 'client_id',
+ m_group: 'group_id',
+ m_center: 'center_id',
+ m_office: 'office_id',
+ m_loan: 'loan_id',
+ m_savings_account: 'savings_account_id',
+ m_savings_account_transaction: 'savings_transaction_id',
+ m_product_loan: 'product_loan_id',
+ m_savings_product: 'savings_product_id',
+ m_share_product: 'share_product_id'
+ };
+
+ return tableToColumnMap[appTableName] || '';
+ }
+
+ /**
+ * Create and set data table form and columns table.
*/
ngOnInit() {
this.initData();
@@ -193,7 +222,12 @@ export class EditDataTableComponent implements OnInit {
* Initializes data table changes and column data.
*/
initData() {
- this.columnData.shift();
+ // Remove the 'id' column if it exists (primary key for multi-row datatables)
+ // but keep the relationship column visible (it's already marked as system)
+ if (this.columnData.length > 0 && this.columnData[0].columnName === 'id') {
+ this.columnData.shift();
+ }
+
this.dataTableChangesData.apptableName = this.dataTableData.applicationTableName;
this.dataTableChangesData.entitySubType = this.dataTableData.entitySubType;
for (let index = 0; index < this.columnData.length; index++) {
diff --git a/src/app/system/manage-jobs/scheduler-jobs/view-history-scheduler-job/view-history-scheduler-job.component.ts b/src/app/system/manage-jobs/scheduler-jobs/view-history-scheduler-job/view-history-scheduler-job.component.ts
index 418ae50299..6f0bd57d9a 100644
--- a/src/app/system/manage-jobs/scheduler-jobs/view-history-scheduler-job/view-history-scheduler-job.component.ts
+++ b/src/app/system/manage-jobs/scheduler-jobs/view-history-scheduler-job/view-history-scheduler-job.component.ts
@@ -116,9 +116,9 @@ export class ViewHistorySchedulerJobComponent implements OnInit {
const filters = JSON.parse(filtersJson);
filters.forEach((filter: any) => {
const val = data[filter.id] === null ? '' : data[filter.id];
- if (filter.value !== '') {
- matchFilter.push(val === parseInt(filter.value, 10));
- } else if (filter.value === '') {
+ if (filter.value !== '' && val !== '') {
+ matchFilter.push(parseInt(val.toString(), 10) === parseInt(filter.value, 10));
+ } else if (filter.value === '' || val === '') {
matchFilter.push(val.toString().toLowerCase().includes(filter.value.toLowerCase()));
}
});
diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.ts
index f1b006f878..41ac2cc114 100644
--- a/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.ts
+++ b/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.ts
@@ -175,7 +175,7 @@ export class ClientApprovalComponent {
});
this.tasksService.submitBatchData(this.batchRequests).subscribe((response: any) => {
response.forEach((responseEle: any) => {
- if ((responseEle.statusCode = '200')) {
+ if (responseEle.statusCode === '200') {
activatedAccounts++;
responseEle.body = JSON.parse(responseEle.body);
if (selectedAccounts === activatedAccounts) {
diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.ts
index c4f6ca2d83..b3cc62ec08 100644
--- a/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.ts
+++ b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.ts
@@ -205,7 +205,7 @@ export class LoanApprovalComponent {
});
this.tasksService.submitBatchData(this.batchRequests).subscribe((response: any) => {
response.forEach((responseEle: any) => {
- if ((responseEle.statusCode = '200')) {
+ if (responseEle.statusCode === '200') {
approvedAccounts++;
responseEle.body = JSON.parse(responseEle.body);
if (selectedAccounts === approvedAccounts) {
diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.ts
index 28c84e5862..03b8b3f24c 100644
--- a/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.ts
+++ b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.ts
@@ -157,7 +157,7 @@ export class LoanDisbursalComponent {
});
this.tasksService.submitBatchData(this.batchRequests).subscribe((response: any) => {
response.forEach((responseEle: any) => {
- if ((responseEle.statusCode = '200')) {
+ if (responseEle.statusCode === '200') {
approvedAccounts++;
responseEle.body = JSON.parse(responseEle.body);
if (selectedAccounts === approvedAccounts) {
diff --git a/src/app/tasks/tasks-routing.module.ts b/src/app/tasks/tasks-routing.module.ts
index 6475d4c7a2..977f427958 100644
--- a/src/app/tasks/tasks-routing.module.ts
+++ b/src/app/tasks/tasks-routing.module.ts
@@ -71,7 +71,7 @@ const routes: Routes = [
component: RescheduleLoanComponent,
data: { title: 'Reschedule Loan' },
resolve: {
- recheduleLoansData: GetRescheduleLoans
+ rescheduleLoansData: GetRescheduleLoans
}
}
]
diff --git a/src/app/web-app.component.ts b/src/app/web-app.component.ts
index f64a2adb5c..894ae2d3dc 100644
--- a/src/app/web-app.component.ts
+++ b/src/app/web-app.component.ts
@@ -6,8 +6,8 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { MatDialog } from '@angular/material/dialog';
/** rxjs Imports */
-import { merge, Subscription } from 'rxjs';
-import { filter, map, mergeMap } from 'rxjs/operators';
+import { merge, Subscription, Subject } from 'rxjs';
+import { filter, map, mergeMap, takeUntil, take } from 'rxjs/operators';
/** Translation Imports */
import { TranslateService } from '@ngx-translate/core';
@@ -108,6 +108,7 @@ export class WebAppComponent implements OnInit, OnDestroy {
i18nService = inject(I18nService);
private authSubscription: Subscription;
+ private destroy$ = new Subject();
/** Inserted by Angular inject() migration for backwards compatibility */
constructor(...args: unknown[]);
@@ -179,13 +180,17 @@ export class WebAppComponent implements OnInit, OnDestroy {
return route;
}),
filter((route) => route.outlet === 'primary'),
- mergeMap((route) => route.data)
+ mergeMap((route) => route.data),
+ takeUntil(this.destroy$)
)
.subscribe((event) => {
const title = event['title'] ? `labels.text.${event['title']}` : 'APP_NAME';
- this.i18nService.translate(title).subscribe((titleTranslated: any) => {
- this.titleService.setTitle(titleTranslated);
- });
+ this.i18nService
+ .translate(title)
+ .pipe(take(1))
+ .subscribe((titleTranslated: any) => {
+ this.titleService.setTitle(titleTranslated);
+ });
});
// Stores top 100 user activites as local storage object.
@@ -196,7 +201,7 @@ export class WebAppComponent implements OnInit, OnDestroy {
activities = length > 100 ? activitiesArray.slice(length - 100) : activitiesArray;
}
// Store route URLs array in local storage on navigation end.
- onNavigationEnd.subscribe(() => {
+ onNavigationEnd.pipe(takeUntil(this.destroy$)).subscribe(() => {
activities.push(this.router.url);
localStorage.setItem('mifosXLocation', JSON.stringify(activities));
});
@@ -258,6 +263,8 @@ export class WebAppComponent implements OnInit, OnDestroy {
}
ngOnDestroy() {
+ this.destroy$.next();
+ this.destroy$.complete();
if (this.authSubscription) {
this.authSubscription.unsubscribe();
}
diff --git a/src/app/zitadel/auth.service.ts b/src/app/zitadel/auth.service.ts
index cc7d8dc3ba..6ea0a7f448 100644
--- a/src/app/zitadel/auth.service.ts
+++ b/src/app/zitadel/auth.service.ts
@@ -36,7 +36,7 @@ export class AuthService {
async login() {
const codeVerifier = this.generateRandomString();
const codeChallenge = await this.generateCodeChallenge(codeVerifier);
- localStorage.setItem('code_verifier', codeVerifier);
+ sessionStorage.setItem('code_verifier', codeVerifier);
const url =
`${this.authUrl}` +
`?client_id=${encodeURIComponent(this.clientId)}` +
@@ -108,56 +108,51 @@ export class AuthService {
return base64;
}
- exchangeCodeForTokens(code: string, codeVerifier: string | null) {
+ async exchangeCodeForTokens(code: string, codeVerifier: string | null): Promise {
const payload = {
code: code,
code_verifier: codeVerifier || ''
};
- fetch(this.api + 'authentication/token', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(payload)
- })
- .then((res) => {
- if (!res.ok) {
- throw new Error(`Error exchanging code: ${res.status} ${res.statusText}`);
- }
- return res.json();
- })
- .then(
- (tokens: {
- access_token: string;
- id_token: string;
- refresh_token: string;
- expires_in: number;
- token_type: string;
- }) => {
- const token: OAuth2Token = {
- access_token: tokens.access_token,
- token_type: tokens.token_type,
- refresh_token: tokens.refresh_token,
- expires_in: tokens.expires_in,
- scope: 'Bearer'
- };
-
- localStorage.setItem('id_token', tokens.id_token);
- localStorage.setItem('mifosXZitadel', 'true');
- sessionStorage.setItem('mifosXZitadelTokenDetails', JSON.stringify(token));
- localStorage.setItem('refresh_token', tokens.refresh_token);
- this.scheduleRefresh(tokens.expires_in);
- localStorage.removeItem('auth_code');
- localStorage.removeItem('code_verifier');
- this.userdetails();
- }
- )
- .catch((error) => {
- localStorage.removeItem('auth_code');
- localStorage.removeItem('code_verifier');
- window.location.href = '/#/login';
+ try {
+ const response = await fetch(this.api + 'authentication/token', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(payload)
});
+
+ if (!response.ok) {
+ throw new Error(`Error exchanging code: ${response.status} ${response.statusText}`);
+ }
+
+ const tokens: {
+ access_token: string;
+ id_token: string;
+ refresh_token: string;
+ expires_in: number;
+ token_type: string;
+ } = await response.json();
+
+ const token: OAuth2Token = {
+ access_token: tokens.access_token,
+ token_type: tokens.token_type,
+ refresh_token: tokens.refresh_token,
+ expires_in: tokens.expires_in,
+ scope: 'Bearer'
+ };
+
+ sessionStorage.setItem('mifosXZitadelTokenDetails', JSON.stringify(token));
+ localStorage.setItem('id_token', tokens.id_token);
+ localStorage.setItem('refresh_token', tokens.refresh_token);
+ localStorage.setItem('mifosXZitadel', 'true');
+ this.scheduleRefresh(tokens.expires_in);
+ await this.userdetails();
+ } catch (error) {
+ window.location.href = '/#/login';
+ throw error;
+ }
}
userdetails() {
diff --git a/src/assets/translations/cs-CS.json b/src/assets/translations/cs-CS.json
index 6d5971c9a9..de029aa21d 100644
--- a/src/assets/translations/cs-CS.json
+++ b/src/assets/translations/cs-CS.json
@@ -425,6 +425,7 @@
"GSIM Application": "Aplikace GSIM",
"Generate Report": "Vygenerovat zprávu",
"Get Parameters": "Získat parametry",
+ "Go back": "Vrátit se",
"Go to next step": "Přejděte k dalšímu kroku",
"Group Loan Application": "Žádost o skupinovou půjčku",
"Group Saving Application": "Aplikace pro ukládání skupin",
@@ -459,6 +460,7 @@
"Pie Chart": "Koláčový graf",
"Post Dividend": "Vyplácet dividendu",
"Previous": "Předchozí",
+ "Preview": "Náhled",
"Print": "Tisk",
"Proceed": "Pokračovat",
"Productive Collection Sheet": "Produktivní kolekce list",
@@ -1069,6 +1071,7 @@
"Reject Checker": "Odmítnout kontrolu",
"Release Amount": "Částka uvolnění",
"Repaid Every": "Splaceno každý",
+ "Repayment Schedule Preview": "Náhled splátkového kalendáře",
"Report Parameter": "Parametr sestavy",
"Reschedule Loan": "Přeplánovací půjčka",
"Revert Transaction": "Vrátit transakci",
@@ -3043,6 +3046,7 @@
"Loan Products": "Úvěrové produkty",
"Loan Provisioning Criteria Organization": "Definujte kritéria pro poskytování úvěrů pro organizaci",
"Loan Tranche Details": "Podrobnosti o tranši úvěru",
+ "Loan Term Variations": "Variace podmínek úvěru",
"Loan View": "Pohled na půjčku",
"Loan products define the rules, default settings": "Úvěrové produkty definují pravidla, výchozí nastavení a omezení pro nabídky půjček finanční instituce. Úvěrový produkt poskytuje pro klienty finanční instituce šablonu pro více úvěrových účtů.",
"Loan": "Půjčka",
diff --git a/src/assets/translations/de-DE.json b/src/assets/translations/de-DE.json
index d912c7336b..3b6068a50e 100644
--- a/src/assets/translations/de-DE.json
+++ b/src/assets/translations/de-DE.json
@@ -425,6 +425,7 @@
"GSIM Application": "GSIM-Anwendung",
"Generate Report": "Bericht generieren",
"Get Parameters": "Parameter abrufen",
+ "Go back": "Zurück",
"Go to next step": "Gehen Sie zum nächsten Schritt",
"Group Loan Application": "Gruppenkreditantrag",
"Group Saving Application": "Gruppenspeicheranwendung",
@@ -459,6 +460,7 @@
"Pie Chart": "Kuchendiagramm",
"Post Dividend": "Post-Dividende",
"Previous": "Vorherige",
+ "Preview": "Vorschau",
"Print": "Drucken",
"Proceed": "Fortfahren",
"Productive Collection Sheet": "Produktives Sammlungsblatt",
@@ -1070,6 +1072,7 @@
"Reject Checker": "Überprüfung ablehnen",
"Release Amount": "Freigabebetrag",
"Repaid Every": "Alle zurückgezahlt",
+ "Repayment Schedule Preview": "Rückzahlungsplan Vorschau",
"Report Parameter": "Berichtsparameter",
"Reschedule Loan": "Darlehen neu plant",
"Revert Transaction": "Transaktion rückgängig machen",
@@ -3042,6 +3045,7 @@
"Loan Products": "Kreditprodukte",
"Loan Provisioning Criteria Organization": "Definieren Sie Kreditbereitstellungskriterien für die Organisation",
"Loan Tranche Details": "Details zur Darlehenstranche",
+ "Loan Term Variations": "Begriffsvariationen des Darlehens",
"Loan View": "Kreditansicht",
"Loan products define the rules, default settings": "Kreditprodukte definieren die Regeln, Standardeinstellungen und Einschränkungen für die Kreditangebote eines Finanzinstituts. Ein Kreditprodukt bietet eine Vorlage für mehrere Kreditkonten für die Kunden des Finanzinstituts.",
"Loan": "Darlehen",
diff --git a/src/assets/translations/en-US.json b/src/assets/translations/en-US.json
index 5ee1253c18..e7d7183b78 100644
--- a/src/assets/translations/en-US.json
+++ b/src/assets/translations/en-US.json
@@ -426,6 +426,7 @@
"GSIM Application": "GSIM Application",
"Generate Report": "Generate Report",
"Get Parameters": "Get Parameters",
+ "Go back": "Go back",
"Go to next step": "Go to next step",
"Group Loan Application": "Group Loan Application",
"Group Saving Application": "Group Saving Application",
@@ -460,6 +461,7 @@
"Pie Chart": "Pie Chart",
"Post Dividend": "Post Dividend",
"Previous": "Previous",
+ "Preview": "Preview",
"Print": "Print",
"Proceed": "Proceed",
"Productive Collection Sheet": "Productive Collection Sheet",
@@ -1075,6 +1077,7 @@
"Reject Checker": "Reject Checker",
"Release Amount": "Release Amount",
"Repaid Every": "Repaid Every",
+ "Repayment Schedule Preview": "Repayment Schedule Preview",
"Report Parameter": "Report Parameter",
"Reschedule Loan": "Reschedule Loan",
"Revert Transaction": "Revert Transaction",
@@ -3136,6 +3139,7 @@
"Loan Products": "Loan Products",
"Loan Provisioning Criteria Organization": "Define Loan Provisioning Criteria for Organization",
"Loan Tranche Details": "Loan Tranche Details",
+ "Loan Term Variations": "Loan Term Variations",
"Loan View": "Loan View",
"Loan products define the rules, default settings": "Loan products define the rules, default settings, and constraints for a financial institution's lending offerings. A loan product provides a template for multiple loan accounts for the financial institution's clients.",
"Loan": "Loan",
diff --git a/src/assets/translations/es-CL.json b/src/assets/translations/es-CL.json
index 2d2050cd52..0fe05fd0be 100644
--- a/src/assets/translations/es-CL.json
+++ b/src/assets/translations/es-CL.json
@@ -425,6 +425,7 @@
"GSIM Application": "Aplicación GSIM",
"Generate Report": "Generar reporte",
"Get Parameters": "Obtener parámetros",
+ "Go back": "Volver",
"Go to next step": "Ir al siguiente paso",
"Group Loan Application": "Solicitud de Crédito grupal",
"Group Saving Application": "Solicitud de ahorro grupal",
@@ -459,6 +460,7 @@
"Pie Chart": "Gráfico circular",
"Post Dividend": "Publicar dividendo",
"Previous": "Anterior",
+ "Preview": "Vista previa",
"Print": "Imprimir",
"Proceed": "Proceder",
"Productive Collection Sheet": "Hoja de Recolección Productiva",
@@ -926,8 +928,8 @@
"Expenses": "Gastos",
"External Asset Owner": "Propietario de activos externos",
"External Services": "Servicios externos",
- "Family Member": "Miembro de la familia",
- "Family Members": "Miembros de la familia",
+ "Family Member": "Referencia personal",
+ "Family Members": "Referencias personales",
"Fees to Specific Income Accounts": "Tarifas a cuentas de ingresos específicas",
"Filter holidays": "Barra de búsqueda para filtrar días festivos según diferentes oficinas.",
"Filter reports by name": "Barra de búsqueda para filtrar reportes por nombre.",
@@ -1069,6 +1071,7 @@
"Reject Checker": "Rechazador",
"Release Amount": "Liberar Monto",
"Repaid Every": "Frecuencia de Pago",
+ "Repayment Schedule Preview": "Vista previa del calendario de pagos",
"Report Parameter": "Parámetro de reporte",
"Reschedule Loan": "Reprogramación del Crédito",
"Revert Transaction": "Revertir transacción",
@@ -2703,7 +2706,7 @@
"the Transaction Type": "el tipo de transacción"
},
"text": {
- "A": "A",
+ "A": "Una",
"Ability to manage holidays for individual offices": "La capacidad de gestionar días festivos para oficinas individuales es una herramienta muy útil para una organización que abarca varias ubicaciones. Utilice esta opción para personalizar los días festivos para cada oficina de su organización.",
"Account Detail": "Detalle de cuenta",
"Account Number Preferences": "Preferencias de número de cuenta",
@@ -3004,7 +3007,7 @@
"GLAccount Balances Application": "Esto facilita que una organización que desee migrar a Mifos transfiera saldos de cuentas del libro mayor (desde su aplicación de contabilidad existente o sistema manual) a Mifos X Accounting.",
"GSIM Account View": "Vista de cuenta GSIM",
"General": "General",
- "Get involved": "Involucrarse",
+ "Get involved": "¡Te invitamos a involucrarte",
"Global configurations, Cache and Business Date": "Configuraciones globales, configuración de caché y fecha del sistema",
"Group": "Grupo",
"Group Actions": "Acciones grupales",
@@ -3043,6 +3046,7 @@
"Loan Products": "Productos de Crédito",
"Loan Provisioning Criteria Organization": "Definir criterios de concesión de Créditos para la organización",
"Loan Tranche Details": "Detalles de la Dispersión de Crédito",
+ "Loan Term Variations": "Variaciones de Términos del Crédito",
"Loan View": "Vista de Crédito",
"Loan products define the rules, default settings": "Los productos crediticios definen las reglas, la configuración predeterminada y las restricciones para las ofertas de Créditos de una institución financiera. Un producto de Crédito proporciona una plantilla para múltiples cuentas de Crédito para los clientes de la institución financiera.",
"Loan": "Crédito",
@@ -3321,9 +3325,9 @@
"by": "por",
"do not match": "no coinciden",
"edit": "editar",
- "elimination of poverty": "que tiene como objetivo acelerar la eliminación de la pobreza al permitir que las organizaciones brinden de manera más efectiva y eficiente servicios financieros responsables a los pobres y no bancarizados del mundo. ¿Suena interesante?",
+ "elimination of poverty": "que tiene como objetivo acelerar la eliminación de la pobreza al permitir que las organizaciones brinden de manera más efectiva y eficiente servicios financieros responsables a las personas de escasos recursos y no bancarizados alrededor del mundo. ¿Suena interesante?",
"global community": "Comunidad global",
- "is designed by the": "está diseñado por el",
+ "is designed by the": "está diseñado por la",
"per annum": "anualmente",
"undefined": "No definido",
"username": "nombre de usuario",
diff --git a/src/assets/translations/es-MX.json b/src/assets/translations/es-MX.json
index aee096f3a8..39552ee3ba 100644
--- a/src/assets/translations/es-MX.json
+++ b/src/assets/translations/es-MX.json
@@ -425,6 +425,7 @@
"GSIM Application": "Aplicación GSIM",
"Generate Report": "Generar reporte",
"Get Parameters": "Obtener parámetros",
+ "Go back": "Volver",
"Go to next step": "Ir al siguiente paso",
"Group Loan Application": "Solicitud de Crédito grupal",
"Group Saving Application": "Solicitud de ahorro grupal",
@@ -459,6 +460,7 @@
"Pie Chart": "Gráfico circular",
"Post Dividend": "Publicar dividendo",
"Previous": "Anterior",
+ "Preview": "Vista previa",
"Print": "Imprimir",
"Proceed": "Proceder",
"Productive Collection Sheet": "Hoja de Recolección Productiva",
@@ -1069,6 +1071,7 @@
"Reject Checker": "Rechazador",
"Release Amount": "Liberar Monto",
"Repaid Every": "Frecuencia de Pago",
+ "Repayment Schedule Preview": "Vista previa del calendario de pagos",
"Report Parameter": "Parámetro de reporte",
"Reschedule Loan": "Reprogramación del Crédito",
"Revert Transaction": "Revertir transacción",
@@ -1625,7 +1628,8 @@
"FAMILY MEMBERS": "REFERENCIAS PERSONALES Y FAMILIARES",
"FCM End Point": "Punto final FCM",
"Failure Count": "Número de Fallos",
- "Family Members": "Miembros de la familia",
+ "Family Member": "Referencia personal",
+ "Family Members": "Referencias personales",
"Favicon": "favicon",
"Fee": "Tarifa",
"Fee Amount": "Importe de la cuota",
@@ -2704,7 +2708,7 @@
"the Transaction Type": "el tipo de transacción"
},
"text": {
- "A": "A",
+ "A": "Una",
"Ability to manage holidays for individual offices": "La capacidad de gestionar días festivos para oficinas individuales es una herramienta muy útil para una organización que abarca varias ubicaciones. Utilice esta opción para personalizar los días festivos para cada oficina de su organización.",
"Account Detail": "Detalle de cuenta",
"Account Number Preferences": "Preferencias de número de cuenta",
@@ -3005,7 +3009,7 @@
"GLAccount Balances Application": "Esto facilita que una organización que desee migrar a Mifos transfiera saldos de cuentas del libro mayor (desde su aplicación de contabilidad existente o sistema manual) a Mifos X Accounting.",
"GSIM Account View": "Vista de cuenta GSIM",
"General": "General",
- "Get involved": "Involucrarse",
+ "Get involved": "¡Te invitamos a involucrarte",
"Global configurations, Cache and Business Date": "Configuraciones globales, configuración de caché y fecha del sistema",
"Group": "Grupo",
"Group Actions": "Acciones grupales",
@@ -3044,6 +3048,7 @@
"Loan Products": "Productos de Crédito",
"Loan Provisioning Criteria Organization": "Definir criterios de concesión de Créditos para la organización",
"Loan Tranche Details": "Detalles de la Dispersión de Crédito",
+ "Loan Term Variations": "Variaciones de Términos del Crédito",
"Loan View": "Vista de Crédito",
"Loan products define the rules, default settings": "Los productos crediticios definen las reglas, la configuración predeterminada y las restricciones para las ofertas de Créditos de una institución financiera. Un producto de Crédito proporciona una plantilla para múltiples cuentas de Crédito para los clientes de la institución financiera.",
"Loan": "Crédito",
@@ -3322,9 +3327,9 @@
"by": "por",
"do not match": "no coinciden",
"edit": "editar",
- "elimination of poverty": "que tiene como objetivo acelerar la eliminación de la pobreza al permitir que las organizaciones brinden de manera más efectiva y eficiente servicios financieros responsables a los pobres y no bancarizados del mundo. ¿Suena interesante?",
+ "elimination of poverty": "que tiene como objetivo acelerar la eliminación de la pobreza al permitir que las organizaciones brinden de manera más efectiva y eficiente servicios financieros responsables a las personas de escasos recursos y no bancarizados alrededor del mundo. ¿Suena interesante?",
"global community": "Comunidad global",
- "is designed by the": "está diseñado por el",
+ "is designed by the": "está diseñado por la",
"per annum": "anualmente",
"undefined": "No definido",
"username": "nombre de usuario",
diff --git a/src/assets/translations/fr-FR.json b/src/assets/translations/fr-FR.json
index d8ae26659b..50f8fe4182 100644
--- a/src/assets/translations/fr-FR.json
+++ b/src/assets/translations/fr-FR.json
@@ -425,6 +425,7 @@
"GSIM Application": "Demande GSIM",
"Generate Report": "Générer un rapport",
"Get Parameters": "Obtenir les paramètres",
+ "Go back": "Retour",
"Go to next step": "Passer à l'étape suivante",
"Group Loan Application": "Demande de prêt de groupe",
"Group Saving Application": "Demande d'épargne de groupe",
@@ -459,6 +460,7 @@
"Pie Chart": "Diagramme circulaire",
"Post Dividend": "Après dividende",
"Previous": "Précédent",
+ "Preview": "Aperçu",
"Print": "Imprimer",
"Proceed": "Procéder",
"Productive Collection Sheet": "Fiche de collecte productive",
@@ -1070,6 +1072,7 @@
"Reject Checker": "Rejeter le vérificateur",
"Release Amount": "Montant de la libération",
"Repaid Every": "Remboursé tous les",
+ "Repayment Schedule Preview": "Aperçu du calendrier de remboursement",
"Report Parameter": "Paramètre de rapport",
"Reschedule Loan": "Prêt de reproduction",
"Revert Transaction": "Annuler la transaction",
@@ -3042,6 +3045,7 @@
"Loan Products": "Produits de prêt",
"Loan Provisioning Criteria Organization": "Définir les critères de provisionnement des prêts pour l'organisation",
"Loan Tranche Details": "Détails de la tranche de prêt",
+ "Loan Term Variations": "Variations de Durée du Prêt",
"Loan View": "Vue du prêt",
"Loan products define the rules, default settings": "Les produits de prêt définissent les règles, les paramètres par défaut et les contraintes des offres de prêt d'une institution financière. Un produit de prêt fournit un modèle pour plusieurs comptes de prêt pour les clients de l'institution financière.",
"Loan": "Prêt",
diff --git a/src/assets/translations/it-IT.json b/src/assets/translations/it-IT.json
index d53c3be24e..cc0c4345ad 100644
--- a/src/assets/translations/it-IT.json
+++ b/src/assets/translations/it-IT.json
@@ -425,6 +425,7 @@
"GSIM Application": "Applicazione GSMIM",
"Generate Report": "Genera rapporto",
"Get Parameters": "Ottieni parametri",
+ "Go back": "Indietro",
"Go to next step": "Vai al passaggio successivo",
"Group Loan Application": "Richiesta di prestito di gruppo",
"Group Saving Application": "Applicazione di salvataggio di gruppo",
@@ -459,6 +460,7 @@
"Pie Chart": "Grafico a torta",
"Post Dividend": "Post dividendo",
"Previous": "Precedente",
+ "Preview": "Anteprima",
"Print": "Stampa",
"Proceed": "Procedere",
"Productive Collection Sheet": "Foglio di raccolta produttiva",
@@ -1070,6 +1072,7 @@
"Reject Checker": "Rifiutare il controllo",
"Release Amount": "Importo di rilascio",
"Repaid Every": "Rimborsato ogni",
+ "Repayment Schedule Preview": "Anteprima piano di rimborso",
"Report Parameter": "Parametro del report",
"Reschedule Loan": "Prestito riprogrammato",
"Revert Transaction": "Annulla transazione",
@@ -3043,6 +3046,7 @@
"Loan Products": "Prodotti di prestito",
"Loan Provisioning Criteria Organization": "Definire i criteri di erogazione del prestito per l'organizzazione",
"Loan Tranche Details": "Dettagli della tranche del prestito",
+ "Loan Term Variations": "Variazioni dei Termini del Prestito",
"Loan View": "Visualizzazione prestito",
"Loan products define the rules, default settings": "I prodotti di prestito definiscono le regole, le impostazioni predefinite e i vincoli per le offerte di prestito di un istituto finanziario. Un prodotto di prestito fornisce un modello per più conti di prestito per i clienti dell'istituto finanziario.",
"Loan": "Prestito",
diff --git a/src/assets/translations/ko-KO.json b/src/assets/translations/ko-KO.json
index ec0b5d8964..f605bdb68e 100644
--- a/src/assets/translations/ko-KO.json
+++ b/src/assets/translations/ko-KO.json
@@ -425,6 +425,7 @@
"GSIM Application": "GSIM 애플리케이션",
"Generate Report": "보고서 생성",
"Get Parameters": "매개변수 가져오기",
+ "Go back": "돌아가기",
"Go to next step": "다음 단계로 이동",
"Group Loan Application": "단체대출 신청",
"Group Saving Application": "그룹저장 신청",
@@ -459,6 +460,7 @@
"Pie Chart": "파이 차트",
"Post Dividend": "배당 후",
"Previous": "이전의",
+ "Preview": "미리보기",
"Print": "인쇄",
"Proceed": "진행하다",
"Productive Collection Sheet": "생산적인 컬렉션 시트",
@@ -1071,6 +1073,7 @@
"Reject Checker": "검사기를 거부하십시오",
"Release Amount": "출시 금액",
"Repaid Every": "마다 상환",
+ "Repayment Schedule Preview": "상환 일정 미리보기",
"Report Parameter": "보고서 매개변수",
"Reschedule Loan": "일정 조정 대출",
"Revert Transaction": "거래 되돌리기",
@@ -3043,6 +3046,7 @@
"Loan Products": "대출상품",
"Loan Provisioning Criteria Organization": "조직에 대한 대출 준비 기준 정의",
"Loan Tranche Details": "대출 트랜치 세부정보",
+ "Loan Term Variations": "대출 용어 변형",
"Loan View": "대출보기",
"Loan products define the rules, default settings": "대출 상품은 금융 기관의 대출 상품에 대한 규칙, 기본 설정 및 제약 조건을 정의합니다. 대출 상품은 금융 기관 고객을 위한 여러 대출 계정에 대한 템플릿을 제공합니다.",
"Loan": "대출",
diff --git a/src/assets/translations/lt-LT.json b/src/assets/translations/lt-LT.json
index 7ba48e34cf..60b8764268 100644
--- a/src/assets/translations/lt-LT.json
+++ b/src/assets/translations/lt-LT.json
@@ -425,6 +425,7 @@
"GSIM Application": "GSIM programa",
"Generate Report": "Sukurti ataskaitą",
"Get Parameters": "Gaukite parametrus",
+ "Go back": "Grįžti",
"Go to next step": "Eikite į kitą veiksmą",
"Group Loan Application": "Grupinės paskolos paraiška",
"Group Saving Application": "Grupės išsaugojimo programa",
@@ -459,6 +460,7 @@
"Pie Chart": "Skritulinė diagrama",
"Post Dividend": "Paskelbkite dividendus",
"Previous": "Ankstesnis",
+ "Preview": "Peržiūra",
"Print": "Spausdinti",
"Proceed": "Tęskite",
"Productive Collection Sheet": "Produktyvus surinkimo lapas",
@@ -1070,6 +1072,7 @@
"Reject Checker": "Atmeskite tikrintuvą",
"Release Amount": "Išleidimo suma",
"Repaid Every": "Atlyginama kas",
+ "Repayment Schedule Preview": "Grąžinimo grafiko peržiūra",
"Report Parameter": "Ataskaitos parametras",
"Reschedule Loan": "Paskelbta paskola",
"Revert Transaction": "Grąžinti operaciją",
@@ -3043,6 +3046,7 @@
"Loan Products": "Paskolos produktai",
"Loan Provisioning Criteria Organization": "Apibrėžkite organizacijos paskolos suteikimo kriterijus",
"Loan Tranche Details": "Išsami informacija apie paskolos dalį",
+ "Loan Term Variations": "Paskolos terminų variacijos",
"Loan View": "Paskolos vaizdas",
"Loan products define the rules, default settings": "Paskolų produktai apibrėžia finansų įstaigos skolinimo pasiūlymų taisykles, numatytuosius nustatymus ir apribojimus. Paskolos produktas suteikia šabloną kelioms paskolos paskyroms finansų įstaigos klientams.",
"Loan": "Paskola",
diff --git a/src/assets/translations/lv-LV.json b/src/assets/translations/lv-LV.json
index 6e960a16e2..7a942f9931 100644
--- a/src/assets/translations/lv-LV.json
+++ b/src/assets/translations/lv-LV.json
@@ -425,6 +425,7 @@
"GSIM Application": "GSIM lietojumprogramma",
"Generate Report": "Ģenerēt atskaiti",
"Get Parameters": "Iegūstiet parametrus",
+ "Go back": "Atpakaļ",
"Go to next step": "Pārejiet uz nākamo soli",
"Group Loan Application": "Grupas aizdevuma pieteikums",
"Group Saving Application": "Grupas saglabāšanas lietojumprogramma",
@@ -459,6 +460,7 @@
"Pie Chart": "Sektoru diagramma",
"Post Dividend": "Pēc dividendes",
"Previous": "Iepriekšējais",
+ "Preview": "Priekšskatījums",
"Print": "Drukāt",
"Proceed": "Turpināt",
"Productive Collection Sheet": "Produktīva kolekcijas lapa",
@@ -1070,6 +1072,7 @@
"Reject Checker": "Noraidīt pārbaudītāju",
"Release Amount": "Izlaiduma summa",
"Repaid Every": "Atmaksāts Katrs",
+ "Repayment Schedule Preview": "Atmaksas grafika priekšskatījums",
"Report Parameter": "Pārskata parametrs",
"Reschedule Loan": "Pārplānot aizdevumu",
"Revert Transaction": "Atsaukt darījumu",
@@ -3043,6 +3046,7 @@
"Loan Products": "Aizdevuma produkti",
"Loan Provisioning Criteria Organization": "Definējiet organizācijas aizdevuma piešķiršanas kritērijus",
"Loan Tranche Details": "Sīkāka informācija par aizdevuma daļu",
+ "Loan Term Variations": "Aizdevuma terminu variācijas",
"Loan View": "Aizdevuma skats",
"Loan products define the rules, default settings": "Aizdevuma produkti nosaka noteikumus, noklusējuma iestatījumus un ierobežojumus finanšu iestādes aizdevumu piedāvājumiem. Aizdevuma produkts nodrošina veidni vairākiem aizdevuma kontiem finanšu iestādes klientiem.",
"Loan": "Aizdevums",
diff --git a/src/assets/translations/ne-NE.json b/src/assets/translations/ne-NE.json
index 3ba1e55f0c..4d88bba621 100644
--- a/src/assets/translations/ne-NE.json
+++ b/src/assets/translations/ne-NE.json
@@ -425,6 +425,7 @@
"GSIM Application": "GSIM आवेदन",
"Generate Report": "रिपोर्ट उत्पन्न गर्नुहोस्",
"Get Parameters": "प्यारामिटरहरू प्राप्त गर्नुहोस्",
+ "Go back": "फिर्ता जानुहोस्",
"Go to next step": "अर्को चरणमा जानुहोस्",
"Group Loan Application": "समूह ऋण आवेदन",
"Group Saving Application": "समूह बचत आवेदन",
@@ -459,6 +460,7 @@
"Pie Chart": "पाई चार्ट",
"Post Dividend": "लाभांश पोस्ट गर्नुहोस्",
"Previous": "अघिल्लो",
+ "Preview": "पूर्वावलोकन",
"Print": "छाप्नुहोस्",
"Proceed": "अगाडि बढ्नुहोस्",
"Productive Collection Sheet": "उत्पादक संग्रह पाना",
@@ -1070,6 +1072,7 @@
"Reject Checker": "परीक्षकलाई अस्वीकार गर्नुहोस्",
"Release Amount": "रिलिज रकम",
"Repaid Every": "प्रत्येक भुक्तान गरियो",
+ "Repayment Schedule Preview": "भुक्तानी तालिका पूर्वावलोकन",
"Report Parameter": "रिपोर्ट प्यारामिटर",
"Reschedule Loan": "Receplate ण लिएको",
"Revert Transaction": "लेनदेन उल्टाउनुहोस्",
@@ -3043,6 +3046,7 @@
"Loan Products": "ऋण उत्पादनहरू",
"Loan Provisioning Criteria Organization": "संस्थाको लागि ऋण प्रावधान मापदण्ड परिभाषित गर्नुहोस्",
"Loan Tranche Details": "ऋण किस्ता विवरण",
+ "Loan Term Variations": "ऋण अवधि भिन्नताहरू",
"Loan View": "ऋण दृश्य",
"Loan products define the rules, default settings": "ऋण उत्पादनहरूले वित्तीय संस्थाको ऋण प्रस्तावहरूको लागि नियमहरू, पूर्वनिर्धारित सेटिङहरू, र अवरोधहरू परिभाषित गर्दछ। ऋण उत्पादनले वित्तीय संस्थाका ग्राहकहरूको लागि बहु ऋण खाताहरूको लागि टेम्प्लेट प्रदान गर्दछ।",
"Loan": "ऋण",
diff --git a/src/assets/translations/pt-PT.json b/src/assets/translations/pt-PT.json
index 84dd4e8c84..6cc8b7fa5e 100644
--- a/src/assets/translations/pt-PT.json
+++ b/src/assets/translations/pt-PT.json
@@ -425,6 +425,7 @@
"GSIM Application": "Aplicação GSM",
"Generate Report": "Gerar relatório",
"Get Parameters": "Obter parâmetros",
+ "Go back": "Voltar",
"Go to next step": "Ir para a próxima etapa",
"Group Loan Application": "Solicitação de empréstimo em grupo",
"Group Saving Application": "Aplicativo para salvar grupo",
@@ -459,6 +460,7 @@
"Pie Chart": "Gráfico de pizza",
"Post Dividend": "Pós Dividendo",
"Previous": "Anterior",
+ "Preview": "Pré-visualização",
"Print": "Imprimir",
"Proceed": "Continuar",
"Productive Collection Sheet": "Folha de Coleta Produtiva",
@@ -1070,6 +1072,7 @@
"Reject Checker": "Rejeitar verificador",
"Release Amount": "Valor de liberação",
"Repaid Every": "Reembolsado a cada",
+ "Repayment Schedule Preview": "Pré-visualização do calendário de pagamentos",
"Report Parameter": "Parâmetro de relatório",
"Reschedule Loan": "Empréstimo de remarcado",
"Revert Transaction": "Reverter transação",
@@ -3043,6 +3046,7 @@
"Loan Products": "Produtos de empréstimo",
"Loan Provisioning Criteria Organization": "Definir critérios de provisionamento de empréstimos para a organização",
"Loan Tranche Details": "Detalhes da parcela do empréstimo",
+ "Loan Term Variations": "Variações de Prazo do Empréstimo",
"Loan View": "Visão do empréstimo",
"Loan products define the rules, default settings": "Os produtos de empréstimo definem as regras, configurações padrão e restrições para as ofertas de empréstimo de uma instituição financeira. Um produto de empréstimo fornece um modelo para múltiplas contas de empréstimo para os clientes da instituição financeira.",
"Loan": "Empréstimo",
diff --git a/src/assets/translations/sw-SW.json b/src/assets/translations/sw-SW.json
index 6c3984d95d..bf135fa54b 100644
--- a/src/assets/translations/sw-SW.json
+++ b/src/assets/translations/sw-SW.json
@@ -425,6 +425,7 @@
"GSIM Application": "Maombi ya GSIM",
"Generate Report": "Tengeneza Ripoti",
"Get Parameters": "Pata Vigezo",
+ "Go back": "Rudi nyuma",
"Go to next step": "Nenda kwenye hatua inayofuata",
"Group Loan Application": "Maombi ya Mkopo wa Kikundi",
"Group Saving Application": "Maombi ya Kuokoa ya Kikundi",
@@ -459,6 +460,7 @@
"Pie Chart": "Jedwali la mdwara",
"Post Dividend": "Chapisha Gawio",
"Previous": "Iliyotangulia",
+ "Preview": "Onyesha awali",
"Print": "Chapisha",
"Proceed": "Endelea",
"Productive Collection Sheet": "Karatasi ya Ukusanyaji Yenye Tija",
@@ -1070,6 +1072,7 @@
"Reject Checker": "Kukataa ukaguzi",
"Release Amount": "Kiasi cha Kutolewa",
"Repaid Every": "Kulipwa Kila",
+ "Repayment Schedule Preview": "Onyesha awali ratiba ya malipo",
"Report Parameter": "Ripoti Parameta",
"Reschedule Loan": "Rejesha mkopo",
"Revert Transaction": "Rejesha Muamala",
@@ -3042,6 +3045,7 @@
"Loan Products": "Bidhaa za Mkopo",
"Loan Provisioning Criteria Organization": "Bainisha Vigezo vya Utoaji wa Mkopo kwa Shirika",
"Loan Tranche Details": "Maelezo ya Mkopo",
+ "Loan Term Variations": "Tofauti za Muda za Mkopo",
"Loan View": "Mtazamo wa mkopo",
"Loan products define the rules, default settings": "Bidhaa za mkopo hufafanua sheria, mipangilio chaguo-msingi na vikwazo vya utoaji wa mikopo wa taasisi ya fedha. Bidhaa ya mkopo hutoa kiolezo cha akaunti nyingi za mkopo kwa wateja wa taasisi ya fedha.",
"Loan": "Mkopo",
diff --git a/src/environments/.env.ts b/src/environments/.env.ts
deleted file mode 100644
index 33c80f5685..0000000000
--- a/src/environments/.env.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-// IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN!
-/* tslint:disable */
-export default {
- 'mifos_x': {
- 'version': '251030',
- 'hash': '25a79252'
- },
- 'allow_switching_backend_instance': true
-};
-/* tslint:enable */
diff --git a/src/index.html b/src/index.html
index a168e7b28e..8704892244 100644
--- a/src/index.html
+++ b/src/index.html
@@ -25,7 +25,7 @@
}
const code = getQueryParam('code');
if (code) {
- localStorage.setItem('auth_code', code);
+ sessionStorage.setItem('auth_code', code);
window.location.href = '/#/callback';
}
diff --git a/src/theme/_dark_content.scss b/src/theme/_dark_content.scss
index 77fbf34a2c..e21a1346d8 100644
--- a/src/theme/_dark_content.scss
+++ b/src/theme/_dark_content.scss
@@ -267,4 +267,11 @@ body.dark-theme {
content: url('../assets/images/white-mifos.png');
}
}
+
+ // Table row hover effect for dark mode
+ .mat-mdc-row:hover,
+ tr.select-row:hover,
+ .select-row:hover {
+ background-color: rgb(255 255 255 / 8%) !important;
+ }
}