feat(accounts): add Modify Application for rejected Current/Savings/FD/RD accounts
- Add 'Modify Application' button in kebab menu for rejected accounts (all 4 types)
- Rewrite edit components with tab-based UI matching create form
- Pre-populate all create-time values in edit form: product, account status,
customer segment, account type, joint holders, nominees, introducer,
deposit account link, senior citizen rate, tenure, flags (msme/balanceSwap/autoDebit)
- Fix edit mode detection using accountNo presence
- Add preselectedIntroducer @Input to IntroducerSearchComponent for pre-fill
- Support both camelCase and lowercase keys for joint holders (MySQL queryForList)
- Fix account number display: actual accountNo before previewAccountNo
- Fix cbsAccountType restore: cbsAccountType only, avoid Fineract enum object
- Add nominees fetch via /clients/{id}/nominees API in all 4 edit components
- Fix RD/FD Interest Rate Chart: SimpleChanges to prevent reset race condition
- Pre-check Senior Citizen checkbox in edit form from isSeniorCitizen DB field
- Auto-calculate rate when tenure pre-filled in edit mode
- FD calculator: use backend calculate-fd-interest API for all compounding types
- Support interestCompoundingPeriodInMonths=0 (daily) in FD calculator API
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
3be2f96210
commit
260fabaa48
+35
-1
@@ -29,6 +29,7 @@ export class CurrentAccountCurrencyStepComponent implements OnChanges {
|
||||
@Input() currentAccountProductTemplate: any;
|
||||
@Input() primaryClientId: number | null = null;
|
||||
@Input() isNonIndividualClient = false;
|
||||
@Input() existingAccount: any = null;
|
||||
@ViewChild(NomineeFormComponent) nomineeFormComp: NomineeFormComponent;
|
||||
|
||||
nominees: any[] = [];
|
||||
@@ -41,8 +42,29 @@ export class CurrentAccountCurrencyStepComponent implements OnChanges {
|
||||
msme = false;
|
||||
autoDebitFacility = false;
|
||||
|
||||
private existingAccountApplied = false;
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['currentAccountProductTemplate']?.currentValue) {
|
||||
if (changes['existingAccount']?.currentValue) {
|
||||
const ea = changes['existingAccount'].currentValue;
|
||||
this.existingAccountApplied = true;
|
||||
if (ea.balanceSwapOption !== undefined) {
|
||||
this.balanceSwapOption = !!ea.balanceSwapOption;
|
||||
}
|
||||
if (ea.msme !== undefined) {
|
||||
this.msme = !!ea.msme;
|
||||
}
|
||||
if (ea.autoDebitFacility !== undefined) {
|
||||
this.autoDebitFacility = !!ea.autoDebitFacility;
|
||||
}
|
||||
// nominees: try array first, then parse nomineeData JSON string
|
||||
const nominees =
|
||||
Array.isArray(ea.nominees) && ea.nominees.length > 0 ? ea.nominees : this.parseNomineeData(ea.nomineeData);
|
||||
if (nominees.length > 0) {
|
||||
this.nominees = [...nominees];
|
||||
}
|
||||
}
|
||||
if (changes['currentAccountProductTemplate']?.currentValue && !this.existingAccountApplied) {
|
||||
const pt = changes['currentAccountProductTemplate'].currentValue;
|
||||
this.balanceSwapOption = !!pt.balanceSwapOption;
|
||||
this.msme = !!pt.msme;
|
||||
@@ -50,6 +72,18 @@ export class CurrentAccountCurrencyStepComponent implements OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
private parseNomineeData(nomineeData: any): any[] {
|
||||
if (!nomineeData) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const parsed = typeof nomineeData === 'string' ? JSON.parse(nomineeData) : nomineeData;
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
get totalShare(): number {
|
||||
return this.nominees.reduce((sum, n) => sum + (+n.percentageShare || 0), 0);
|
||||
}
|
||||
|
||||
+4
-1
@@ -124,7 +124,7 @@
|
||||
<mat-label>Account No</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[value]="productTemplate?.previewAccountNo || currentAccountTemplate?.accountNo || 'Auto-generated'"
|
||||
[value]="currentAccountTemplate?.accountNo || productTemplate?.previewAccountNo || 'Auto-generated'"
|
||||
readonly
|
||||
/>
|
||||
</mat-form-field>
|
||||
@@ -167,6 +167,9 @@
|
||||
@if (productTemplate?.introducerRequired) {
|
||||
<mifosx-introducer-search
|
||||
[excludedClientIds]="primaryClientId ? [primaryClientId] : []"
|
||||
[preselectedIntroducer]="
|
||||
introducerId ? { clientId: introducerId, displayName: introducerName, cifNumber: introducerCif } : null
|
||||
"
|
||||
(introducerChange)="onIntroducerChange($event)"
|
||||
></mifosx-introducer-search>
|
||||
}
|
||||
|
||||
+121
-3
@@ -65,6 +65,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
|
||||
introducerId: number | null = null;
|
||||
introducerName: string | null = null;
|
||||
introducerCif: string | null = null;
|
||||
private isEditMode = false;
|
||||
|
||||
// ── Non-Individual (existing) ──────────────────────────────────────────
|
||||
isNonIndividualClient = false;
|
||||
@@ -201,11 +202,29 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
|
||||
|
||||
if (this.currentAccountTemplate) {
|
||||
this.productData = this.currentAccountTemplate.productOptions;
|
||||
if (this.currentAccountTemplate.currentAccountProductId) {
|
||||
const existingProductId =
|
||||
this.currentAccountTemplate.currentAccountProductId ?? this.currentAccountTemplate.productId;
|
||||
// Edit mode: account response always has accountNo; pure template has previewAccountNo only
|
||||
if (this.currentAccountTemplate.accountNo) {
|
||||
this.isEditMode = true;
|
||||
}
|
||||
if ((!this.productData || this.productData.length === 0) && existingProductId) {
|
||||
this.isEditMode = true;
|
||||
this.productData = [
|
||||
{
|
||||
id: existingProductId,
|
||||
name:
|
||||
this.currentAccountTemplate.productName ??
|
||||
this.currentAccountTemplate.currentAccountProductName ??
|
||||
'Product'
|
||||
}
|
||||
];
|
||||
}
|
||||
if (existingProductId) {
|
||||
this.currentAccountDetailsForm.patchValue({
|
||||
productId: this.currentAccountTemplate.currentAccountProductId,
|
||||
productId: existingProductId,
|
||||
submittedOnDate:
|
||||
this.currentAccountTemplate.timeline.submittedOnDate &&
|
||||
this.currentAccountTemplate.timeline?.submittedOnDate &&
|
||||
new Date(this.currentAccountTemplate.timeline.submittedOnDate),
|
||||
externalId: this.currentAccountTemplate.externalId
|
||||
});
|
||||
@@ -273,10 +292,106 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
|
||||
} else {
|
||||
this.currentAccountDetailsForm.get('fieldOfficerId').patchValue('');
|
||||
}
|
||||
// === Restore existing account values when in edit/modify mode ===
|
||||
this.restoreExistingAccountValues();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private restoreExistingAccountValues(): void {
|
||||
const ea = this.currentAccountTemplate;
|
||||
// Edit mode: API returns `productId`, create template uses `currentAccountProductId`
|
||||
if (!ea?.currentAccountProductId && !ea?.productId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const strVal = (v: any): string => (typeof v === 'object' && v !== null ? (v.value ?? '') : (v ?? ''));
|
||||
|
||||
const accountStatus = strVal(ea.accountStatus);
|
||||
if (accountStatus) {
|
||||
this.selectedAccountStatus = accountStatus;
|
||||
}
|
||||
|
||||
const customerSegment = strVal(ea.customerSegment);
|
||||
if (customerSegment) {
|
||||
this.selectedCustomerSegment = customerSegment;
|
||||
}
|
||||
|
||||
// cbsAccountType is the CBS descriptive type ("Standard Account", etc.)
|
||||
// ea.accountType is Fineract's enum-based type ({id,value:"Individual"}) — do NOT use as fallback
|
||||
const accountType = ea.cbsAccountType ? strVal(ea.cbsAccountType) : null;
|
||||
if (accountType) {
|
||||
this.selectedAccountType = accountType;
|
||||
}
|
||||
|
||||
if (accountStatus === 'JOINT') {
|
||||
const modeOfOp = strVal(ea.modeOfOperation);
|
||||
if (modeOfOp) {
|
||||
this.modeOfOperation = modeOfOp;
|
||||
}
|
||||
// jointHolders may not be returned by backend — skip if absent
|
||||
if (Array.isArray(ea.jointHolders) && ea.jointHolders.length > 0) {
|
||||
// Support both camelCase and lowercase keys (MySQL queryForList returns lowercase)
|
||||
const isPH = (h: any) => h.isPrimary ?? h.isprimary ?? false;
|
||||
const primary = ea.jointHolders.find((h: any) => isPH(h));
|
||||
const ps = primary?.sharePercentage ?? primary?.sharepercentage;
|
||||
if (ps) {
|
||||
this.primaryHolderShare = +ps;
|
||||
}
|
||||
this.jointHolders = ea.jointHolders
|
||||
.filter((h: any) => !isPH(h))
|
||||
.map((h: any) => ({
|
||||
clientId: h.clientId ?? h.clientid,
|
||||
clientName: h.displayName ?? h.displayname ?? h.clientName ?? '',
|
||||
clientCif: h.cifNumber ?? h.cifnumber ?? h.clientCif ?? '',
|
||||
sharePercentage: +(h.sharePercentage ?? h.sharepercentage ?? 0),
|
||||
isMandatory: !!(h.isMandatory ?? h.ismandatory)
|
||||
}));
|
||||
}
|
||||
}
|
||||
if (accountStatus === 'INDIVIDUAL_MALE' && ea.specialCategory) {
|
||||
this.selectedSpecialCategory = ea.specialCategory;
|
||||
}
|
||||
if (accountStatus === 'INDIVIDUAL_FEMALE' && ea.weakerSection) {
|
||||
this.selectedWeakerSection = ea.weakerSection;
|
||||
}
|
||||
if (accountStatus === 'MINOR') {
|
||||
if (ea.guardianClientId) {
|
||||
this.guardianClientId = ea.guardianClientId;
|
||||
}
|
||||
if (ea.guardianName) {
|
||||
this.guardianName = ea.guardianName;
|
||||
}
|
||||
if (ea.guardianCifNumber) {
|
||||
this.guardianCif = ea.guardianCifNumber;
|
||||
}
|
||||
if (ea.guardianRelationship) {
|
||||
this.guardianRelationship = ea.guardianRelationship;
|
||||
}
|
||||
if (ea.minorDateOfBirth) {
|
||||
const d = new Date(ea.minorDateOfBirth);
|
||||
if (!isNaN(d.getTime())) {
|
||||
this.minorDob = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
// API returns introducerClientId, not introducerId
|
||||
const introId = ea.introducerId ?? ea.introducerClientId;
|
||||
if (introId) {
|
||||
this.introducerId = introId;
|
||||
this.introducerName = ea.introducerName ?? null;
|
||||
this.introducerCif = ea.introducerCifNumber ?? null;
|
||||
}
|
||||
this.currentAccountDetailsForm.patchValue({
|
||||
balanceSwapOption: !!ea.balanceSwapOption,
|
||||
msme: !!ea.msme,
|
||||
autoDebitFacility: !!ea.autoDebitFacility,
|
||||
...(ea.establishmentDate ? { establishmentDate: new Date(ea.establishmentDate) } : {}),
|
||||
...(ea.startOperationDate ? { startOperationDate: new Date(ea.startOperationDate) } : {})
|
||||
});
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
|
||||
private extractOptions(options: any[], fallback?: string): string[] {
|
||||
if (Array.isArray(options) && options.length > 0) {
|
||||
return options.map((o: any) => o.value ?? o.name ?? o);
|
||||
@@ -317,6 +432,9 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
|
||||
}
|
||||
|
||||
private autoDetectAccountStatus(): void {
|
||||
if (this.isEditMode) {
|
||||
return;
|
||||
}
|
||||
if (this.clientAge === null) return;
|
||||
if (this.clientAge < 18) {
|
||||
this.selectedAccountStatus = 'MINOR';
|
||||
|
||||
+10
-1
@@ -40,10 +40,19 @@ const DEFAULT_FACILITY_CONFIG = {
|
||||
})
|
||||
export class CurrentAccountDigitalStepComponent {
|
||||
facilityConfig = { ...DEFAULT_FACILITY_CONFIG };
|
||||
private hasExistingAccount = false;
|
||||
|
||||
@Input()
|
||||
set existingAccount(value: any) {
|
||||
if (value?.accountFacilityConfig) {
|
||||
this.hasExistingAccount = true;
|
||||
this.facilityConfig = { ...DEFAULT_FACILITY_CONFIG, ...value.accountFacilityConfig };
|
||||
}
|
||||
}
|
||||
|
||||
@Input()
|
||||
set currentAccountProductTemplate(value: any) {
|
||||
if (value?.accountFacilityConfig) {
|
||||
if (!this.hasExistingAccount && value?.accountFacilityConfig) {
|
||||
this.facilityConfig = { ...DEFAULT_FACILITY_CONFIG, ...value.accountFacilityConfig };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +143,16 @@ export class CurrentAccountButtonsConfiguration {
|
||||
}
|
||||
];
|
||||
break;
|
||||
case 'Rejected':
|
||||
case 'rejected':
|
||||
this.buttonsArray = [
|
||||
{
|
||||
name: 'Modify Application',
|
||||
icon: 'pen',
|
||||
taskPermissionName: 'UPDATE_CURRENTACCOUNT'
|
||||
}
|
||||
];
|
||||
break;
|
||||
default:
|
||||
this.buttonsArray = [];
|
||||
}
|
||||
|
||||
+139
-69
@@ -6,76 +6,146 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="container">
|
||||
<mat-stepper
|
||||
orientation="vertical"
|
||||
[linear]="false"
|
||||
class="mat-elevation-z8"
|
||||
labelPosition="bottom"
|
||||
#currentAccountProductStepper
|
||||
<div class="cca-container">
|
||||
<!-- Hero -->
|
||||
<div class="cca-header">
|
||||
<fa-icon icon="university" class="cca-header__icon"></fa-icon>
|
||||
<div>
|
||||
<h2 class="cca-header__title">Modify Current Account Application</h2>
|
||||
<div class="cca-header__sub">
|
||||
Review and update the account details, currency settings and digital channels before resubmitting.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wizard tabs -->
|
||||
<mat-tab-group
|
||||
class="cca-tabs"
|
||||
animationDuration="200ms"
|
||||
[preserveContent]="true"
|
||||
[(selectedIndex)]="selectedTabIndex"
|
||||
>
|
||||
<ng-template matStepperIcon="number">
|
||||
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="edit">
|
||||
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="done">
|
||||
<fa-icon icon="check" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="error">
|
||||
<fa-icon icon="exclamation-triangle" size="lg"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="preview">
|
||||
<fa-icon icon="eye" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<mat-step [stepControl]="currentAccountDetailsForm">
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.DETAILS' | translate }}</ng-template>
|
||||
|
||||
<mifosx-current-account-details-step
|
||||
[currentAccountTemplate]="currentAccountAndTemplate"
|
||||
(currentAccountProductTemplate)="setTemplate($event)"
|
||||
>
|
||||
</mifosx-current-account-details-step>
|
||||
</mat-step>
|
||||
|
||||
<mat-step [stepControl]="currentAccountTermsForm">
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template>
|
||||
|
||||
<mifosx-current-account-terms-step
|
||||
[currentAccountProductTemplate]="currentAccountProductTemplate"
|
||||
[currentAccountTemplate]="currentAccountAndTemplate"
|
||||
></mifosx-current-account-terms-step>
|
||||
</mat-step>
|
||||
|
||||
<mat-step>
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template>
|
||||
|
||||
<mifosx-current-account-charges-step
|
||||
[currentAccountProductTemplate]="currentAccountProductTemplate"
|
||||
[currentAccountTemplate]="currentAccountAndTemplate"
|
||||
[currencyCode]="currentAccountTermsForm.get('currencyCode')"
|
||||
>
|
||||
</mifosx-current-account-charges-step>
|
||||
</mat-step>
|
||||
|
||||
@if (currentAccountFormValidAndNotPristine) {
|
||||
<mat-step completed>
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template>
|
||||
<mifosx-current-account-preview-step
|
||||
[currentAccountProductTemplate]="currentAccountProductTemplate"
|
||||
<!-- Tab 1: Product Classification + Details -->
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="check-square" class="cca-tab__icon"></fa-icon>
|
||||
Product
|
||||
</ng-template>
|
||||
<div class="cca-tab-body">
|
||||
<mifosx-current-account-details-step
|
||||
[currentAccountTemplate]="currentAccountAndTemplate"
|
||||
[currentAccountTermsForm]="currentAccountTermsForm"
|
||||
[currentAccount]="currentAccount"
|
||||
(submitEvent)="submit()"
|
||||
[hideButtons]="true"
|
||||
(currentAccountProductTemplate)="setTemplate($event)"
|
||||
>
|
||||
</mifosx-current-account-preview-step>
|
||||
</mat-step>
|
||||
}
|
||||
</mat-stepper>
|
||||
</mifosx-current-account-details-step>
|
||||
|
||||
<div class="cca-actions">
|
||||
<span class="cca-actions__spacer"></span>
|
||||
<button mat-raised-button color="primary" [disabled]="!currentAccountProductTemplate" (click)="next()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab 2: Currency -->
|
||||
<mat-tab [disabled]="!currentAccountProductTemplate">
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="dollar-sign" class="cca-tab__icon"></fa-icon>
|
||||
Currency
|
||||
</ng-template>
|
||||
<div class="cca-tab-body">
|
||||
@if (currentAccountProductTemplate) {
|
||||
<mifosx-current-account-currency-step
|
||||
[currentAccountProductTemplate]="currentAccountProductTemplate"
|
||||
[primaryClientId]="currentAccountAndTemplate?.clientId ?? null"
|
||||
[isNonIndividualClient]="currentAccountDetailsStep.isNonIndividualClient"
|
||||
[existingAccount]="currentAccountAndTemplate"
|
||||
></mifosx-current-account-currency-step>
|
||||
}
|
||||
|
||||
<div class="cca-actions">
|
||||
<button mat-stroked-button (click)="prev()">
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<span class="cca-actions__spacer"></span>
|
||||
<button mat-raised-button color="primary" (click)="next()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab 3: Digital -->
|
||||
<mat-tab [disabled]="!currentAccountProductTemplate">
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="mobile-alt" class="cca-tab__icon"></fa-icon>
|
||||
Digital
|
||||
</ng-template>
|
||||
<div class="cca-tab-body">
|
||||
@if (currentAccountProductTemplate) {
|
||||
<mifosx-current-account-digital-step
|
||||
[currentAccountProductTemplate]="currentAccountProductTemplate"
|
||||
[existingAccount]="currentAccountAndTemplate"
|
||||
></mifosx-current-account-digital-step>
|
||||
}
|
||||
|
||||
<div class="cca-actions">
|
||||
<button mat-stroked-button (click)="prev()">
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<span class="cca-actions__spacer"></span>
|
||||
<button mat-raised-button color="primary" (click)="next()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab 4: Preview -->
|
||||
<mat-tab [disabled]="!currentAccountProductTemplate">
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="eye" class="cca-tab__icon"></fa-icon>
|
||||
{{ 'labels.inputs.PREVIEW' | translate }}
|
||||
</ng-template>
|
||||
<div class="cca-tab-body">
|
||||
@if (currentAccountProductTemplate) {
|
||||
<mifosx-current-account-preview-step
|
||||
[currentAccountProductTemplate]="currentAccountProductTemplate"
|
||||
[currentAccountTemplate]="currentAccountAndTemplate"
|
||||
[currentAccountTermForm]="currentAccountTermForm"
|
||||
[currentAccountSettingsForm]="currentAccountSettingsForm"
|
||||
[currentAccountTermsForm]="currentAccountTermsForm"
|
||||
[currentAccount]="currentAccount"
|
||||
[currentAccountDigital]="currentAccountDigital"
|
||||
[currentAccountNominees]="currentAccountCurrencyStep?.nominees ?? []"
|
||||
[currentAccountCurrencyFlags]="currentAccountCurrencyFlags"
|
||||
[hideButtons]="true"
|
||||
(submitEvent)="submit()"
|
||||
>
|
||||
</mifosx-current-account-preview-step>
|
||||
}
|
||||
|
||||
<div class="cca-actions">
|
||||
<button mat-stroked-button (click)="prev()">
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<span class="cca-actions__spacer"></span>
|
||||
<button mat-stroked-button [routerLink]="['../']">
|
||||
{{ 'labels.buttons.Cancel' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" class="preview-submit-row__primary" (click)="submit()">
|
||||
<fa-icon icon="check" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Submit' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
|
||||
@@ -5,3 +5,164 @@
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
$navy: #0d1b2a;
|
||||
$blue: #1e88e5;
|
||||
$blue-d: #1e40af;
|
||||
$indigo: #4f46e5;
|
||||
$border: #e2e8f0;
|
||||
|
||||
.cca-container {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
/* ─── Hero header (matches Nexora pages) ────────────────────────────── */
|
||||
.cca-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&__icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, $blue 0%, $indigo 100%);
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
box-shadow: 0 2px 8px rgb(29 78 216 / 25%);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: $navy;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
margin-top: 2px;
|
||||
font-size: 12.5px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Wizard tab strip ──────────────────────────────────────────────── */
|
||||
.cca-tabs {
|
||||
background: #fff;
|
||||
border: 1px solid $border;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
|
||||
overflow: hidden;
|
||||
|
||||
::ng-deep {
|
||||
.mat-mdc-tab-header {
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid $border;
|
||||
}
|
||||
|
||||
.mat-mdc-tab {
|
||||
min-width: 140px;
|
||||
height: 50px;
|
||||
padding: 0 22px;
|
||||
font-size: 13px !important;
|
||||
font-weight: 600 !important;
|
||||
letter-spacing: 0.01em;
|
||||
color: #475569 !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab:hover:not(.mdc-tab--active, .mat-mdc-tab-disabled) {
|
||||
background: rgb(30 64 175 / 5%);
|
||||
color: $navy !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab.mdc-tab--active {
|
||||
.mdc-tab__text-label {
|
||||
color: $blue-d !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mdc-tab-indicator__content--underline {
|
||||
border-color: $blue-d !important;
|
||||
border-top-width: 3px !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab.mat-mdc-tab-disabled {
|
||||
opacity: 0.45 !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mat-mdc-tab-body-wrapper {
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
.mat-mdc-tab-body,
|
||||
.mat-mdc-tab-body-content {
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cca-tab__icon {
|
||||
margin-right: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ─── Tab body + action bar ─────────────────────────────────────────── */
|
||||
.cca-tab-body {
|
||||
padding: 24px;
|
||||
|
||||
@media (width <= 768px) {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.cca-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
|
||||
&__spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 40px !important;
|
||||
border-radius: 10px !important;
|
||||
font-weight: 600 !important;
|
||||
padding: 0 18px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.m-l-10 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.m-r-10 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
button.preview-submit-row__primary,
|
||||
.mat-mdc-raised-button.mat-primary.preview-submit-row__primary,
|
||||
.preview-submit-row__primary {
|
||||
background: linear-gradient(135deg, #10b981 0%, #047857 100%) !important;
|
||||
|
||||
--mdc-protected-button-container-color: transparent !important;
|
||||
--mdc-filled-button-container-color: transparent !important;
|
||||
|
||||
color: #fff !important;
|
||||
box-shadow: 0 4px 12px -2px rgb(4 120 87 / 35%) !important;
|
||||
}
|
||||
|
||||
@@ -9,23 +9,26 @@
|
||||
/** Angular Imports */
|
||||
import { Component, ViewChild, inject } from '@angular/core';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { ClientsService } from 'app/clients/clients.service';
|
||||
|
||||
/** Custom Components */
|
||||
import { CurrentAccountDetailsStepComponent } from '../current-account-stepper/current-account-details-step/current-account-details-step.component';
|
||||
import { CurrentAccountTermsStepComponent } from '../current-account-stepper/current-account-terms-step/current-account-terms-step.component';
|
||||
import { CurrentAccountChargesStepComponent } from '../current-account-stepper/current-account-charges-step/current-account-charges-step.component';
|
||||
import { CurrentAccountCurrencyStepComponent } from '../current-account-stepper/current-account-currency-step/current-account-currency-step.component';
|
||||
import { CurrentAccountDigitalStepComponent } from '../current-account-stepper/current-account-digital-step/current-account-digital-step.component';
|
||||
|
||||
/** Custom Services */
|
||||
import { CurrentAccountsService } from '../current-accounts.service';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper';
|
||||
import { AlertService } from 'app/core/alert/alert.service';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { CurrentAccountPreviewStepComponent } from '../current-account-stepper/current-account-preview-step/current-account-preview-step.component';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/**
|
||||
* Edit Current Account Component
|
||||
* Edit Current Account Component — same tab UI as Create, pre-populated with existing data.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-edit-current-account-account',
|
||||
@@ -33,14 +36,11 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
styleUrls: ['./edit-current-account.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatStepper,
|
||||
MatStepperIcon,
|
||||
MatTabsModule,
|
||||
FaIconComponent,
|
||||
MatStep,
|
||||
MatStepLabel,
|
||||
CurrentAccountDetailsStepComponent,
|
||||
CurrentAccountTermsStepComponent,
|
||||
CurrentAccountChargesStepComponent,
|
||||
CurrentAccountCurrencyStepComponent,
|
||||
CurrentAccountDigitalStepComponent,
|
||||
CurrentAccountPreviewStepComponent
|
||||
]
|
||||
})
|
||||
@@ -50,109 +50,172 @@ export class EditCurrentAccountComponent {
|
||||
private dateUtils = inject(Dates);
|
||||
private currentAccountsService = inject(CurrentAccountsService);
|
||||
private settingsService = inject(SettingsService);
|
||||
private alertService = inject(AlertService);
|
||||
private clientsService = inject(ClientsService);
|
||||
|
||||
/** Current Account Template */
|
||||
/** Full account data + template from resolver */
|
||||
currentAccountAndTemplate: any;
|
||||
/** Current Account Product Template */
|
||||
/** Product template emitted by details step */
|
||||
currentAccountProductTemplate: any;
|
||||
/** Currently visible wizard tab (0-indexed). */
|
||||
selectedTabIndex = 0;
|
||||
private readonly totalTabs = 4;
|
||||
|
||||
/** Current Account Details Step */
|
||||
@ViewChild(CurrentAccountDetailsStepComponent, { static: true })
|
||||
currentAccountDetailsStep: CurrentAccountDetailsStepComponent;
|
||||
/** Current Account Terms Step */
|
||||
@ViewChild(CurrentAccountTermsStepComponent, { static: true })
|
||||
currentAccountTermsStep: CurrentAccountTermsStepComponent;
|
||||
/** Current Account Charges Step */
|
||||
@ViewChild(CurrentAccountChargesStepComponent, { static: true })
|
||||
currentAccountChargesStep: CurrentAccountChargesStepComponent;
|
||||
|
||||
/**
|
||||
* Fetches current account template from `resolve`
|
||||
* @param {ActivatedRoute} route Activated Route
|
||||
* @param {Router} router Router
|
||||
* @param {Dates} dateUtils Date Utils
|
||||
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service
|
||||
* @param {SettingsService} settingsService Settings Service
|
||||
*/
|
||||
@ViewChild(CurrentAccountCurrencyStepComponent)
|
||||
currentAccountCurrencyStep: CurrentAccountCurrencyStepComponent;
|
||||
|
||||
@ViewChild(CurrentAccountDigitalStepComponent)
|
||||
currentAccountDigitalStep: CurrentAccountDigitalStepComponent;
|
||||
|
||||
constructor() {
|
||||
this.route.data.subscribe((data: { currentAccountAndTemplate: any }) => {
|
||||
this.currentAccountAndTemplate = data.currentAccountAndTemplate;
|
||||
const account = data.currentAccountAndTemplate;
|
||||
this.currentAccountAndTemplate = account;
|
||||
if (account?.clientId && account?.accountNo) {
|
||||
this.clientsService
|
||||
.getClientNominees(String(account.clientId), account.accountNo, 'CB')
|
||||
.subscribe((nominees: any[]) => {
|
||||
if (Array.isArray(nominees) && nominees.length > 0) {
|
||||
this.currentAccountAndTemplate = { ...this.currentAccountAndTemplate, nominees };
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets current account product template.
|
||||
* @param {any} $event API response
|
||||
*/
|
||||
setTemplate($event: any) {
|
||||
this.currentAccountProductTemplate = $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves current account details form.
|
||||
*/
|
||||
next(): void {
|
||||
if (this.selectedTabIndex < this.totalTabs - 1) {
|
||||
this.selectedTabIndex += 1;
|
||||
}
|
||||
}
|
||||
|
||||
prev(): void {
|
||||
if (this.selectedTabIndex > 0) {
|
||||
this.selectedTabIndex -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
get currentAccountDetailsForm() {
|
||||
return this.currentAccountDetailsStep.currentAccountDetailsForm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves current account terms form.
|
||||
*/
|
||||
get currentAccountTermsForm() {
|
||||
return this.currentAccountTermsStep.currentAccountTermsForm;
|
||||
get currentAccountTermForm(): FormGroup {
|
||||
return new FormGroup({});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks validity and pristinity of overall current account form .
|
||||
*/
|
||||
get currentAccountFormValidAndNotPristine() {
|
||||
return (
|
||||
this.currentAccountDetailsForm.valid &&
|
||||
this.currentAccountTermsForm.valid &&
|
||||
(!this.currentAccountDetailsForm.pristine ||
|
||||
!this.currentAccountTermsForm.pristine ||
|
||||
!this.currentAccountChargesStep.pristine)
|
||||
);
|
||||
get currentAccountSettingsForm(): FormGroup {
|
||||
return new FormGroup({});
|
||||
}
|
||||
|
||||
get currentAccountTermsForm(): FormGroup {
|
||||
return new FormGroup({});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves current account object.
|
||||
*/
|
||||
get currentAccount() {
|
||||
return { ...this.currentAccountDetailsStep.currentAccountDetails };
|
||||
}
|
||||
|
||||
get currentAccountDigital() {
|
||||
return this.currentAccountDigitalStep?.currentAccountDigital ?? null;
|
||||
}
|
||||
|
||||
get currentAccountCurrencyFlags() {
|
||||
return {
|
||||
...this.currentAccountDetailsStep.currentAccountDetails,
|
||||
...this.currentAccountTermsStep.currentAccountTerms,
|
||||
...this.currentAccountChargesStep.currentAccountCharges
|
||||
balanceSwapOption: this.currentAccountCurrencyStep?.balanceSwapOption ?? false,
|
||||
msme: this.currentAccountCurrencyStep?.msme ?? false,
|
||||
autoDebitFacility: this.currentAccountCurrencyStep?.autoDebitFacility ?? false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new share account.
|
||||
*/
|
||||
submit() {
|
||||
// TODO: Update once language and date settings are setup
|
||||
const detailsStep = this.currentAccountDetailsStep;
|
||||
if (detailsStep.selectedAccountStatus === 'JOINT' && detailsStep.totalJointSharePercentage !== 100) {
|
||||
this.alertService.alert({
|
||||
type: 'Error',
|
||||
message: `Total ownership is ${detailsStep.totalJointSharePercentage}%. It must equal 100% before submitting.`
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!this.currentAccountDetailsStep.isNonIndividualClient &&
|
||||
this.currentAccountProductTemplate?.nominationFacility &&
|
||||
(this.currentAccountCurrencyStep?.nominees ?? []).length === 0
|
||||
) {
|
||||
this.alertService.alert({
|
||||
type: 'Error',
|
||||
message: 'Nomination Facility is enabled for this product. Please add at least one nominee before submitting.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.currentAccountProductTemplate?.introducerRequired && !this.currentAccountDetailsStep.introducerId) {
|
||||
this.alertService.alert({
|
||||
type: 'Error',
|
||||
message: 'Introducer is required for this product. Please select an introducer before submitting.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
const locale = this.settingsService.language.code;
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
const monthDayFormat = 'dd MMMM';
|
||||
const currentAccount = {
|
||||
...this.currentAccount,
|
||||
charges: this.currentAccount.charges.map((charge: any) => ({
|
||||
chargeId: charge.id,
|
||||
amount: charge.amount,
|
||||
dueDate: this.dateUtils.formatDate(charge.dueDate, dateFormat),
|
||||
feeOnMonthDay: this.dateUtils.formatDate(charge.feeOnMonthDay, dateFormat),
|
||||
feeInterval: charge.feeInterval
|
||||
})),
|
||||
submittedOnDate: this.dateUtils.formatDate(this.currentAccount.submittedOnDate, dateFormat),
|
||||
const details = this.currentAccount;
|
||||
|
||||
const currentAccount: any = {
|
||||
...details,
|
||||
...this.currentAccountCurrencyStep.currencyStepData,
|
||||
...(this.currentAccountDigitalStep?.currentAccountDigital ?? {}),
|
||||
charges: [],
|
||||
submittedOnDate: this.dateUtils.formatDate(details.submittedOnDate, dateFormat),
|
||||
dateFormat,
|
||||
monthDayFormat,
|
||||
locale
|
||||
};
|
||||
|
||||
if (details.establishmentDate) {
|
||||
currentAccount.establishmentDate = this.dateUtils.formatDate(details.establishmentDate, dateFormat);
|
||||
}
|
||||
if (details.startOperationDate) {
|
||||
currentAccount.startOperationDate = this.dateUtils.formatDate(details.startOperationDate, dateFormat);
|
||||
}
|
||||
|
||||
if (this.currentAccountDetailsStep.isNonIndividualClient) {
|
||||
currentAccount.isNonIndividualClient = true;
|
||||
currentAccount.partners = this.currentAccountDetailsStep.relatedClients.map((c: any) => ({
|
||||
clientId: c.clientId,
|
||||
clientName: c.clientName,
|
||||
clientCif: c.clientCif,
|
||||
sharePercentage: c.sharePercentage,
|
||||
isMandatory: c.isMandatory
|
||||
}));
|
||||
} else {
|
||||
delete currentAccount.isNonIndividualClient;
|
||||
}
|
||||
|
||||
if (this.currentAccountAndTemplate.clientId) {
|
||||
currentAccount.clientId = this.currentAccountAndTemplate.clientId;
|
||||
} else {
|
||||
currentAccount.groupId = this.currentAccountAndTemplate.groupId;
|
||||
}
|
||||
|
||||
const nominees = this.currentAccountCurrencyStep?.nominees ?? [];
|
||||
if (nominees.length > 0) {
|
||||
currentAccount.nominees = nominees.map((n: any) => ({
|
||||
nomineeClientId: n.nomineeClientId,
|
||||
nominationType: n.nominationType,
|
||||
relationshipId: n.relationshipId,
|
||||
percentageShare: n.percentageShare,
|
||||
nomineeIsMinor: n.nomineeIsMinor,
|
||||
...(n.guardianClientId ? { guardianClientId: n.guardianClientId } : {}),
|
||||
remarks: n.remarks
|
||||
}));
|
||||
}
|
||||
|
||||
this.currentAccountsService
|
||||
.updateCurrentAccount(this.currentAccountAndTemplate.id, currentAccount)
|
||||
.subscribe((response: any) => {
|
||||
|
||||
+1
@@ -87,6 +87,7 @@
|
||||
[fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate"
|
||||
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
|
||||
[cbsBaseRates]="cbsBaseRates"
|
||||
[submittedOnDate]="submittedOnDate"
|
||||
(tenureSelected)="onTenureSelected($event)"
|
||||
>
|
||||
</mifosx-fixed-deposit-account-interest-rate-chart-step>
|
||||
|
||||
+5
@@ -153,6 +153,11 @@ export class CreateFixedDepositAccountComponent {
|
||||
};
|
||||
}
|
||||
|
||||
get submittedOnDate(): Date | null {
|
||||
const val = this.fixedDepositsAccountDetailsStep?.fixedDepositAccountDetails?.submittedOnDate;
|
||||
return val ? new Date(val) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the fixed deposit form and creates a new fixed deposit account
|
||||
*/
|
||||
|
||||
+138
-89
@@ -6,96 +6,145 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="container">
|
||||
<mat-stepper
|
||||
orientation="vertical"
|
||||
[linear]="false"
|
||||
class="mat-elevation-z8"
|
||||
labelPosition="bottom"
|
||||
#fixedDepositAccountStepper
|
||||
<div class="cfa-container">
|
||||
<!-- Hero Header -->
|
||||
<div class="cfa-header">
|
||||
<fa-icon icon="university" class="cfa-header__icon"></fa-icon>
|
||||
<div>
|
||||
<h2 class="cfa-header__title">Modify Fixed Deposit Account Application</h2>
|
||||
<div class="cfa-header__sub">
|
||||
Review and update the account details, currency and interest terms before resubmitting.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wizard tabs -->
|
||||
<mat-tab-group
|
||||
class="cfa-tabs"
|
||||
animationDuration="200ms"
|
||||
[preserveContent]="true"
|
||||
[(selectedIndex)]="selectedTabIndex"
|
||||
>
|
||||
<ng-template matStepperIcon="number">
|
||||
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="edit">
|
||||
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="done">
|
||||
<fa-icon icon="check" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="error">
|
||||
<fa-icon icon="exclamation-triangle" size="lg"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="preview">
|
||||
<fa-icon icon="eye" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<mat-step [stepControl]="fixedDepositAccountDetailsForm">
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.DETAILS' | translate }}</ng-template>
|
||||
|
||||
<mifosx-fixed-deposit-account-details-step
|
||||
[fixedDepositsAccountTemplate]="fixedDepositsAccountAndTemplate"
|
||||
(fixedDepositsAccountProductTemplate)="setTemplate($event)"
|
||||
>
|
||||
</mifosx-fixed-deposit-account-details-step>
|
||||
</mat-step>
|
||||
|
||||
<mat-step [stepControl]="fixedDepositAccountTermsForm">
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template>
|
||||
|
||||
<mifosx-fixed-deposit-account-terms-step
|
||||
[fixedDepositsAccountTemplate]="fixedDepositsAccountAndTemplate"
|
||||
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
|
||||
>
|
||||
</mifosx-fixed-deposit-account-terms-step>
|
||||
</mat-step>
|
||||
|
||||
<mat-step [stepControl]="fixedDepositAccountSettingsForm">
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.SETTINGS' | translate }}</ng-template>
|
||||
|
||||
<mifosx-fixed-deposit-account-settings-step
|
||||
[fixedDepositsAccountTemplate]="fixedDepositsAccountAndTemplate"
|
||||
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
|
||||
>
|
||||
</mifosx-fixed-deposit-account-settings-step>
|
||||
</mat-step>
|
||||
|
||||
<mat-step>
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.INTEREST RATE CHART' | translate }}</ng-template>
|
||||
|
||||
<mifosx-fixed-deposit-account-interest-rate-chart-step
|
||||
[fixedDepositsAccountTemplate]="fixedDepositsAccountAndTemplate"
|
||||
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
|
||||
>
|
||||
</mifosx-fixed-deposit-account-interest-rate-chart-step>
|
||||
</mat-step>
|
||||
|
||||
<mat-step>
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template>
|
||||
|
||||
<mifosx-fixed-deposit-account-charges-step
|
||||
[fixedDepositsAccountTemplate]="fixedDepositsAccountAndTemplate"
|
||||
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
|
||||
[fixedDepositAccountFormValid]="fixedDepositAccountFormValidAndNotPristine"
|
||||
>
|
||||
</mifosx-fixed-deposit-account-charges-step>
|
||||
</mat-step>
|
||||
|
||||
@if (fixedDepositAccountFormValidAndNotPristine) {
|
||||
<mat-step state="preview" completed>
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template>
|
||||
<mifosx-fixed-deposit-account-preview-step
|
||||
<!-- Tab 1: Product Classification + Details -->
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="check-square" class="cfa-tab__icon"></fa-icon>
|
||||
Product
|
||||
</ng-template>
|
||||
<div class="cfa-tab-body">
|
||||
<mifosx-fixed-deposit-account-details-step
|
||||
[fixedDepositsAccountTemplate]="fixedDepositsAccountAndTemplate"
|
||||
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
|
||||
[fixedDepositAccountData]="fixedDepositAccount"
|
||||
(submitEvent)="submit()"
|
||||
(fixedDepositsAccountProductTemplate)="setTemplate($event)"
|
||||
[hideButtons]="true"
|
||||
>
|
||||
</mifosx-fixed-deposit-account-preview-step>
|
||||
</mat-step>
|
||||
}
|
||||
</mat-stepper>
|
||||
</mifosx-fixed-deposit-account-details-step>
|
||||
|
||||
<div class="cfa-actions">
|
||||
<span class="cfa-actions__spacer"></span>
|
||||
<button mat-raised-button color="primary" [disabled]="!fixedDepositsAccountProductTemplate" (click)="next()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab 2: Currency -->
|
||||
<mat-tab [disabled]="!fixedDepositsAccountProductTemplate">
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="dollar-sign" class="cfa-tab__icon"></fa-icon>
|
||||
Currency
|
||||
</ng-template>
|
||||
<div class="cfa-tab-body">
|
||||
@if (fixedDepositsAccountProductTemplate) {
|
||||
<mifosx-fixed-deposit-account-currency-step
|
||||
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
|
||||
[primaryClientId]="fixedDepositsAccountAndTemplate?.clientId ?? null"
|
||||
[existingAccount]="fixedDepositsAccountAndTemplate"
|
||||
></mifosx-fixed-deposit-account-currency-step>
|
||||
}
|
||||
|
||||
<div class="cfa-actions">
|
||||
<button mat-stroked-button (click)="prev()">
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<span class="cfa-actions__spacer"></span>
|
||||
<button mat-raised-button color="primary" (click)="next()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab 3: Interest Rate Chart -->
|
||||
<mat-tab [disabled]="!fixedDepositsAccountProductTemplate">
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="university" class="cfa-tab__icon"></fa-icon>
|
||||
Interest Rate Chart
|
||||
</ng-template>
|
||||
<div class="cfa-tab-body">
|
||||
@if (fixedDepositsAccountProductTemplate) {
|
||||
<mifosx-fixed-deposit-account-interest-rate-chart-step
|
||||
[fixedDepositsAccountTemplate]="fixedDepositsAccountAndTemplate"
|
||||
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
|
||||
[cbsBaseRates]="cbsBaseRates"
|
||||
[initialTenure]="selectedTenure"
|
||||
[submittedOnDate]="submittedOnDate"
|
||||
(tenureSelected)="onTenureSelected($event)"
|
||||
>
|
||||
</mifosx-fixed-deposit-account-interest-rate-chart-step>
|
||||
}
|
||||
|
||||
<div class="cfa-actions">
|
||||
<button mat-stroked-button (click)="prev()">
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<span class="cfa-actions__spacer"></span>
|
||||
<button mat-raised-button color="primary" (click)="next()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab 4: Preview -->
|
||||
<mat-tab [disabled]="!fixedDepositsAccountProductTemplate">
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="eye" class="cfa-tab__icon"></fa-icon>
|
||||
{{ 'labels.inputs.PREVIEW' | translate }}
|
||||
</ng-template>
|
||||
<div class="cfa-tab-body">
|
||||
@if (fixedDepositsAccountProductTemplate) {
|
||||
<mifosx-fixed-deposit-account-preview-step
|
||||
[fixedDepositsAccountTemplate]="fixedDepositsAccountAndTemplate"
|
||||
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
|
||||
[fixedDepositAccountData]="fixedDepositAccount"
|
||||
[selectedTenure]="selectedTenure"
|
||||
[fdNominees]="fixedDepositAccountCurrencyStep?.nominees ?? []"
|
||||
[hideButtons]="true"
|
||||
(submitEvent)="submit()"
|
||||
></mifosx-fixed-deposit-account-preview-step>
|
||||
}
|
||||
|
||||
<div class="cfa-actions">
|
||||
<button mat-stroked-button (click)="prev()">
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<span class="cfa-actions__spacer"></span>
|
||||
<button mat-stroked-button [routerLink]="['../']">
|
||||
{{ 'labels.buttons.Cancel' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" class="preview-submit-row__primary" (click)="submit()">
|
||||
<fa-icon icon="check" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Submit' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
|
||||
+161
@@ -5,3 +5,164 @@
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
$navy: #0d1b2a;
|
||||
$blue: #1e88e5;
|
||||
$blue-d: #1e40af;
|
||||
$indigo: #4f46e5;
|
||||
$border: #e2e8f0;
|
||||
|
||||
.cfa-container {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
/* ─── Hero header (matches Nexora pages) ────────────────────────────── */
|
||||
.cfa-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&__icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, $blue 0%, $indigo 100%);
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
box-shadow: 0 2px 8px rgb(29 78 216 / 25%);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: $navy;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
margin-top: 2px;
|
||||
font-size: 12.5px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Wizard tab strip ──────────────────────────────────────────────── */
|
||||
.cfa-tabs {
|
||||
background: #fff;
|
||||
border: 1px solid $border;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
|
||||
overflow: hidden;
|
||||
|
||||
::ng-deep {
|
||||
.mat-mdc-tab-header {
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid $border;
|
||||
}
|
||||
|
||||
.mat-mdc-tab {
|
||||
min-width: 140px;
|
||||
height: 50px;
|
||||
padding: 0 22px;
|
||||
font-size: 13px !important;
|
||||
font-weight: 600 !important;
|
||||
letter-spacing: 0.01em;
|
||||
color: #475569 !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab:hover:not(.mdc-tab--active, .mat-mdc-tab-disabled) {
|
||||
background: rgb(30 64 175 / 5%);
|
||||
color: $navy !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab.mdc-tab--active {
|
||||
.mdc-tab__text-label {
|
||||
color: $blue-d !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mdc-tab-indicator__content--underline {
|
||||
border-color: $blue-d !important;
|
||||
border-top-width: 3px !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab.mat-mdc-tab-disabled {
|
||||
opacity: 0.45 !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mat-mdc-tab-body-wrapper {
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
.mat-mdc-tab-body,
|
||||
.mat-mdc-tab-body-content {
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cfa-tab__icon {
|
||||
margin-right: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ─── Tab body + action bar ─────────────────────────────────────────── */
|
||||
.cfa-tab-body {
|
||||
padding: 24px;
|
||||
|
||||
@media (width <= 768px) {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.cfa-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
|
||||
&__spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 40px !important;
|
||||
border-radius: 10px !important;
|
||||
font-weight: 600 !important;
|
||||
padding: 0 18px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.m-l-10 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.m-r-10 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
button.preview-submit-row__primary,
|
||||
.mat-mdc-raised-button.mat-primary.preview-submit-row__primary,
|
||||
.preview-submit-row__primary {
|
||||
background: linear-gradient(135deg, #10b981 0%, #047857 100%) !important;
|
||||
|
||||
--mdc-protected-button-container-color: transparent !important;
|
||||
--mdc-filled-button-container-color: transparent !important;
|
||||
|
||||
color: #fff !important;
|
||||
box-shadow: 0 4px 12px -2px rgb(4 120 87 / 35%) !important;
|
||||
}
|
||||
|
||||
+198
-88
@@ -12,23 +12,23 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
/** Custom Services */
|
||||
import { FixedDepositsService } from '../fixed-deposits.service';
|
||||
import { ClientsService } from 'app/clients/clients.service';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { AlertService } from 'app/core/alert/alert.service';
|
||||
import { ProductsService } from 'app/products/products.service';
|
||||
|
||||
/** Custom Components */
|
||||
import { FixedDepositAccountDetailsStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component';
|
||||
import { FixedDepositAccountTermsStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-terms-step/fixed-deposit-account-terms-step.component';
|
||||
import { FixedDepositAccountSettingsStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-settings-step/fixed-deposit-account-settings-step.component';
|
||||
import { FixedDepositAccountChargesStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-charges-step/fixed-deposit-account-charges-step.component';
|
||||
import { FixedDepositAccountCurrencyStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-currency-step/fixed-deposit-account-currency-step.component';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { Currency } from 'app/shared/models/general.model';
|
||||
import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { FixedDepositAccountInterestRateChartStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component';
|
||||
import { FixedDepositAccountPreviewStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/**
|
||||
* Edit Fixed Deposit Account Component
|
||||
* Edit Fixed Deposit Account Component — same tab UI as Create, pre-populated with existing data.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-edit-fixed-deposit-account',
|
||||
@@ -36,16 +36,11 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
styleUrls: ['./edit-fixed-deposit-account.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatStepper,
|
||||
MatStepperIcon,
|
||||
MatTabsModule,
|
||||
FaIconComponent,
|
||||
MatStep,
|
||||
MatStepLabel,
|
||||
FixedDepositAccountDetailsStepComponent,
|
||||
FixedDepositAccountTermsStepComponent,
|
||||
FixedDepositAccountSettingsStepComponent,
|
||||
FixedDepositAccountInterestRateChartStepComponent,
|
||||
FixedDepositAccountChargesStepComponent,
|
||||
FixedDepositAccountCurrencyStepComponent,
|
||||
FixedDepositAccountPreviewStepComponent
|
||||
]
|
||||
})
|
||||
@@ -55,110 +50,201 @@ export class EditFixedDepositAccountComponent {
|
||||
private dateUtils = inject(Dates);
|
||||
private fixedDepositsService = inject(FixedDepositsService);
|
||||
private settingsService = inject(SettingsService);
|
||||
private alertService = inject(AlertService);
|
||||
private productsService = inject(ProductsService);
|
||||
private clientsService = inject(ClientsService);
|
||||
|
||||
/** Fixed Deposits Account Details Step */
|
||||
@ViewChild(FixedDepositAccountDetailsStepComponent, { static: true })
|
||||
fixedDepositsAccountDetailsStep: FixedDepositAccountDetailsStepComponent;
|
||||
/** Fixed Deposits Account Terms Step */
|
||||
@ViewChild(FixedDepositAccountTermsStepComponent, { static: true })
|
||||
fixedDepositAccountTermsStep: FixedDepositAccountTermsStepComponent;
|
||||
/** Fixed Deposits Account Settings Step */
|
||||
@ViewChild(FixedDepositAccountSettingsStepComponent, { static: true })
|
||||
fixedDepositAccountSettingsStep: FixedDepositAccountSettingsStepComponent;
|
||||
/** Fixed Deposits Account Charges Step */
|
||||
@ViewChild(FixedDepositAccountChargesStepComponent, { static: true })
|
||||
fixedDepositAccountChargesStep: FixedDepositAccountChargesStepComponent;
|
||||
@ViewChild(FixedDepositAccountCurrencyStepComponent)
|
||||
fixedDepositAccountCurrencyStep: FixedDepositAccountCurrencyStepComponent;
|
||||
|
||||
/** Fixed Deposits Account Template */
|
||||
/** Full account data + template from resolver */
|
||||
fixedDepositsAccountAndTemplate: any;
|
||||
/** Fixed Deposit Account Product Template */
|
||||
/** Product template emitted by details step */
|
||||
fixedDepositsAccountProductTemplate: any;
|
||||
/** Currently visible wizard tab (0-indexed). */
|
||||
selectedTabIndex = 0;
|
||||
private readonly totalTabs = 4;
|
||||
/** Active CBS base interest rate versions for the selected product's FD type */
|
||||
cbsBaseRates: any[] = [];
|
||||
/** Tenure selected from the interest rate chart step */
|
||||
selectedTenure: {
|
||||
depositPeriod: number;
|
||||
depositPeriodFrequencyId: number;
|
||||
depositAmount?: number;
|
||||
maturityAmount?: number;
|
||||
interestEarned?: number;
|
||||
appliedRate?: number;
|
||||
isSeniorCitizen?: boolean;
|
||||
} | null = null;
|
||||
|
||||
currency: Currency;
|
||||
|
||||
/**
|
||||
* Fetches FD account template from `resolve`
|
||||
* @param {ActivatedRoute} route Activated Route
|
||||
* @param {Router} router Router
|
||||
* @param {Dates} dateUtils Date Utils
|
||||
* @param {FixedDepositsService} fixedDepositsService Fixed Deposits Service
|
||||
* @param {SettingsService} settingsService Settings Service
|
||||
*/
|
||||
constructor() {
|
||||
this.route.data.subscribe((data: { fixedDepositsAccountAndTemplate: any }) => {
|
||||
this.fixedDepositsAccountAndTemplate = data.fixedDepositsAccountAndTemplate;
|
||||
const account = data.fixedDepositsAccountAndTemplate;
|
||||
this.fixedDepositsAccountAndTemplate = account;
|
||||
// Pre-populate tenure from existing account
|
||||
if (account?.depositPeriod) {
|
||||
this.selectedTenure = {
|
||||
depositPeriod: account.depositPeriod,
|
||||
depositPeriodFrequencyId: account.depositPeriodFrequencyType?.id ?? 2,
|
||||
depositAmount: account.depositAmount,
|
||||
isSeniorCitizen: !!account.isSeniorCitizen,
|
||||
appliedRate: account.seniorCitizenInterestRate ?? undefined
|
||||
};
|
||||
}
|
||||
if (account?.clientId && account?.accountNo) {
|
||||
this.clientsService
|
||||
.getClientNominees(String(account.clientId), account.accountNo, 'FD')
|
||||
.subscribe((nominees: any[]) => {
|
||||
if (Array.isArray(nominees) && nominees.length > 0) {
|
||||
this.fixedDepositsAccountAndTemplate = { ...this.fixedDepositsAccountAndTemplate, nominees };
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets fixed deposits account product template.
|
||||
* @param {any} $event API response
|
||||
*/
|
||||
next(): void {
|
||||
if (this.selectedTabIndex < this.totalTabs - 1) {
|
||||
this.selectedTabIndex += 1;
|
||||
}
|
||||
}
|
||||
|
||||
prev(): void {
|
||||
if (this.selectedTabIndex > 0) {
|
||||
this.selectedTabIndex -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
setTemplate($event: any) {
|
||||
this.fixedDepositsAccountProductTemplate = $event;
|
||||
this.currency = this.fixedDepositsAccountAndTemplate.currency;
|
||||
const fdType = $event?.fdType;
|
||||
this.productsService.getFdBaseInterestRates(fdType).subscribe({
|
||||
next: (rates: any[]) => (this.cbsBaseRates = rates || []),
|
||||
error: () => (this.cbsBaseRates = [])
|
||||
});
|
||||
}
|
||||
|
||||
onTenureSelected($event: {
|
||||
depositPeriod: number;
|
||||
depositPeriodFrequencyId: number;
|
||||
depositAmount?: number;
|
||||
maturityAmount?: number;
|
||||
interestEarned?: number;
|
||||
appliedRate?: number;
|
||||
isSeniorCitizen?: boolean;
|
||||
}) {
|
||||
this.selectedTenure = $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves Fixed Deposit Account Details Form Data
|
||||
*/
|
||||
get fixedDepositAccountDetailsForm() {
|
||||
return this.fixedDepositsAccountDetailsStep.fixedDepositAccountDetailsForm;
|
||||
return this.fixedDepositsAccountDetailsStep?.fixedDepositAccountDetailsForm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves Fixed Deposit Account Terms Form Data
|
||||
*/
|
||||
get fixedDepositAccountTermsForm() {
|
||||
return this.fixedDepositAccountTermsStep.fixedDepositAccountTermsForm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves Fixed Deposit Account Settings Form Data
|
||||
*/
|
||||
get fixedDepositAccountSettingsForm() {
|
||||
return this.fixedDepositAccountSettingsStep.fixedDepositAccountSettingsForm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if stepper is valid and not pristine.
|
||||
*/
|
||||
get fixedDepositAccountFormValidAndNotPristine() {
|
||||
return (
|
||||
this.fixedDepositAccountDetailsForm.valid &&
|
||||
this.fixedDepositAccountTermsForm.valid &&
|
||||
this.fixedDepositAccountSettingsForm.valid &&
|
||||
(!this.fixedDepositAccountDetailsForm.pristine ||
|
||||
!this.fixedDepositAccountTermsForm.pristine ||
|
||||
!this.fixedDepositAccountSettingsForm.pristine ||
|
||||
!this.fixedDepositAccountChargesStep.pristine)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the fixed deposit account object.
|
||||
*/
|
||||
get fixedDepositAccount() {
|
||||
return {
|
||||
...this.fixedDepositsAccountDetailsStep.fixedDepositAccountDetails,
|
||||
...this.fixedDepositAccountTermsStep.fixedDepositAccountTerms,
|
||||
...this.fixedDepositAccountSettingsStep.fixedDepositAccountSettings,
|
||||
...this.fixedDepositAccountChargesStep?.fixedDepositAccountCharges
|
||||
...this.fixedDepositsAccountDetailsStep?.fixedDepositAccountDetails,
|
||||
...this.fixedDepositAccountCurrencyStep?.currencyStepData
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the fixed deposit form and creates a new fixed deposit account
|
||||
*/
|
||||
get submittedOnDate(): Date | null {
|
||||
const val =
|
||||
this.fixedDepositsAccountDetailsStep?.fixedDepositAccountDetails?.submittedOnDate ??
|
||||
this.fixedDepositsAccountAndTemplate?.timeline?.submittedOnDate;
|
||||
if (!val) return null;
|
||||
const d = Array.isArray(val) ? new Date(val[0], val[1] - 1, val[2]) : new Date(val);
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
|
||||
submit() {
|
||||
const detailsStep = this.fixedDepositsAccountDetailsStep;
|
||||
if (detailsStep?.selectedAccountStatus === 'JOINT' && detailsStep.totalJointSharePercentage !== 100) {
|
||||
this.alertService.alert({
|
||||
type: 'Error',
|
||||
message: `Total ownership is ${detailsStep.totalJointSharePercentage}%. It must equal 100% before submitting.`
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this.fixedDepositsAccountProductTemplate?.nominationFacility &&
|
||||
(this.fixedDepositAccountCurrencyStep?.nominees ?? []).length === 0
|
||||
) {
|
||||
this.alertService.alert({
|
||||
type: 'Error',
|
||||
message: 'Nomination Facility is enabled for this product. Please add at least one nominee before submitting.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!this.selectedTenure || !this.selectedTenure.depositPeriod) {
|
||||
this.alertService.alert({
|
||||
type: 'Error',
|
||||
message: 'Please enter the Deposit Tenure and click "Find Rate" before submitting.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!detailsStep?.selectedCustomerSegment) {
|
||||
this.alertService.alert({ type: 'Error', message: 'Please select a Customer Segment before submitting.' });
|
||||
return;
|
||||
}
|
||||
const locale = this.settingsService.language.code;
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
const monthDayFormat = 'dd MMMM';
|
||||
const fixedDepositAccount = {
|
||||
const pt = this.fixedDepositsAccountProductTemplate;
|
||||
|
||||
const terms: any = {};
|
||||
if (pt?.nominalAnnualInterestRate != null) terms.nominalAnnualInterestRate = pt.nominalAnnualInterestRate;
|
||||
if (pt?.interestCompoundingPeriodType?.id != null)
|
||||
terms.interestCompoundingPeriodType = pt.interestCompoundingPeriodType.id;
|
||||
if (pt?.interestPostingPeriodType?.id != null) terms.interestPostingPeriodType = pt.interestPostingPeriodType.id;
|
||||
if (pt?.interestCalculationType?.id != null) terms.interestCalculationType = pt.interestCalculationType.id;
|
||||
if (pt?.interestCalculationDaysInYearType?.id != null)
|
||||
terms.interestCalculationDaysInYearType = pt.interestCalculationDaysInYearType.id;
|
||||
|
||||
if (this.selectedTenure?.depositAmount != null) {
|
||||
terms.depositAmount = this.selectedTenure.depositAmount;
|
||||
} else if (pt?.depositAmount != null) {
|
||||
terms.depositAmount = pt.depositAmount;
|
||||
}
|
||||
|
||||
if (pt?.minDepositAmount != null) terms.minDepositAmount = pt.minDepositAmount;
|
||||
if (pt?.maxDepositAmount != null) terms.maxDepositAmount = pt.maxDepositAmount;
|
||||
|
||||
const settings: any = {};
|
||||
if (pt?.lockinPeriodFrequency != null) settings.lockinPeriodFrequency = pt.lockinPeriodFrequency;
|
||||
if (pt?.lockinPeriodFrequencyType?.id != null) settings.lockinPeriodFrequencyType = pt.lockinPeriodFrequencyType.id;
|
||||
if (pt?.minDepositTerm != null) settings.minDepositTerm = pt.minDepositTerm;
|
||||
if (pt?.minDepositTermType?.id != null) settings.minDepositTermTypeId = pt.minDepositTermType.id;
|
||||
if (pt?.inMultiplesOfDepositTerm != null) settings.inMultiplesOfDepositTerm = pt.inMultiplesOfDepositTerm;
|
||||
if (pt?.inMultiplesOfDepositTermType?.id != null)
|
||||
settings.inMultiplesOfDepositTermTypeId = pt.inMultiplesOfDepositTermType.id;
|
||||
if (pt?.maxDepositTerm != null) settings.maxDepositTerm = pt.maxDepositTerm;
|
||||
if (pt?.maxDepositTermType?.id != null) settings.maxDepositTermTypeId = pt.maxDepositTermType.id;
|
||||
if (pt?.preClosurePenalApplicable != null) settings.preClosurePenalApplicable = pt.preClosurePenalApplicable;
|
||||
if (pt?.preClosurePenalInterest != null) settings.preClosurePenalInterest = pt.preClosurePenalInterest;
|
||||
if (pt?.preClosurePenalInterestOnType?.id != null)
|
||||
settings.preClosurePenalInterestOnTypeId = pt.preClosurePenalInterestOnType.id;
|
||||
if (pt?.maturityInstructionId != null) settings.maturityInstructionId = pt.maturityInstructionId;
|
||||
if (pt?.transferInterestToSavings != null) settings.transferInterestToSavings = pt.transferInterestToSavings;
|
||||
if (pt?.withHoldTax != null) settings.withHoldTax = pt.withHoldTax;
|
||||
|
||||
const rawCharges: any[] =
|
||||
(this.fixedDepositAccount.charges?.length ? this.fixedDepositAccount.charges : pt?.charges) ?? [];
|
||||
|
||||
const fixedDepositAccount: any = {
|
||||
...this.fixedDepositAccount,
|
||||
...terms,
|
||||
...settings,
|
||||
...(this.selectedTenure
|
||||
? {
|
||||
depositPeriod: this.selectedTenure.depositPeriod,
|
||||
depositPeriodFrequencyId: this.selectedTenure.depositPeriodFrequencyId
|
||||
}
|
||||
: {}),
|
||||
clientId: this.fixedDepositsAccountAndTemplate.clientId,
|
||||
charges: this.fixedDepositAccount.charges.map((charge: any) => ({
|
||||
chargeId: charge.id,
|
||||
charges: rawCharges.map((charge: any) => ({
|
||||
chargeId: charge.chargeId ?? charge.id,
|
||||
amount: charge.amount,
|
||||
dueDate: charge.dueDate && this.dateUtils.formatDate(charge.dueDate, dateFormat),
|
||||
feeOnMonthDay:
|
||||
@@ -166,11 +252,35 @@ export class EditFixedDepositAccountComponent {
|
||||
feeInterval: charge.feeInterval
|
||||
})),
|
||||
submittedOnDate: this.dateUtils.formatDate(this.fixedDepositAccount.submittedOnDate, dateFormat),
|
||||
charts: [{ chartSlabs: this.fixedDepositsAccountProductTemplate.accountChart.chartSlabs }],
|
||||
charts: [{ chartSlabs: pt?.accountChart?.chartSlabs ?? [] }],
|
||||
dateFormat,
|
||||
monthDayFormat,
|
||||
locale
|
||||
};
|
||||
|
||||
const nominees = this.fixedDepositAccountCurrencyStep?.nominees ?? [];
|
||||
if (nominees.length > 0) {
|
||||
fixedDepositAccount.nominees = nominees.map((n: any) => ({
|
||||
nomineeClientId: n.nomineeClientId,
|
||||
nominationType: n.nominationType,
|
||||
relationshipId: n.relationshipId,
|
||||
percentageShare: n.percentageShare,
|
||||
nomineeIsMinor: n.nomineeIsMinor,
|
||||
...(n.guardianClientId ? { guardianClientId: n.guardianClientId } : {}),
|
||||
remarks: n.remarks
|
||||
}));
|
||||
}
|
||||
|
||||
if (
|
||||
this.selectedTenure?.isSeniorCitizen &&
|
||||
this.selectedTenure.appliedRate != null &&
|
||||
this.selectedTenure.appliedRate > 0
|
||||
) {
|
||||
fixedDepositAccount.isSeniorCitizen = true;
|
||||
fixedDepositAccount.seniorCitizenInterestRate = this.selectedTenure.appliedRate;
|
||||
fixedDepositAccount.nominalAnnualInterestRate = this.selectedTenure.appliedRate;
|
||||
}
|
||||
|
||||
this.fixedDepositsService
|
||||
.updateFixedDepositAccount(this.fixedDepositsAccountAndTemplate.id, fixedDepositAccount)
|
||||
.subscribe((response: any) => {
|
||||
|
||||
+14
-1
@@ -28,6 +28,7 @@ import { FormsModule } from '@angular/forms';
|
||||
export class FixedDepositAccountCurrencyStepComponent implements OnChanges {
|
||||
@Input() fixedDepositsAccountProductTemplate: any;
|
||||
@Input() primaryClientId: number | null = null;
|
||||
@Input() existingAccount: any = null;
|
||||
@ViewChild(NomineeFormComponent) nomineeFormComp: NomineeFormComponent;
|
||||
|
||||
nominees: any[] = [];
|
||||
@@ -38,8 +39,20 @@ export class FixedDepositAccountCurrencyStepComponent implements OnChanges {
|
||||
autoDebitFacility = false;
|
||||
nominationFacility = false;
|
||||
|
||||
private existingAccountApplied = false;
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['fixedDepositsAccountProductTemplate']?.currentValue) {
|
||||
if (changes['existingAccount']?.currentValue) {
|
||||
const ea = changes['existingAccount'].currentValue;
|
||||
this.existingAccountApplied = true;
|
||||
if (ea.autoDebitFacility !== undefined) {
|
||||
this.autoDebitFacility = !!ea.autoDebitFacility;
|
||||
}
|
||||
if (Array.isArray(ea.nominees) && ea.nominees.length > 0) {
|
||||
this.nominees = [...ea.nominees];
|
||||
}
|
||||
}
|
||||
if (changes['fixedDepositsAccountProductTemplate']?.currentValue && !this.existingAccountApplied) {
|
||||
const pt = changes['fixedDepositsAccountProductTemplate'].currentValue;
|
||||
this.autoDebitFacility = !!pt.autoDebitFacility;
|
||||
this.nominationFacility = !!pt.nominationFacility;
|
||||
|
||||
+1
-1
@@ -101,7 +101,7 @@
|
||||
<mat-label>Account No</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[value]="productTemplate?.previewAccountNo || fixedDepositsAccountTemplate?.accountNo || 'Auto-generated'"
|
||||
[value]="fixedDepositsAccountTemplate?.accountNo || productTemplate?.previewAccountNo || 'Auto-generated'"
|
||||
readonly
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
+118
-2
@@ -71,6 +71,7 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit {
|
||||
fixedDepositAccountDetailsForm!: UntypedFormGroup;
|
||||
|
||||
isProductSelected = false;
|
||||
private isEditMode = false;
|
||||
|
||||
/** Fixed Deposits Account Template with product data */
|
||||
@Output() fixedDepositsAccountProductTemplate = new EventEmitter();
|
||||
@@ -248,6 +249,20 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit {
|
||||
currencyCode: a.currency?.code ?? ''
|
||||
}));
|
||||
this.dalLoading = false;
|
||||
// Pre-select linked account in edit mode
|
||||
const linkedId =
|
||||
this.fixedDepositsAccountTemplate?.linkedAccount?.id ?? this.fixedDepositsAccountTemplate?.linkedAccountId;
|
||||
if (linkedId) {
|
||||
const savings = this.dalAllSavings.find((a: any) => a.id === linkedId);
|
||||
const current = this.dalAllCurrent.find((a: any) => a.id === linkedId);
|
||||
if (savings) {
|
||||
this.dalSelectedType = 'SAVINGS';
|
||||
this.selectedLinkedAccount = savings;
|
||||
} else if (current) {
|
||||
this.dalSelectedType = 'CURRENT';
|
||||
this.selectedLinkedAccount = current;
|
||||
}
|
||||
}
|
||||
this.cdr.detectChanges();
|
||||
},
|
||||
error: () => {
|
||||
@@ -265,9 +280,27 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit {
|
||||
this.buildDependencies();
|
||||
if (this.fixedDepositsAccountTemplate) {
|
||||
this.productData = this.fixedDepositsAccountTemplate.productOptions;
|
||||
if (this.fixedDepositsAccountTemplate.depositProductId) {
|
||||
const existingProductId =
|
||||
this.fixedDepositsAccountTemplate.depositProductId ?? this.fixedDepositsAccountTemplate.productId;
|
||||
// Edit mode: account response has accountNo; pure template has previewAccountNo but no accountNo
|
||||
if (this.fixedDepositsAccountTemplate.accountNo) {
|
||||
this.isEditMode = true;
|
||||
}
|
||||
if ((!this.productData || this.productData.length === 0) && existingProductId) {
|
||||
this.isEditMode = true;
|
||||
this.productData = [
|
||||
{
|
||||
id: existingProductId,
|
||||
name:
|
||||
this.fixedDepositsAccountTemplate.productName ??
|
||||
this.fixedDepositsAccountTemplate.depositProductName ??
|
||||
'Product'
|
||||
}
|
||||
];
|
||||
}
|
||||
if (existingProductId) {
|
||||
this.fixedDepositAccountDetailsForm.patchValue({
|
||||
productId: this.fixedDepositsAccountTemplate.depositProductId,
|
||||
productId: existingProductId,
|
||||
submittedOnDate:
|
||||
this.fixedDepositsAccountTemplate.timeline?.submittedOnDate &&
|
||||
new Date(this.fixedDepositsAccountTemplate.timeline.submittedOnDate),
|
||||
@@ -361,6 +394,9 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit {
|
||||
}
|
||||
|
||||
private autoDetectAccountStatus(): void {
|
||||
if (this.isEditMode) {
|
||||
return;
|
||||
}
|
||||
if (this.clientAge === null) return;
|
||||
if (this.clientAge < 18) {
|
||||
this.selectedAccountStatus = 'MINOR';
|
||||
@@ -430,10 +466,90 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit {
|
||||
} else {
|
||||
this.fixedDepositAccountDetailsForm.get('fieldOfficerId')!.patchValue('');
|
||||
}
|
||||
// === Restore existing account values when in edit/modify mode ===
|
||||
this.restoreExistingAccountValues();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private restoreExistingAccountValues(): void {
|
||||
const ea = this.fixedDepositsAccountTemplate;
|
||||
if (!ea?.depositProductId && !ea?.productId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const strVal = (v: any): string => (typeof v === 'object' && v !== null ? (v.value ?? '') : (v ?? ''));
|
||||
|
||||
const accountStatus = strVal(ea.accountStatus);
|
||||
if (accountStatus) {
|
||||
this.selectedAccountStatus = accountStatus;
|
||||
}
|
||||
|
||||
const customerSegment = strVal(ea.customerSegment);
|
||||
if (customerSegment) {
|
||||
this.selectedCustomerSegment = customerSegment;
|
||||
}
|
||||
|
||||
// FD API returns cbs_account_type aliased as "accountType" (string); Current/Savings return it as "cbsAccountType"
|
||||
const rawType = ea.cbsAccountType ?? (typeof ea.accountType === 'string' ? ea.accountType : null);
|
||||
const accountType = rawType ? strVal(rawType) : null;
|
||||
if (accountType) {
|
||||
this.selectedAccountType = accountType;
|
||||
}
|
||||
|
||||
if (accountStatus === 'JOINT') {
|
||||
const modeOfOp = strVal(ea.modeOfOperation);
|
||||
if (modeOfOp) {
|
||||
this.modeOfOperation = modeOfOp;
|
||||
}
|
||||
if (Array.isArray(ea.jointHolders) && ea.jointHolders.length > 0) {
|
||||
// Support both camelCase (fixed) and lowercase (MySQL queryForList) key formats
|
||||
const isPrimary = (h: any) => h.isPrimary ?? h.isprimary ?? false;
|
||||
const primary = ea.jointHolders.find((h: any) => isPrimary(h));
|
||||
const primaryShare = primary?.sharePercentage ?? primary?.sharepercentage;
|
||||
if (primaryShare) {
|
||||
this.primaryHolderShare = +primaryShare;
|
||||
}
|
||||
this.jointHolders = ea.jointHolders
|
||||
.filter((h: any) => !isPrimary(h))
|
||||
.map((h: any) => ({
|
||||
clientId: h.clientId ?? h.clientid,
|
||||
clientName: h.displayName ?? h.displayname ?? h.clientName ?? '',
|
||||
clientCif: h.cifNumber ?? h.cifnumber ?? h.clientCif ?? '',
|
||||
sharePercentage: +(h.sharePercentage ?? h.sharepercentage ?? 0),
|
||||
isMandatory: !!(h.isMandatory ?? h.ismandatory)
|
||||
}));
|
||||
}
|
||||
}
|
||||
if (accountStatus === 'INDIVIDUAL_MALE' && ea.specialCategory) {
|
||||
this.selectedSpecialCategory = ea.specialCategory;
|
||||
}
|
||||
if (accountStatus === 'INDIVIDUAL_FEMALE' && ea.weakerSection) {
|
||||
this.selectedWeakerSection = ea.weakerSection;
|
||||
}
|
||||
if (accountStatus === 'MINOR') {
|
||||
if (ea.guardianClientId) {
|
||||
this.guardianClientId = ea.guardianClientId;
|
||||
}
|
||||
if (ea.guardianName) {
|
||||
this.guardianName = ea.guardianName;
|
||||
}
|
||||
if (ea.guardianCifNumber) {
|
||||
this.guardianCif = ea.guardianCifNumber;
|
||||
}
|
||||
if (ea.guardianRelationship) {
|
||||
this.guardianRelationship = ea.guardianRelationship;
|
||||
}
|
||||
if (ea.minorDateOfBirth) {
|
||||
const d = new Date(ea.minorDateOfBirth);
|
||||
if (!isNaN(d.getTime())) {
|
||||
this.minorDob = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
|
||||
hasValue(val: any): boolean {
|
||||
return val !== null && val !== undefined && val !== '';
|
||||
}
|
||||
|
||||
+154
-34
@@ -6,7 +6,7 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component, Input, OnChanges, Output, EventEmitter, ViewChild } from '@angular/core';
|
||||
import { Component, Input, OnChanges, SimpleChanges, Output, EventEmitter, ViewChild, inject } from '@angular/core';
|
||||
import {
|
||||
MatTableDataSource,
|
||||
MatTable,
|
||||
@@ -25,6 +25,7 @@ import { NgIf, NgSwitch, TitleCasePipe, DecimalPipe } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { DateFormatPipe } from '../../../../pipes/date-format.pipe';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { FixedDepositsService } from '../../fixed-deposits.service';
|
||||
|
||||
export type FdPeriodUnit = 'Days' | 'Months' | 'Years';
|
||||
|
||||
@@ -69,10 +70,22 @@ export interface SlabMatchResult {
|
||||
]
|
||||
})
|
||||
export class FixedDepositAccountInterestRateChartStepComponent implements OnChanges {
|
||||
private fixedDepositsService = inject(FixedDepositsService);
|
||||
|
||||
@Input() fixedDepositsAccountTemplate: any;
|
||||
@Input() fixedDepositsAccountProductTemplate: any;
|
||||
/** Active CBS base interest rate versions — when provided, overrides the product chart slabs */
|
||||
@Input() cbsBaseRates: any[] = [];
|
||||
/** Pre-populate tenure from existing account (edit mode) */
|
||||
@Input() initialTenure: {
|
||||
depositPeriod: number;
|
||||
depositPeriodFrequencyId: number;
|
||||
depositAmount?: number;
|
||||
isSeniorCitizen?: boolean;
|
||||
appliedRate?: number;
|
||||
} | null = null;
|
||||
/** Submission date for exact calculation (passed from create/edit form) */
|
||||
@Input() submittedOnDate: Date | null = null;
|
||||
|
||||
activeBase: any = null;
|
||||
|
||||
@@ -157,8 +170,10 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan
|
||||
return this.fdType === 'SPECIAL';
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
if (this.fixedDepositsAccountProductTemplate) {
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
// Only reset/re-apply when TEMPLATE or cbsBaseRates changes, NOT when initialTenure/submittedOnDate changes alone
|
||||
const templateChanged = changes['fixedDepositsAccountProductTemplate'] || changes['cbsBaseRates'];
|
||||
if (templateChanged && this.fixedDepositsAccountProductTemplate) {
|
||||
const fdType = this.fixedDepositsAccountProductTemplate?.fdType;
|
||||
this.activeBase = (this.cbsBaseRates || []).find((r) => r.fdType === fdType && r.isActive);
|
||||
this.interestRateChartData = this.activeBase?.slabs
|
||||
@@ -169,9 +184,36 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan
|
||||
}
|
||||
this.resetCalculator();
|
||||
this.tenurePeriodUnit = this.allowedPeriodUnits[0];
|
||||
// Pre-fill tenure in edit mode
|
||||
this.applyInitialTenure();
|
||||
}
|
||||
}
|
||||
|
||||
private applyInitialTenure(): void {
|
||||
if (!this.initialTenure?.depositPeriod) {
|
||||
return;
|
||||
}
|
||||
// Map frequency ID to period unit: Days=0, Weeks=1, Months=2, Years=3
|
||||
const freqId = this.initialTenure.depositPeriodFrequencyId;
|
||||
const unitMap: Record<number, FdPeriodUnit> = { 0: 'Days', 1: 'Days', 2: 'Months', 3: 'Years' };
|
||||
const unit: FdPeriodUnit = unitMap[freqId] ?? 'Months';
|
||||
if (this.allowedPeriodUnits.includes(unit)) {
|
||||
this.tenurePeriodUnit = unit;
|
||||
} else {
|
||||
this.tenurePeriodUnit = this.allowedPeriodUnits[0];
|
||||
}
|
||||
this.tenureValue = this.initialTenure.depositPeriod;
|
||||
if (this.initialTenure.depositAmount) {
|
||||
this.principalAmount = this.initialTenure.depositAmount;
|
||||
}
|
||||
// Pre-check senior citizen if it was set during create
|
||||
if (this.initialTenure.isSeniorCitizen) {
|
||||
this.isSeniorCitizen = true;
|
||||
}
|
||||
// Auto-calculate when tenure is pre-filled (edit mode)
|
||||
setTimeout(() => this.calculateRate(), 0);
|
||||
}
|
||||
|
||||
private normalizeCbsSlabs(slabs: any[]): any[] {
|
||||
return (slabs || []).map((s) => ({
|
||||
...s,
|
||||
@@ -257,38 +299,116 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan
|
||||
tenureDays: enteredDays
|
||||
};
|
||||
|
||||
if (this.principalAmount && this.principalAmount > 0) {
|
||||
const p = this.principalAmount;
|
||||
const r = rate / 100;
|
||||
const years = enteredDays / 365;
|
||||
const n = 4; // Quarterly compounding
|
||||
|
||||
if (this.isSpecial) {
|
||||
// Monthly payout: simple interest paid monthly, principal returned at maturity
|
||||
result.estimatedMonthlyPayout = (p * rate) / 1200;
|
||||
result.interestEarned = result.estimatedMonthlyPayout * (enteredDays / 30);
|
||||
result.maturityAmount = p; // principal returned at maturity
|
||||
} else {
|
||||
// Compound interest (Standard Quarterly)
|
||||
result.maturityAmount = p * Math.pow(1 + r / n, n * years);
|
||||
result.interestEarned = result.maturityAmount - p;
|
||||
}
|
||||
}
|
||||
|
||||
this.matchResult = result;
|
||||
|
||||
// Emit selected tenure and calculated maturity to parent
|
||||
this.matchResult = result; // Show slab match immediately, maturity updates async if principal entered
|
||||
const frequencyId = this.resolveFrequencyId(this.tenurePeriodUnit);
|
||||
if (frequencyId !== null) {
|
||||
this.tenureSelected.emit({
|
||||
depositPeriod: this.tenureValue,
|
||||
depositPeriodFrequencyId: frequencyId,
|
||||
depositAmount: this.principalAmount || undefined,
|
||||
maturityAmount: result.maturityAmount,
|
||||
interestEarned: result.interestEarned,
|
||||
appliedRate: result.appliedRate,
|
||||
isSeniorCitizen: this.isSeniorCitizen
|
||||
});
|
||||
|
||||
if (this.principalAmount && this.principalAmount > 0) {
|
||||
// Convert tenure to months for backend API
|
||||
let tenureInMonths = 0;
|
||||
if (this.tenurePeriodUnit === 'Years') {
|
||||
tenureInMonths = this.tenureValue * 12;
|
||||
} else if (this.tenurePeriodUnit === 'Months') {
|
||||
tenureInMonths = this.tenureValue;
|
||||
} else {
|
||||
tenureInMonths = Math.floor(enteredDays / 30);
|
||||
}
|
||||
|
||||
// Map compounding period type id → months (0=Daily, 1=Daily, 4=Monthly, 5=Quarterly, 6=SemiAnnual, 7=Annual)
|
||||
const compMap: Record<number, number> = { 1: 0, 4: 1, 5: 3, 6: 6, 7: 12 };
|
||||
const compId = this.fixedDepositsAccountProductTemplate?.interestCompoundingPeriodType?.id ?? 4;
|
||||
const compMonths = compMap[compId] ?? 1;
|
||||
|
||||
// Map posting period type id → months
|
||||
const postId = this.fixedDepositsAccountProductTemplate?.interestPostingPeriodType?.id ?? 4;
|
||||
const postMonths = compMap[postId] ?? 1;
|
||||
|
||||
const emitResult = () => {
|
||||
if (frequencyId !== null) {
|
||||
this.tenureSelected.emit({
|
||||
depositPeriod: this.tenureValue,
|
||||
depositPeriodFrequencyId: frequencyId,
|
||||
depositAmount: this.principalAmount || undefined,
|
||||
maturityAmount: result.maturityAmount,
|
||||
interestEarned: result.interestEarned,
|
||||
appliedRate: result.appliedRate,
|
||||
isSeniorCitizen: this.isSeniorCitizen
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get submittedOnDate — Input takes priority over template data
|
||||
let submittedOnDateStr: string;
|
||||
if (this.submittedOnDate) {
|
||||
submittedOnDateStr = this.submittedOnDate.toISOString().split('T')[0];
|
||||
} else {
|
||||
const submittedOnDateRaw =
|
||||
this.fixedDepositsAccountTemplate?.timeline?.submittedOnDate ??
|
||||
this.fixedDepositsAccountProductTemplate?.timeline?.submittedOnDate;
|
||||
if (submittedOnDateRaw) {
|
||||
const d = Array.isArray(submittedOnDateRaw)
|
||||
? new Date(submittedOnDateRaw[0], submittedOnDateRaw[1] - 1, submittedOnDateRaw[2])
|
||||
: new Date(submittedOnDateRaw);
|
||||
submittedOnDateStr = !isNaN(d.getTime())
|
||||
? d.toISOString().split('T')[0]
|
||||
: new Date().toISOString().split('T')[0];
|
||||
} else {
|
||||
submittedOnDateStr = new Date().toISOString().split('T')[0];
|
||||
}
|
||||
}
|
||||
const daysInYearVal =
|
||||
this.fixedDepositsAccountProductTemplate?.interestCalculationDaysInYearType?.id === 360 ? 360 : 365;
|
||||
|
||||
// Always use backend API for accurate calculation (including daily compounding with 0)
|
||||
this.fixedDepositsService
|
||||
.calculateFixedDepositInterest({
|
||||
principalAmount: this.principalAmount,
|
||||
annualInterestRate: rate,
|
||||
tenureInMonths,
|
||||
interestCompoundingPeriodInMonths: compMonths, // 0 = daily, now supported by backend
|
||||
interestPostingPeriodInMonths: postMonths,
|
||||
submittedOnDate: submittedOnDateStr,
|
||||
daysInYear: daysInYearVal
|
||||
})
|
||||
.subscribe({
|
||||
next: (apiResult: any) => {
|
||||
result.maturityAmount = apiResult.maturityAmount;
|
||||
result.interestEarned =
|
||||
apiResult.totalInterest ?? Math.round((apiResult.maturityAmount - this.principalAmount) * 100) / 100;
|
||||
this.matchResult = { ...result };
|
||||
emitResult();
|
||||
},
|
||||
error: () => {
|
||||
// Fallback if API fails: frontend formula
|
||||
const r = rate / 100;
|
||||
if (compMonths === 0) {
|
||||
// Daily compounding fallback
|
||||
result.maturityAmount =
|
||||
Math.round(this.principalAmount * Math.pow(1 + r / daysInYearVal, enteredDays) * 100) / 100;
|
||||
} else {
|
||||
const n = 12 / compMonths;
|
||||
const totalCompoundings = Math.floor(tenureInMonths / compMonths);
|
||||
const rPerCompounding = rate / (n * 100);
|
||||
result.maturityAmount =
|
||||
Math.round(this.principalAmount * Math.pow(1 + rPerCompounding, totalCompoundings) * 100) / 100;
|
||||
}
|
||||
result.interestEarned = Math.round((result.maturityAmount - this.principalAmount) * 100) / 100;
|
||||
this.matchResult = { ...result };
|
||||
emitResult();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.matchResult = result;
|
||||
if (frequencyId !== null) {
|
||||
this.tenureSelected.emit({
|
||||
depositPeriod: this.tenureValue,
|
||||
depositPeriodFrequencyId: frequencyId,
|
||||
depositAmount: undefined,
|
||||
maturityAmount: undefined,
|
||||
interestEarned: undefined,
|
||||
appliedRate: result.appliedRate,
|
||||
isSeniorCitizen: this.isSeniorCitizen
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
@@ -92,6 +92,15 @@ export class FixedDepositsButtonsConfiguration {
|
||||
}
|
||||
];
|
||||
break;
|
||||
case 'Rejected':
|
||||
case 'rejected':
|
||||
this.buttonsArray = [
|
||||
{
|
||||
name: 'Modify Application',
|
||||
icon: 'fa fa-pencil'
|
||||
}
|
||||
];
|
||||
break;
|
||||
default:
|
||||
this.buttonsArray = [];
|
||||
}
|
||||
|
||||
@@ -114,6 +114,34 @@ export class FixedDepositsService {
|
||||
return this.http.get(`/fixeddepositaccounts/${accountId}`, { params: httpParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate FD maturity using backend formula.
|
||||
* When submittedOnDate + daysInYear are provided, uses exact calendar-based calculation
|
||||
* matching the actual account creation engine.
|
||||
*/
|
||||
calculateFixedDepositInterest(params: {
|
||||
principalAmount: number;
|
||||
annualInterestRate: number;
|
||||
tenureInMonths: number;
|
||||
interestCompoundingPeriodInMonths: number;
|
||||
interestPostingPeriodInMonths: number;
|
||||
submittedOnDate?: string; // yyyy-MM-dd — enables exact calendar calculation
|
||||
daysInYear?: number; // 360 or 365
|
||||
}): Observable<any> {
|
||||
let httpParams = new HttpParams();
|
||||
Object.entries(params).forEach(
|
||||
([
|
||||
key,
|
||||
val
|
||||
]) => {
|
||||
if (val !== undefined && val !== null) {
|
||||
httpParams = httpParams.set(key, String(val));
|
||||
}
|
||||
}
|
||||
);
|
||||
return this.http.get(`/fixeddepositaccounts/calculate-fd-interest`, { params: httpParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param fixedDepositAccount Fixed Deposit Account
|
||||
* @returns {Observable<any>}
|
||||
|
||||
+150
-96
@@ -6,103 +6,157 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="container">
|
||||
<mat-stepper
|
||||
orientation="vertical"
|
||||
[linear]="false"
|
||||
class="mat-elevation-z8"
|
||||
labelPosition="bottom"
|
||||
#recurringDepositAccountStepper
|
||||
<div class="cra-container">
|
||||
<!-- Hero Header -->
|
||||
<div class="cra-header">
|
||||
<fa-icon icon="university" class="cra-header__icon"></fa-icon>
|
||||
<div>
|
||||
<h2 class="cra-header__title">Modify Recurring Deposit Account Application</h2>
|
||||
<div class="cra-header__sub">
|
||||
Review and update the account details, currency and interest terms before resubmitting.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wizard tabs -->
|
||||
<mat-tab-group
|
||||
class="cra-tabs"
|
||||
animationDuration="200ms"
|
||||
[preserveContent]="true"
|
||||
[(selectedIndex)]="selectedTabIndex"
|
||||
>
|
||||
<ng-template matStepperIcon="number">
|
||||
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="edit">
|
||||
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="done">
|
||||
<fa-icon icon="check" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="error">
|
||||
<fa-icon icon="exclamation-triangle" size="lg"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="preview">
|
||||
<fa-icon icon="eye" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<mat-step [stepControl]="recurringDepositAccountDetailsForm">
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.DETAILS' | translate }}</ng-template>
|
||||
|
||||
<mifosx-recurring-deposits-account-details-step
|
||||
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
|
||||
(recurringDepositsAccountProductTemplate)="setTemplate($event)"
|
||||
>
|
||||
</mifosx-recurring-deposits-account-details-step>
|
||||
</mat-step>
|
||||
|
||||
<mat-step [stepControl]="recurringDepositAccountTermsForm">
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template>
|
||||
|
||||
<mifosx-recurring-deposits-account-terms-step
|
||||
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
|
||||
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
|
||||
>
|
||||
</mifosx-recurring-deposits-account-terms-step>
|
||||
</mat-step>
|
||||
|
||||
<mat-step [stepControl]="recurringDepositAccountSettingsForm">
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.SETTINGS' | translate }}</ng-template>
|
||||
|
||||
<mifosx-recurring-deposits-account-settings-step
|
||||
[isNew]="false"
|
||||
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
|
||||
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
|
||||
>
|
||||
</mifosx-recurring-deposits-account-settings-step>
|
||||
</mat-step>
|
||||
|
||||
<mat-step>
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.INTEREST RATE CHART' | translate }}</ng-template>
|
||||
|
||||
<mifosx-recurring-deposits-account-interest-rate-chart-step
|
||||
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
|
||||
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
|
||||
[submittedOnDate]="submittedOnDate"
|
||||
>
|
||||
</mifosx-recurring-deposits-account-interest-rate-chart-step>
|
||||
</mat-step>
|
||||
|
||||
<mat-step>
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template>
|
||||
|
||||
<mifosx-recurring-deposits-account-charges-step
|
||||
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
|
||||
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
|
||||
[recurringDepositAccountFormValid]="recurringDepositAccountFormValidAndNotPristine"
|
||||
>
|
||||
</mifosx-recurring-deposits-account-charges-step>
|
||||
</mat-step>
|
||||
|
||||
@if (recurringDepositAccountFormValidAndNotPristine) {
|
||||
<mat-step state="preview" completed>
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template>
|
||||
<mifosx-recurring-deposits-account-preview-step
|
||||
<!-- Tab 1: Product Classification + Details -->
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="check-square" class="cra-tab__icon"></fa-icon>
|
||||
Product
|
||||
</ng-template>
|
||||
<div class="cra-tab-body">
|
||||
<mifosx-recurring-deposits-account-details-step
|
||||
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
|
||||
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
|
||||
[recurringDepositAccountData]="recurringDepositAccountData"
|
||||
[rdTenureDays]="rdTenureDays"
|
||||
[rdApplicableSlab]="rdApplicableSlab"
|
||||
[rdMaturityAmount]="rdMaturityAmount"
|
||||
[rdInterestEarned]="rdInterestEarned"
|
||||
[rdDaysInYear]="rdDaysInYear"
|
||||
(submitEvent)="submit()"
|
||||
(recurringDepositsAccountProductTemplate)="setTemplate($event)"
|
||||
>
|
||||
</mifosx-recurring-deposits-account-preview-step>
|
||||
</mat-step>
|
||||
}
|
||||
</mat-stepper>
|
||||
</mifosx-recurring-deposits-account-details-step>
|
||||
|
||||
<div class="cra-actions">
|
||||
<span class="cra-actions__spacer"></span>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
[disabled]="!recurringDepositsAccountProductTemplate"
|
||||
(click)="next()"
|
||||
>
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab 2: Currency -->
|
||||
<mat-tab [disabled]="!recurringDepositsAccountProductTemplate">
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="dollar-sign" class="cra-tab__icon"></fa-icon>
|
||||
Currency
|
||||
</ng-template>
|
||||
<div class="cra-tab-body">
|
||||
@if (recurringDepositsAccountProductTemplate) {
|
||||
<mifosx-recurring-deposits-account-currency-step
|
||||
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
|
||||
[primaryClientId]="recurringDepositsAccountAndTemplate?.clientId ?? null"
|
||||
[existingAccount]="recurringDepositsAccountAndTemplate"
|
||||
></mifosx-recurring-deposits-account-currency-step>
|
||||
}
|
||||
|
||||
<div class="cra-actions">
|
||||
<button mat-stroked-button (click)="prev()">
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<span class="cra-actions__spacer"></span>
|
||||
<button mat-raised-button color="primary" (click)="next()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab 3: Interest Rate Chart -->
|
||||
<mat-tab [disabled]="!recurringDepositsAccountProductTemplate">
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="university" class="cra-tab__icon"></fa-icon>
|
||||
Interest Rate Chart
|
||||
</ng-template>
|
||||
<div class="cra-tab-body">
|
||||
@if (recurringDepositsAccountProductTemplate) {
|
||||
<mifosx-recurring-deposits-account-interest-rate-chart-step
|
||||
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
|
||||
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
|
||||
[submittedOnDate]="submittedOnDate"
|
||||
[cbsBaseRates]="cbsBaseRates"
|
||||
[initialTenure]="initialTenure"
|
||||
(tenureSelected)="handleTenureSelected($event)"
|
||||
>
|
||||
</mifosx-recurring-deposits-account-interest-rate-chart-step>
|
||||
}
|
||||
|
||||
<div class="cra-actions">
|
||||
<button mat-stroked-button (click)="prev()">
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<span class="cra-actions__spacer"></span>
|
||||
<button mat-raised-button color="primary" (click)="next()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab 4: Preview -->
|
||||
<mat-tab [disabled]="!recurringDepositsAccountProductTemplate">
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="eye" class="cra-tab__icon"></fa-icon>
|
||||
{{ 'labels.inputs.PREVIEW' | translate }}
|
||||
</ng-template>
|
||||
<div class="cra-tab-body">
|
||||
@if (recurringDepositsAccountProductTemplate) {
|
||||
<mifosx-recurring-deposits-account-preview-step
|
||||
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
|
||||
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
|
||||
[recurringDepositAccountData]="recurringDepositAccountData"
|
||||
[rdNominees]="recurringDepositsAccountCurrencyStep?.nominees ?? []"
|
||||
[rdCurrencyFlags]="rdCurrencyFlags"
|
||||
[rdTenureDays]="rdTenureDays"
|
||||
[rdApplicableSlab]="rdApplicableSlab"
|
||||
[rdMaturityAmount]="rdMaturityAmount"
|
||||
[rdInterestEarned]="rdInterestEarned"
|
||||
[rdDaysInYear]="rdDaysInYear"
|
||||
[rdAppliedRate]="rdAppliedRate"
|
||||
[rdIsSeniorCitizen]="rdIsSeniorCitizen"
|
||||
[hideButtons]="true"
|
||||
(submitEvent)="submit()"
|
||||
>
|
||||
</mifosx-recurring-deposits-account-preview-step>
|
||||
}
|
||||
|
||||
<div class="cra-actions">
|
||||
<button mat-stroked-button (click)="prev()">
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<span class="cra-actions__spacer"></span>
|
||||
<button mat-stroked-button [routerLink]="['../']">
|
||||
{{ 'labels.buttons.Cancel' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" class="preview-submit-row__primary" (click)="submit()">
|
||||
<fa-icon icon="check" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Submit' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
|
||||
+161
@@ -5,3 +5,164 @@
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
$navy: #0d1b2a;
|
||||
$blue: #1e88e5;
|
||||
$blue-d: #1e40af;
|
||||
$indigo: #4f46e5;
|
||||
$border: #e2e8f0;
|
||||
|
||||
.cra-container {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
/* ─── Hero header (matches Nexora pages) ────────────────────────────── */
|
||||
.cra-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&__icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, $blue 0%, $indigo 100%);
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
box-shadow: 0 2px 8px rgb(29 78 216 / 25%);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: $navy;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
margin-top: 2px;
|
||||
font-size: 12.5px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Wizard tab strip ──────────────────────────────────────────────── */
|
||||
.cra-tabs {
|
||||
background: #fff;
|
||||
border: 1px solid $border;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
|
||||
overflow: hidden;
|
||||
|
||||
::ng-deep {
|
||||
.mat-mdc-tab-header {
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid $border;
|
||||
}
|
||||
|
||||
.mat-mdc-tab {
|
||||
min-width: 140px;
|
||||
height: 50px;
|
||||
padding: 0 22px;
|
||||
font-size: 13px !important;
|
||||
font-weight: 600 !important;
|
||||
letter-spacing: 0.01em;
|
||||
color: #475569 !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab:hover:not(.mdc-tab--active, .mat-mdc-tab-disabled) {
|
||||
background: rgb(30 64 175 / 5%);
|
||||
color: $navy !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab.mdc-tab--active {
|
||||
.mdc-tab__text-label {
|
||||
color: $blue-d !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mdc-tab-indicator__content--underline {
|
||||
border-color: $blue-d !important;
|
||||
border-top-width: 3px !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab.mat-mdc-tab-disabled {
|
||||
opacity: 0.45 !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mat-mdc-tab-body-wrapper {
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
.mat-mdc-tab-body,
|
||||
.mat-mdc-tab-body-content {
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cra-tab__icon {
|
||||
margin-right: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ─── Tab body + action bar ─────────────────────────────────────────── */
|
||||
.cra-tab-body {
|
||||
padding: 24px;
|
||||
|
||||
@media (width <= 768px) {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.cra-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
|
||||
&__spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 40px !important;
|
||||
border-radius: 10px !important;
|
||||
font-weight: 600 !important;
|
||||
padding: 0 18px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.m-l-10 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.m-r-10 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
button.preview-submit-row__primary,
|
||||
.mat-mdc-raised-button.mat-primary.preview-submit-row__primary,
|
||||
.preview-submit-row__primary {
|
||||
background: linear-gradient(135deg, #10b981 0%, #047857 100%) !important;
|
||||
|
||||
--mdc-protected-button-container-color: transparent !important;
|
||||
--mdc-filled-button-container-color: transparent !important;
|
||||
|
||||
color: #fff !important;
|
||||
box-shadow: 0 4px 12px -2px rgb(4 120 87 / 35%) !important;
|
||||
}
|
||||
|
||||
+243
-110
@@ -12,23 +12,23 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
/** Custom Services */
|
||||
import { RecurringDepositsService } from '../recurring-deposits.service';
|
||||
import { ClientsService } from 'app/clients/clients.service';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { ProductsService } from 'app/products/products.service';
|
||||
import { AlertService } from 'app/core/alert/alert.service';
|
||||
|
||||
/** Step Components */
|
||||
import { RecurringDepositsAccountDetailsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component';
|
||||
import { RecurringDepositsAccountTermsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-terms-step/recurring-deposits-account-terms-step.component';
|
||||
import { RecurringDepositsAccountSettingsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-settings-step/recurring-deposits-account-settings-step.component';
|
||||
import { RecurringDepositsAccountChargesStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-charges-step/recurring-deposits-account-charges-step.component';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { RecurringDepositsAccountCurrencyStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component';
|
||||
import { RecurringDepositsAccountInterestRateChartStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component';
|
||||
import { RecurringDepositsAccountPreviewStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/**
|
||||
* Edit new recurring deposit account
|
||||
* Edit Recurring Deposit Account Component — same tab UI as Create, pre-populated with existing data.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-edit-recurring-deposit-account',
|
||||
@@ -36,16 +36,11 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
styleUrls: ['./edit-recurring-deposit-account.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatStepper,
|
||||
MatStepperIcon,
|
||||
MatTabsModule,
|
||||
FaIconComponent,
|
||||
MatStep,
|
||||
MatStepLabel,
|
||||
RecurringDepositsAccountDetailsStepComponent,
|
||||
RecurringDepositsAccountTermsStepComponent,
|
||||
RecurringDepositsAccountSettingsStepComponent,
|
||||
RecurringDepositsAccountCurrencyStepComponent,
|
||||
RecurringDepositsAccountInterestRateChartStepComponent,
|
||||
RecurringDepositsAccountChargesStepComponent,
|
||||
RecurringDepositsAccountPreviewStepComponent
|
||||
]
|
||||
})
|
||||
@@ -56,20 +51,117 @@ export class EditRecurringDepositAccountComponent {
|
||||
private recurringDepositsService = inject(RecurringDepositsService);
|
||||
private settingsService = inject(SettingsService);
|
||||
private productsService = inject(ProductsService);
|
||||
private alertService = inject(AlertService);
|
||||
private clientsService = inject(ClientsService);
|
||||
|
||||
/** Imports all the step component */
|
||||
@ViewChild(RecurringDepositsAccountDetailsStepComponent, { static: true })
|
||||
recurringDepositsAccountDetailsStep: RecurringDepositsAccountDetailsStepComponent;
|
||||
@ViewChild(RecurringDepositsAccountTermsStepComponent, { static: true })
|
||||
recurringDepositAccountTermsStep: RecurringDepositsAccountTermsStepComponent;
|
||||
@ViewChild(RecurringDepositsAccountSettingsStepComponent, { static: true })
|
||||
recurringDepositAccountSettingsStep: RecurringDepositsAccountSettingsStepComponent;
|
||||
@ViewChild(RecurringDepositsAccountChargesStepComponent, { static: true })
|
||||
recurringDepositAccountChargesStep: RecurringDepositsAccountChargesStepComponent;
|
||||
|
||||
@ViewChild(RecurringDepositsAccountCurrencyStepComponent)
|
||||
recurringDepositsAccountCurrencyStep: RecurringDepositsAccountCurrencyStepComponent;
|
||||
|
||||
@ViewChild(RecurringDepositsAccountInterestRateChartStepComponent)
|
||||
recurringDepositsAccountChartStep: RecurringDepositsAccountInterestRateChartStepComponent;
|
||||
|
||||
/** Full account data + template from resolver */
|
||||
recurringDepositsAccountAndTemplate: any;
|
||||
/** Product template emitted by details step */
|
||||
recurringDepositsAccountProductTemplate: any;
|
||||
/** Currently visible wizard tab (0-indexed). */
|
||||
selectedTabIndex = 0;
|
||||
private readonly totalTabs = 4;
|
||||
/** Active CBS base interest rate versions */
|
||||
cbsBaseRates: any[] = [];
|
||||
/** Selected tenure value and frequency from calculator */
|
||||
selectedTenureValue: number | null = null;
|
||||
selectedTenureFrequencyId: number | null = null;
|
||||
selectedDepositAmount: number | null = null;
|
||||
rdAppliedRate: number | null = null;
|
||||
rdIsSeniorCitizen = false;
|
||||
|
||||
constructor() {
|
||||
this.route.data.subscribe((data: { recurringDepositsAccountAndTemplate: any }) => {
|
||||
const account = data.recurringDepositsAccountAndTemplate;
|
||||
this.recurringDepositsAccountAndTemplate = account;
|
||||
// Pre-populate tenure from existing account
|
||||
if (account?.depositPeriod) {
|
||||
this.selectedTenureValue = account.depositPeriod;
|
||||
this.selectedTenureFrequencyId = account.depositPeriodFrequencyType?.id ?? 2;
|
||||
}
|
||||
if (account?.mandatoryRecommendedDepositAmount) {
|
||||
this.selectedDepositAmount = account.mandatoryRecommendedDepositAmount;
|
||||
}
|
||||
if (account?.clientId && account?.accountNo) {
|
||||
this.clientsService
|
||||
.getClientNominees(String(account.clientId), account.accountNo, 'RD')
|
||||
.subscribe((nominees: any[]) => {
|
||||
if (Array.isArray(nominees) && nominees.length > 0) {
|
||||
this.recurringDepositsAccountAndTemplate = { ...this.recurringDepositsAccountAndTemplate, nominees };
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get initialTenure(): {
|
||||
depositPeriod: number;
|
||||
depositPeriodFrequencyId: number;
|
||||
depositAmount?: number;
|
||||
isSeniorCitizen?: boolean;
|
||||
appliedRate?: number;
|
||||
} | null {
|
||||
if (!this.selectedTenureValue) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
depositPeriod: this.selectedTenureValue,
|
||||
depositPeriodFrequencyId: this.selectedTenureFrequencyId ?? 2,
|
||||
depositAmount: this.selectedDepositAmount ?? undefined,
|
||||
isSeniorCitizen: !!this.recurringDepositsAccountAndTemplate?.isSeniorCitizen,
|
||||
appliedRate: this.recurringDepositsAccountAndTemplate?.seniorCitizenInterestRate ?? undefined
|
||||
};
|
||||
}
|
||||
|
||||
next(): void {
|
||||
if (this.selectedTabIndex < this.totalTabs - 1) {
|
||||
this.selectedTabIndex += 1;
|
||||
}
|
||||
}
|
||||
|
||||
prev(): void {
|
||||
if (this.selectedTabIndex > 0) {
|
||||
this.selectedTabIndex -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
setTemplate($event: any) {
|
||||
this.recurringDepositsAccountProductTemplate = $event;
|
||||
this.productsService.getFdBaseInterestRates().subscribe({
|
||||
next: (rates: any[]) => (this.cbsBaseRates = rates || []),
|
||||
error: () => (this.cbsBaseRates = [])
|
||||
});
|
||||
}
|
||||
|
||||
handleTenureSelected($event: any) {
|
||||
this.selectedTenureValue = $event.depositPeriod;
|
||||
this.selectedTenureFrequencyId = $event.depositPeriodFrequencyId;
|
||||
this.selectedDepositAmount = $event.depositAmount;
|
||||
this.rdAppliedRate = $event.appliedRate ?? null;
|
||||
this.rdIsSeniorCitizen = $event.isSeniorCitizen ?? false;
|
||||
}
|
||||
|
||||
get recurringDepositAccountDetailsForm() {
|
||||
return this.recurringDepositsAccountDetailsStep.recurringDepositAccountDetailsForm;
|
||||
}
|
||||
|
||||
get recurringDepositAccountData() {
|
||||
return { ...this.recurringDepositsAccountDetailsStep.recurringDepositAccountDetails };
|
||||
}
|
||||
|
||||
get recurringDepositAccount() {
|
||||
return { ...this.recurringDepositsAccountDetailsStep.recurringDepositAccountDetails };
|
||||
}
|
||||
|
||||
get rdTenureDays(): number | null {
|
||||
return this.recurringDepositsAccountChartStep?.matchResult?.tenureDays ?? null;
|
||||
}
|
||||
@@ -94,112 +186,153 @@ export class EditRecurringDepositAccountComponent {
|
||||
return this.recurringDepositAccountDetailsForm?.value?.submittedOnDate ?? null;
|
||||
}
|
||||
|
||||
/** Recurring Deposits Account And Template */
|
||||
recurringDepositsAccountAndTemplate: any;
|
||||
/** Recurring Deposit Account Product Template */
|
||||
recurringDepositsAccountProductTemplate: any;
|
||||
|
||||
constructor() {
|
||||
this.route.data.subscribe((data: { recurringDepositsAccountAndTemplate: any }) => {
|
||||
this.recurringDepositsAccountAndTemplate = data.recurringDepositsAccountAndTemplate;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets recurring deposits account product template.
|
||||
* @param {any} $event API response
|
||||
*/
|
||||
setTemplate($event: any) {
|
||||
this.recurringDepositsAccountProductTemplate = $event;
|
||||
}
|
||||
|
||||
/** Get Recurring Deposit Account Details Form Data */
|
||||
get recurringDepositAccountDetailsForm() {
|
||||
return this.recurringDepositsAccountDetailsStep.recurringDepositAccountDetailsForm;
|
||||
}
|
||||
|
||||
/** Get Recurring Deposit Account Terms Form Data */
|
||||
get recurringDepositAccountTermsForm() {
|
||||
return this.recurringDepositAccountTermsStep.recurringDepositAccountTermsForm;
|
||||
}
|
||||
|
||||
/** Get Recurring Deposit Account Settings Form Data */
|
||||
get recurringDepositAccountSettingsForm() {
|
||||
return this.recurringDepositAccountSettingsStep.recurringDepositAccountSettingsForm;
|
||||
}
|
||||
|
||||
// Checks if stepper is valid and not pristine.
|
||||
get recurringDepositAccountFormValidAndNotPristine() {
|
||||
return (
|
||||
this.recurringDepositAccountDetailsForm.valid &&
|
||||
this.recurringDepositAccountTermsForm.valid &&
|
||||
this.recurringDepositAccountSettingsForm.valid &&
|
||||
(!this.recurringDepositAccountDetailsForm.pristine ||
|
||||
!this.recurringDepositAccountTermsForm.pristine ||
|
||||
!this.recurringDepositAccountSettingsForm.pristine ||
|
||||
!this.recurringDepositAccountChargesStep.pristine)
|
||||
);
|
||||
}
|
||||
|
||||
/** Retrieves Data of all the forms */
|
||||
get recurringDepositAccountData() {
|
||||
get rdCurrencyFlags() {
|
||||
return {
|
||||
...this.recurringDepositsAccountDetailsStep.recurringDepositAccountDetails,
|
||||
...this.recurringDepositAccountTermsStep.recurringDepositAccountTerms,
|
||||
...this.recurringDepositAccountSettingsStep.recurringDepositAccountSettings,
|
||||
...this.recurringDepositAccountChargesStep.recurringDepositAccountCharges
|
||||
autoDebitFacility: this.recurringDepositsAccountCurrencyStep?.autoDebitFacility ?? false
|
||||
};
|
||||
}
|
||||
|
||||
/** Retrieves Data of all forms except Currency to submit the data */
|
||||
get recurringDepositAccount() {
|
||||
return {
|
||||
...this.recurringDepositsAccountDetailsStep.recurringDepositAccountDetails,
|
||||
...this.recurringDepositAccountTermsStep.recurringDepositAccountTerms,
|
||||
...this.recurringDepositAccountSettingsStep.recurringDepositAccountSettings,
|
||||
...this.recurringDepositAccountChargesStep.recurringDepositAccountCharges
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the recurring deposit form to create a new recurring deposit account
|
||||
*/
|
||||
submit() {
|
||||
const detailsStep = this.recurringDepositsAccountDetailsStep;
|
||||
if (detailsStep.selectedAccountStatus === 'JOINT' && detailsStep.totalJointSharePercentage !== 100) {
|
||||
this.alertService.alert({
|
||||
type: 'Error',
|
||||
message: `Total ownership is ${detailsStep.totalJointSharePercentage}%. It must equal 100% before submitting.`
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this.recurringDepositsAccountProductTemplate?.nominationFacility &&
|
||||
(this.recurringDepositsAccountCurrencyStep?.nominees ?? []).length === 0
|
||||
) {
|
||||
this.alertService.alert({
|
||||
type: 'Error',
|
||||
message: 'Nomination Facility is enabled for this product. Please add at least one nominee before submitting.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!this.selectedTenureValue || this.selectedTenureFrequencyId === null) {
|
||||
this.alertService.alert({
|
||||
type: 'Error',
|
||||
message: 'Please enter the Deposit Tenure and click "Find Rate" before submitting.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!this.selectedDepositAmount || this.selectedDepositAmount <= 0) {
|
||||
this.alertService.alert({
|
||||
type: 'Error',
|
||||
message: 'Please enter the Monthly Installment amount and click "Find Rate" before submitting.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!detailsStep?.selectedCustomerSegment) {
|
||||
this.alertService.alert({ type: 'Error', message: 'Please select a Customer Segment before submitting.' });
|
||||
return;
|
||||
}
|
||||
const locale = this.settingsService.language.code;
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
const monthDayFormat = 'dd MMMM';
|
||||
const recurringDepositAccount = {
|
||||
...this.recurringDepositAccount,
|
||||
const pt = this.recurringDepositsAccountProductTemplate;
|
||||
const details = this.recurringDepositAccount;
|
||||
const currencyStep = this.recurringDepositsAccountCurrencyStep;
|
||||
|
||||
const terms: any = {};
|
||||
if (pt?.nominalAnnualInterestRate != null) terms.nominalAnnualInterestRate = pt.nominalAnnualInterestRate;
|
||||
if (pt?.interestCompoundingPeriodType?.id != null)
|
||||
terms.interestCompoundingPeriodType = pt.interestCompoundingPeriodType.id;
|
||||
if (pt?.interestPostingPeriodType?.id != null) terms.interestPostingPeriodType = pt.interestPostingPeriodType.id;
|
||||
if (pt?.interestCalculationType?.id != null) terms.interestCalculationType = pt.interestCalculationType.id;
|
||||
if (pt?.interestCalculationDaysInYearType?.id != null)
|
||||
terms.interestCalculationDaysInYearType = pt.interestCalculationDaysInYearType.id;
|
||||
|
||||
const settings: any = {};
|
||||
if (pt?.lockinPeriodFrequency != null) settings.lockinPeriodFrequency = pt.lockinPeriodFrequency;
|
||||
if (pt?.lockinPeriodFrequencyType?.id != null) settings.lockinPeriodFrequencyType = pt.lockinPeriodFrequencyType.id;
|
||||
if (pt?.minDepositTerm != null) settings.minDepositTerm = pt.minDepositTerm;
|
||||
if (pt?.minDepositTermType?.id != null) settings.minDepositTermTypeId = pt.minDepositTermType.id;
|
||||
if (pt?.inMultiplesOfDepositTerm != null) settings.inMultiplesOfDepositTerm = pt.inMultiplesOfDepositTerm;
|
||||
if (pt?.inMultiplesOfDepositTermType?.id != null)
|
||||
settings.inMultiplesOfDepositTermTypeId = pt.inMultiplesOfDepositTermType.id;
|
||||
if (pt?.maxDepositTerm != null) settings.maxDepositTerm = pt.maxDepositTerm;
|
||||
if (pt?.maxDepositTermType?.id != null) settings.maxDepositTermTypeId = pt.maxDepositTermType.id;
|
||||
if (pt?.preClosurePenalApplicable != null) settings.preClosurePenalApplicable = pt.preClosurePenalApplicable;
|
||||
if (pt?.preClosurePenalInterest != null) settings.preClosurePenalInterest = pt.preClosurePenalInterest;
|
||||
if (pt?.preClosurePenalInterestOnType?.id != null)
|
||||
settings.preClosurePenalInterestOnTypeId = pt.preClosurePenalInterestOnType.id;
|
||||
if (pt?.maturityInstructionId != null) settings.maturityInstructionId = pt.maturityInstructionId;
|
||||
if (pt?.transferInterestToSavings != null) settings.transferInterestToSavings = pt.transferInterestToSavings;
|
||||
if (pt?.withHoldTax != null) settings.withHoldTax = pt.withHoldTax;
|
||||
|
||||
const productDefaults: any = {
|
||||
isCalendarInherited: pt?.isCalendarInherited ?? false,
|
||||
isMandatoryDeposit: pt?.isMandatoryDeposit ?? false,
|
||||
allowWithdrawal: pt?.allowWithdrawal ?? false,
|
||||
adjustAdvanceTowardsFuturePayments: pt?.adjustAdvanceTowardsFuturePayments ?? false,
|
||||
autoDebitFacility: currencyStep?.autoDebitFacility ?? pt?.autoDebitFacility ?? false
|
||||
};
|
||||
|
||||
const rawCharges: any[] = pt?.charges ?? [];
|
||||
|
||||
const recurringDepositAccount: any = {
|
||||
...details,
|
||||
...terms,
|
||||
...settings,
|
||||
...productDefaults,
|
||||
clientId: this.recurringDepositsAccountAndTemplate.clientId,
|
||||
charges: this.recurringDepositAccount.charges.map((charge: any) => ({
|
||||
chargeId: charge.id,
|
||||
mandatoryRecommendedDepositAmount: pt?.minDepositAmount ?? 0,
|
||||
recurringFrequency: pt?.recurringFrequency ?? 1,
|
||||
recurringFrequencyType: pt?.recurringFrequencyType ?? 2,
|
||||
submittedOnDate: this.dateUtils.formatDate(details.submittedOnDate, dateFormat),
|
||||
expectedFirstDepositOnDate: this.dateUtils.formatDate(details.expectedFirstDepositOnDate, dateFormat),
|
||||
dateFormat,
|
||||
monthDayFormat,
|
||||
locale,
|
||||
charts: [
|
||||
{
|
||||
chartSlabs:
|
||||
pt?.accountChart?.chartSlabs ?? pt?.activeChart?.chartSlabs ?? pt?.interestRateCharts?.[0]?.chartSlabs ?? []
|
||||
}
|
||||
],
|
||||
charges: rawCharges.map((charge: any) => ({
|
||||
chargeId: charge.chargeId ?? charge.id,
|
||||
amount: charge.amount,
|
||||
dueDate: charge.dueDate && this.dateUtils.formatDate(charge.dueDate, dateFormat),
|
||||
feeOnMonthDay: charge.feeOnMonthDay,
|
||||
feeInterval: charge.feeInterval
|
||||
})),
|
||||
isCalendarInherited: this.recurringDepositAccount.recurringDepositAccount
|
||||
? this.recurringDepositAccount.recurringDepositAccount
|
||||
: false,
|
||||
submittedOnDate: this.dateUtils.formatDate(this.recurringDepositAccount.submittedOnDate, dateFormat),
|
||||
expectedFirstDepositOnDate: this.dateUtils.formatDate(
|
||||
this.recurringDepositAccount.expectedFirstDepositOnDate,
|
||||
dateFormat
|
||||
),
|
||||
charts: [
|
||||
{
|
||||
chartSlabs:
|
||||
this.recurringDepositsAccountProductTemplate?.accountChart?.chartSlabs ??
|
||||
this.recurringDepositsAccountProductTemplate?.activeChart?.chartSlabs ??
|
||||
this.recurringDepositsAccountProductTemplate?.interestRateCharts?.[0]?.chartSlabs ??
|
||||
[]
|
||||
}
|
||||
],
|
||||
dateFormat,
|
||||
monthDayFormat,
|
||||
locale
|
||||
}))
|
||||
};
|
||||
|
||||
if (this.selectedTenureValue && this.selectedTenureFrequencyId !== null) {
|
||||
recurringDepositAccount.depositPeriod = this.selectedTenureValue;
|
||||
recurringDepositAccount.depositPeriodFrequencyId = this.selectedTenureFrequencyId;
|
||||
} else if (this.rdTenureDays) {
|
||||
recurringDepositAccount.depositPeriod = this.rdTenureDays;
|
||||
recurringDepositAccount.depositPeriodFrequencyId = 0;
|
||||
}
|
||||
|
||||
if (this.selectedDepositAmount) {
|
||||
recurringDepositAccount.mandatoryRecommendedDepositAmount = this.selectedDepositAmount;
|
||||
}
|
||||
|
||||
if (this.rdIsSeniorCitizen && this.rdAppliedRate != null && this.rdAppliedRate > 0) {
|
||||
recurringDepositAccount.isSeniorCitizen = true;
|
||||
recurringDepositAccount.seniorCitizenInterestRate = this.rdAppliedRate;
|
||||
recurringDepositAccount.nominalAnnualInterestRate = this.rdAppliedRate;
|
||||
}
|
||||
|
||||
const nominees = this.recurringDepositsAccountCurrencyStep?.nominees ?? [];
|
||||
if (nominees.length > 0) {
|
||||
recurringDepositAccount.nominees = nominees.map((n: any) => ({
|
||||
nomineeClientId: n.nomineeClientId,
|
||||
nominationType: n.nominationType,
|
||||
relationshipId: n.relationshipId,
|
||||
percentageShare: n.percentageShare,
|
||||
nomineeIsMinor: n.nomineeIsMinor,
|
||||
...(n.guardianClientId ? { guardianClientId: n.guardianClientId } : {}),
|
||||
remarks: n.remarks
|
||||
}));
|
||||
}
|
||||
|
||||
this.recurringDepositsService
|
||||
.updateRecurringDepositAccount(this.recurringDepositsAccountAndTemplate.id, recurringDepositAccount)
|
||||
.subscribe((response: any) => {
|
||||
|
||||
+14
-1
@@ -28,6 +28,7 @@ import { FormsModule } from '@angular/forms';
|
||||
export class RecurringDepositsAccountCurrencyStepComponent implements OnChanges {
|
||||
@Input() recurringDepositsAccountProductTemplate: any;
|
||||
@Input() primaryClientId: number | null = null;
|
||||
@Input() existingAccount: any = null;
|
||||
@ViewChild(NomineeFormComponent) nomineeFormComp: NomineeFormComponent;
|
||||
|
||||
nominees: any[] = [];
|
||||
@@ -37,8 +38,20 @@ export class RecurringDepositsAccountCurrencyStepComponent implements OnChanges
|
||||
|
||||
autoDebitFacility = false;
|
||||
|
||||
private existingAccountApplied = false;
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['recurringDepositsAccountProductTemplate']?.currentValue) {
|
||||
if (changes['existingAccount']?.currentValue) {
|
||||
const ea = changes['existingAccount'].currentValue;
|
||||
this.existingAccountApplied = true;
|
||||
if (ea.autoDebitFacility !== undefined) {
|
||||
this.autoDebitFacility = !!ea.autoDebitFacility;
|
||||
}
|
||||
if (Array.isArray(ea.nominees) && ea.nominees.length > 0) {
|
||||
this.nominees = [...ea.nominees];
|
||||
}
|
||||
}
|
||||
if (changes['recurringDepositsAccountProductTemplate']?.currentValue && !this.existingAccountApplied) {
|
||||
const pt = changes['recurringDepositsAccountProductTemplate'].currentValue;
|
||||
this.autoDebitFacility = !!pt.autoDebitFacility;
|
||||
}
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@
|
||||
<input
|
||||
matInput
|
||||
[value]="
|
||||
productTemplate?.previewAccountNo || recurringDepositsAccountTemplate?.accountNo || 'Auto-generated'
|
||||
recurringDepositsAccountTemplate?.accountNo || productTemplate?.previewAccountNo || 'Auto-generated'
|
||||
"
|
||||
readonly
|
||||
/>
|
||||
|
||||
+123
-3
@@ -70,6 +70,7 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
|
||||
recurringDepositAccountDetailsForm!: UntypedFormGroup;
|
||||
|
||||
isProductSelected = false;
|
||||
private isEditMode = false;
|
||||
|
||||
/** Recurring Deposits Account Template with product data */
|
||||
@Output() recurringDepositsAccountProductTemplate = new EventEmitter();
|
||||
@@ -224,6 +225,21 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
|
||||
currencyCode: a.currency?.code ?? ''
|
||||
}));
|
||||
this.dalLoading = false;
|
||||
// Pre-select linked account in edit mode
|
||||
const linkedId =
|
||||
this.recurringDepositsAccountTemplate?.linkedAccount?.id ??
|
||||
this.recurringDepositsAccountTemplate?.linkedAccountId;
|
||||
if (linkedId) {
|
||||
const savings = this.dalAllSavings.find((a: any) => a.id === linkedId);
|
||||
const current = this.dalAllCurrent.find((a: any) => a.id === linkedId);
|
||||
if (savings) {
|
||||
this.dalSelectedType = 'SAVINGS';
|
||||
this.selectedLinkedAccount = savings;
|
||||
} else if (current) {
|
||||
this.dalSelectedType = 'CURRENT';
|
||||
this.selectedLinkedAccount = current;
|
||||
}
|
||||
}
|
||||
this.cdr.detectChanges();
|
||||
},
|
||||
error: () => {
|
||||
@@ -241,11 +257,29 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
|
||||
this.buildDependencies();
|
||||
if (this.recurringDepositsAccountTemplate) {
|
||||
this.productData = this.recurringDepositsAccountTemplate.productOptions;
|
||||
if (this.recurringDepositsAccountTemplate.depositProductId) {
|
||||
const existingProductId =
|
||||
this.recurringDepositsAccountTemplate.depositProductId ?? this.recurringDepositsAccountTemplate.productId;
|
||||
// Edit mode: account response has accountNo; pure template has previewAccountNo but no accountNo
|
||||
if (this.recurringDepositsAccountTemplate.accountNo) {
|
||||
this.isEditMode = true;
|
||||
}
|
||||
if ((!this.productData || this.productData.length === 0) && existingProductId) {
|
||||
this.isEditMode = true;
|
||||
this.productData = [
|
||||
{
|
||||
id: existingProductId,
|
||||
name:
|
||||
this.recurringDepositsAccountTemplate.productName ??
|
||||
this.recurringDepositsAccountTemplate.depositProductName ??
|
||||
'Product'
|
||||
}
|
||||
];
|
||||
}
|
||||
if (existingProductId) {
|
||||
this.recurringDepositAccountDetailsForm.patchValue({
|
||||
productId: this.recurringDepositsAccountTemplate.depositProductId,
|
||||
productId: existingProductId,
|
||||
submittedOnDate:
|
||||
this.recurringDepositsAccountTemplate.timeline.submittedOnDate &&
|
||||
this.recurringDepositsAccountTemplate.timeline?.submittedOnDate &&
|
||||
new Date(this.recurringDepositsAccountTemplate.timeline.submittedOnDate),
|
||||
externalId: this.recurringDepositsAccountTemplate.externalId
|
||||
});
|
||||
@@ -317,6 +351,9 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
|
||||
}
|
||||
|
||||
private autoDetectAccountStatus(): void {
|
||||
if (this.isEditMode) {
|
||||
return;
|
||||
}
|
||||
if (this.clientAge === null) return;
|
||||
if (this.clientAge < 18) {
|
||||
this.selectedAccountStatus = 'MINOR';
|
||||
@@ -391,10 +428,93 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
|
||||
} else {
|
||||
this.recurringDepositAccountDetailsForm.get('fieldOfficerId')!.patchValue('');
|
||||
}
|
||||
// === Restore existing account values when in edit/modify mode ===
|
||||
this.restoreExistingAccountValues();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private restoreExistingAccountValues(): void {
|
||||
const ea = this.recurringDepositsAccountTemplate;
|
||||
if (!ea?.depositProductId && !ea?.productId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const strVal = (v: any): string => (typeof v === 'object' && v !== null ? (v.value ?? '') : (v ?? ''));
|
||||
|
||||
const accountStatus = strVal(ea.accountStatus);
|
||||
if (accountStatus) {
|
||||
this.selectedAccountStatus = accountStatus;
|
||||
}
|
||||
|
||||
const customerSegment = strVal(ea.customerSegment);
|
||||
if (customerSegment) {
|
||||
this.selectedCustomerSegment = customerSegment;
|
||||
}
|
||||
|
||||
// RD API returns cbs_account_type aliased as "accountType" (string); Current/Savings return it as "cbsAccountType"
|
||||
const rawType = ea.cbsAccountType ?? (typeof ea.accountType === 'string' ? ea.accountType : null);
|
||||
const accountType = rawType ? strVal(rawType) : null;
|
||||
if (accountType) {
|
||||
this.selectedAccountType = accountType;
|
||||
}
|
||||
|
||||
if (accountStatus === 'JOINT') {
|
||||
const modeOfOp = strVal(ea.modeOfOperation);
|
||||
if (modeOfOp) {
|
||||
this.modeOfOperation = modeOfOp;
|
||||
}
|
||||
if (Array.isArray(ea.jointHolders) && ea.jointHolders.length > 0) {
|
||||
const isPrimaryHolder = (h: any) => h.isPrimary ?? h.isprimary ?? false;
|
||||
const primary = ea.jointHolders.find((h: any) => isPrimaryHolder(h));
|
||||
const primaryShare = primary?.sharePercentage ?? primary?.sharepercentage;
|
||||
if (primaryShare) {
|
||||
this.primaryHolderShare = +primaryShare;
|
||||
}
|
||||
this.jointHolders = ea.jointHolders
|
||||
.filter((h: any) => !isPrimaryHolder(h))
|
||||
.map((h: any) => ({
|
||||
clientId: h.clientId ?? h.clientid,
|
||||
clientName: h.displayName ?? h.displayname ?? h.clientName ?? '',
|
||||
clientCif: h.cifNumber ?? h.cifnumber ?? h.clientCif ?? '',
|
||||
sharePercentage: +(h.sharePercentage ?? h.sharepercentage ?? 0),
|
||||
isMandatory: !!(h.isMandatory ?? h.ismandatory)
|
||||
}));
|
||||
}
|
||||
}
|
||||
if (accountStatus === 'INDIVIDUAL_MALE' && ea.specialCategory) {
|
||||
this.selectedSpecialCategory = ea.specialCategory;
|
||||
}
|
||||
if (accountStatus === 'INDIVIDUAL_FEMALE' && ea.weakerSection) {
|
||||
this.selectedWeakerSection = ea.weakerSection;
|
||||
}
|
||||
if (accountStatus === 'MINOR') {
|
||||
if (ea.guardianClientId) {
|
||||
this.guardianClientId = ea.guardianClientId;
|
||||
}
|
||||
if (ea.guardianName) {
|
||||
this.guardianName = ea.guardianName;
|
||||
}
|
||||
if (ea.guardianCifNumber) {
|
||||
this.guardianCif = ea.guardianCifNumber;
|
||||
}
|
||||
if (ea.guardianRelationship) {
|
||||
this.guardianRelationship = ea.guardianRelationship;
|
||||
}
|
||||
if (ea.minorDateOfBirth) {
|
||||
const d = new Date(ea.minorDateOfBirth);
|
||||
if (!isNaN(d.getTime())) {
|
||||
this.minorDob = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
// autoDebitFacility from existing account (override product default)
|
||||
if (ea.autoDebitFacility !== undefined) {
|
||||
this.recurringDepositAccountDetailsForm.patchValue({ autoDebitFacility: !!ea.autoDebitFacility });
|
||||
}
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
|
||||
hasValue(val: any): boolean {
|
||||
return val !== null && val !== undefined && val !== '';
|
||||
}
|
||||
|
||||
+38
-3
@@ -6,7 +6,7 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component, Input, OnChanges, ViewChild, Output, EventEmitter, inject } from '@angular/core';
|
||||
import { Component, Input, OnChanges, SimpleChanges, ViewChild, Output, EventEmitter, inject } from '@angular/core';
|
||||
import { RecurringDepositsService } from '../../recurring-deposits.service';
|
||||
import {
|
||||
MatTable,
|
||||
@@ -86,6 +86,14 @@ export class RecurringDepositsAccountInterestRateChartStepComponent implements O
|
||||
@Input() submittedOnDate: Date | null = null;
|
||||
/** Active CBS base interest rate versions — when provided, overrides the product chart slabs */
|
||||
@Input() cbsBaseRates: any[] = [];
|
||||
/** Pre-populate tenure from existing account (edit mode) */
|
||||
@Input() initialTenure: {
|
||||
depositPeriod: number;
|
||||
depositPeriodFrequencyId: number;
|
||||
depositAmount?: number;
|
||||
isSeniorCitizen?: boolean;
|
||||
appliedRate?: number;
|
||||
} | null = null;
|
||||
|
||||
@Output() tenureSelected = new EventEmitter<{
|
||||
depositPeriod: number;
|
||||
@@ -165,17 +173,42 @@ export class RecurringDepositsAccountInterestRateChartStepComponent implements O
|
||||
|
||||
private recurringDepositsService = inject(RecurringDepositsService);
|
||||
|
||||
ngOnChanges() {
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
// Only reset/re-apply when the TEMPLATE or cbsBaseRates changes, NOT when initialTenure changes alone
|
||||
const templateChanged =
|
||||
changes['recurringDepositsAccountProductTemplate'] ||
|
||||
changes['recurringDepositsAccountTemplate'] ||
|
||||
changes['cbsBaseRates'];
|
||||
const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
|
||||
if (template) {
|
||||
if (templateChanged && template) {
|
||||
if (this.chartsTableRef) {
|
||||
this.chartsTableRef.renderRows();
|
||||
}
|
||||
this.resetCalculator();
|
||||
this.tenurePeriodUnit = this.allowedPeriodUnits[1]; // Default to Months
|
||||
this.applyInitialTenure();
|
||||
}
|
||||
}
|
||||
|
||||
private applyInitialTenure(): void {
|
||||
if (!this.initialTenure?.depositPeriod) {
|
||||
return;
|
||||
}
|
||||
const freqId = this.initialTenure.depositPeriodFrequencyId;
|
||||
// RD: Months=2, Years=3
|
||||
this.tenurePeriodUnit = freqId === 3 ? 'Years' : 'Months';
|
||||
this.tenureValue = this.initialTenure.depositPeriod;
|
||||
if (this.initialTenure.depositAmount) {
|
||||
this.principalAmount = this.initialTenure.depositAmount;
|
||||
}
|
||||
// Pre-check senior citizen if it was set during create
|
||||
if (this.initialTenure.isSeniorCitizen) {
|
||||
this.isSeniorCitizen = true;
|
||||
}
|
||||
// Auto-calculate when tenure is pre-filled (edit mode)
|
||||
setTimeout(() => this.calculateRate(), 0);
|
||||
}
|
||||
|
||||
// Convert entered tenure to Days for slab matching
|
||||
private toDays(value: number, unit: RdPeriodUnit): number {
|
||||
if (unit === 'Days') return value;
|
||||
@@ -240,6 +273,8 @@ export class RecurringDepositsAccountInterestRateChartStepComponent implements O
|
||||
tenureDays: enteredDays
|
||||
};
|
||||
|
||||
this.matchResult = result; // Show slab match immediately, maturity updates async when API responds
|
||||
|
||||
// Maturity Estimation via Backend API
|
||||
if (this.principalAmount && this.principalAmount > 0) {
|
||||
const compoundingType = this.recurringDepositsAccountProductTemplate?.interestCompoundingPeriodType?.id;
|
||||
|
||||
+10
@@ -111,6 +111,16 @@ export class RecurringDepositsButtonsConfiguration {
|
||||
}
|
||||
];
|
||||
break;
|
||||
case 'Rejected':
|
||||
case 'rejected':
|
||||
this.buttonsArray = [
|
||||
{
|
||||
name: 'Modify Application',
|
||||
icon: 'fa fa-edit',
|
||||
taskPermissionName: 'UPDATE_RECURRINGDEPOSITACCOUNT'
|
||||
}
|
||||
];
|
||||
break;
|
||||
default:
|
||||
this.buttonsArray = [];
|
||||
}
|
||||
|
||||
@@ -6,76 +6,140 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="container">
|
||||
<mat-stepper
|
||||
orientation="vertical"
|
||||
[linear]="false"
|
||||
class="mat-elevation-z8"
|
||||
labelPosition="bottom"
|
||||
#savingProductStepper
|
||||
<div class="csa-container">
|
||||
<!-- Hero -->
|
||||
<div class="csa-header">
|
||||
<fa-icon icon="piggy-bank" class="csa-header__icon"></fa-icon>
|
||||
<div>
|
||||
<h2 class="csa-header__title">Modify Savings Account Application</h2>
|
||||
<div class="csa-header__sub">
|
||||
Review and update the account details, currency settings and digital channels before resubmitting.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wizard tabs -->
|
||||
<mat-tab-group
|
||||
class="csa-tabs"
|
||||
animationDuration="200ms"
|
||||
[preserveContent]="true"
|
||||
[(selectedIndex)]="selectedTabIndex"
|
||||
>
|
||||
<ng-template matStepperIcon="number">
|
||||
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="edit">
|
||||
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="done">
|
||||
<fa-icon icon="check" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="error">
|
||||
<fa-icon icon="exclamation-triangle" size="lg"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<ng-template matStepperIcon="preview">
|
||||
<fa-icon icon="eye" size="sm"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<mat-step [stepControl]="savingsAccountDetailsForm">
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.DETAILS' | translate }}</ng-template>
|
||||
|
||||
<mifosx-savings-account-details-step
|
||||
[savingsAccountTemplate]="savingsAccountAndTemplate"
|
||||
(savingsAccountProductTemplate)="setTemplate($event)"
|
||||
>
|
||||
</mifosx-savings-account-details-step>
|
||||
</mat-step>
|
||||
|
||||
<mat-step [stepControl]="savingsAccountTermsForm">
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template>
|
||||
|
||||
<mifosx-savings-account-terms-step
|
||||
[savingsAccountProductTemplate]="savingsAccountProductTemplate"
|
||||
[savingsAccountTemplate]="savingsAccountAndTemplate"
|
||||
></mifosx-savings-account-terms-step>
|
||||
</mat-step>
|
||||
|
||||
<mat-step>
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template>
|
||||
|
||||
<mifosx-savings-account-charges-step
|
||||
[savingsAccountProductTemplate]="savingsAccountProductTemplate"
|
||||
[savingsAccountTemplate]="savingsAccountAndTemplate"
|
||||
[currencyCode]="savingsAccountTermsForm.get('currencyCode')"
|
||||
>
|
||||
</mifosx-savings-account-charges-step>
|
||||
</mat-step>
|
||||
|
||||
@if (savingsAccountFormValidAndNotPristine) {
|
||||
<mat-step completed>
|
||||
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template>
|
||||
<mifosx-savings-account-preview-step
|
||||
[savingsAccountProductTemplate]="savingsAccountProductTemplate"
|
||||
<!-- Tab 1: Product Classification + Details -->
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="check-square" class="csa-tab__icon"></fa-icon>
|
||||
Product
|
||||
</ng-template>
|
||||
<div class="csa-tab-body">
|
||||
<mifosx-savings-account-details-step
|
||||
[savingsAccountTemplate]="savingsAccountAndTemplate"
|
||||
[savingsAccountTermsForm]="savingsAccountTermsForm"
|
||||
[savingsAccount]="savingsAccount"
|
||||
(submitEvent)="submit()"
|
||||
(savingsAccountProductTemplate)="setTemplate($event)"
|
||||
>
|
||||
</mifosx-savings-account-preview-step>
|
||||
</mat-step>
|
||||
}
|
||||
</mat-stepper>
|
||||
</mifosx-savings-account-details-step>
|
||||
|
||||
<div class="csa-actions">
|
||||
<span class="csa-actions__spacer"></span>
|
||||
<button mat-raised-button color="primary" [disabled]="!savingsAccountProductTemplate" (click)="next()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab 2: Currency -->
|
||||
<mat-tab [disabled]="!savingsAccountProductTemplate">
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="dollar-sign" class="csa-tab__icon"></fa-icon>
|
||||
Currency
|
||||
</ng-template>
|
||||
<div class="csa-tab-body">
|
||||
@if (savingsAccountProductTemplate) {
|
||||
<mifosx-savings-account-currency-step
|
||||
[savingsAccountProductTemplate]="savingsAccountProductTemplate"
|
||||
[primaryClientId]="savingsAccountAndTemplate?.clientId ?? null"
|
||||
[existingAccount]="savingsAccountAndTemplate"
|
||||
></mifosx-savings-account-currency-step>
|
||||
}
|
||||
|
||||
<div class="csa-actions">
|
||||
<button mat-stroked-button (click)="prev()">
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<span class="csa-actions__spacer"></span>
|
||||
<button mat-raised-button color="primary" (click)="next()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab 3: Digital -->
|
||||
<mat-tab [disabled]="!savingsAccountProductTemplate">
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="mobile-alt" class="csa-tab__icon"></fa-icon>
|
||||
Digital
|
||||
</ng-template>
|
||||
<div class="csa-tab-body">
|
||||
@if (savingsAccountProductTemplate) {
|
||||
<mifosx-savings-account-digital-step
|
||||
[savingsAccountProductTemplate]="savingsAccountProductTemplate"
|
||||
[existingAccount]="savingsAccountAndTemplate"
|
||||
></mifosx-savings-account-digital-step>
|
||||
}
|
||||
|
||||
<div class="csa-actions">
|
||||
<button mat-stroked-button (click)="prev()">
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<span class="csa-actions__spacer"></span>
|
||||
<button mat-raised-button color="primary" (click)="next()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab 4: Preview / Submit -->
|
||||
<mat-tab [disabled]="!savingsAccountProductTemplate">
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="eye" class="csa-tab__icon"></fa-icon>
|
||||
{{ 'labels.inputs.PREVIEW' | translate }}
|
||||
</ng-template>
|
||||
<div class="csa-tab-body">
|
||||
@if (savingsAccountProductTemplate) {
|
||||
<mifosx-savings-account-preview-step
|
||||
[savingsAccountProductTemplate]="savingsAccountProductTemplate"
|
||||
[savingsAccountTemplate]="savingsAccountAndTemplate"
|
||||
[savingsAccountTermsForm]="savingsAccountTermsForm"
|
||||
[savingsAccount]="savingsAccount"
|
||||
[savingsAccountDigital]="savingsAccountDigital"
|
||||
[savingsAccountNominees]="savingsAccountCurrencyStep?.nominees ?? []"
|
||||
(submitEvent)="submit()"
|
||||
>
|
||||
</mifosx-savings-account-preview-step>
|
||||
}
|
||||
|
||||
<div class="csa-actions">
|
||||
<button mat-stroked-button (click)="prev()">
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<span class="csa-actions__spacer"></span>
|
||||
<button mat-stroked-button [routerLink]="['../']">
|
||||
{{ 'labels.buttons.Cancel' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" (click)="submit()">
|
||||
<fa-icon icon="check" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Submit' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
|
||||
@@ -5,3 +5,164 @@
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
$navy: #0d1b2a;
|
||||
$blue: #1e88e5;
|
||||
$blue-d: #1e40af;
|
||||
$indigo: #4f46e5;
|
||||
$border: #e2e8f0;
|
||||
|
||||
.csa-container {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
/* ─── Hero header (matches Nexora pages) ────────────────────────────── */
|
||||
.csa-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&__icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, $blue 0%, $indigo 100%);
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
box-shadow: 0 2px 8px rgb(29 78 216 / 25%);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: $navy;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
margin-top: 2px;
|
||||
font-size: 12.5px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Wizard tab strip ──────────────────────────────────────────────── */
|
||||
.csa-tabs {
|
||||
background: #fff;
|
||||
border: 1px solid $border;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
|
||||
overflow: hidden;
|
||||
|
||||
::ng-deep {
|
||||
.mat-mdc-tab-header {
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid $border;
|
||||
}
|
||||
|
||||
.mat-mdc-tab {
|
||||
min-width: 140px;
|
||||
height: 50px;
|
||||
padding: 0 22px;
|
||||
font-size: 13px !important;
|
||||
font-weight: 600 !important;
|
||||
letter-spacing: 0.01em;
|
||||
color: #475569 !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab:hover:not(.mdc-tab--active, .mat-mdc-tab-disabled) {
|
||||
background: rgb(30 64 175 / 5%);
|
||||
color: $navy !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab.mdc-tab--active {
|
||||
.mdc-tab__text-label {
|
||||
color: $blue-d !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mdc-tab-indicator__content--underline {
|
||||
border-color: $blue-d !important;
|
||||
border-top-width: 3px !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab.mat-mdc-tab-disabled {
|
||||
opacity: 0.45 !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mat-mdc-tab-body-wrapper {
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
.mat-mdc-tab-body,
|
||||
.mat-mdc-tab-body-content {
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.csa-tab__icon {
|
||||
margin-right: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ─── Tab body + action bar ─────────────────────────────────────────── */
|
||||
.csa-tab-body {
|
||||
padding: 24px;
|
||||
|
||||
@media (width <= 768px) {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.csa-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
|
||||
&__spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 40px !important;
|
||||
border-radius: 10px !important;
|
||||
font-weight: 600 !important;
|
||||
padding: 0 18px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.m-l-10 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.m-r-10 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
button.preview-submit-row__primary,
|
||||
.mat-mdc-raised-button.mat-primary.preview-submit-row__primary,
|
||||
.preview-submit-row__primary {
|
||||
background: linear-gradient(135deg, #10b981 0%, #047857 100%) !important;
|
||||
|
||||
--mdc-protected-button-container-color: transparent !important;
|
||||
--mdc-filled-button-container-color: transparent !important;
|
||||
|
||||
color: #fff !important;
|
||||
box-shadow: 0 4px 12px -2px rgb(4 120 87 / 35%) !important;
|
||||
}
|
||||
|
||||
@@ -9,23 +9,26 @@
|
||||
/** Angular Imports */
|
||||
import { Component, ViewChild, inject } from '@angular/core';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { ClientsService } from 'app/clients/clients.service';
|
||||
|
||||
/** Custom Components */
|
||||
import { SavingsAccountDetailsStepComponent } from '../savings-account-stepper/savings-account-details-step/savings-account-details-step.component';
|
||||
import { SavingsAccountTermsStepComponent } from '../savings-account-stepper/savings-account-terms-step/savings-account-terms-step.component';
|
||||
import { SavingsAccountChargesStepComponent } from '../savings-account-stepper/savings-account-charges-step/savings-account-charges-step.component';
|
||||
import { SavingsAccountCurrencyStepComponent } from '../savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component';
|
||||
import { SavingsAccountDigitalStepComponent } from '../savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component';
|
||||
|
||||
/** Custom Services */
|
||||
import { SavingsService } from '../savings.service';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper';
|
||||
import { AlertService } from 'app/core/alert/alert.service';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { SavingsAccountPreviewStepComponent } from '../savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/**
|
||||
* Edit Savings Account Component
|
||||
* Edit Savings Account Component — same tab UI as Create, pre-populated with existing data.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-edit-savings-account',
|
||||
@@ -33,14 +36,11 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
styleUrls: ['./edit-savings-account.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatStepper,
|
||||
MatStepperIcon,
|
||||
MatTabsModule,
|
||||
FaIconComponent,
|
||||
MatStep,
|
||||
MatStepLabel,
|
||||
SavingsAccountDetailsStepComponent,
|
||||
SavingsAccountTermsStepComponent,
|
||||
SavingsAccountChargesStepComponent,
|
||||
SavingsAccountCurrencyStepComponent,
|
||||
SavingsAccountDigitalStepComponent,
|
||||
SavingsAccountPreviewStepComponent
|
||||
]
|
||||
})
|
||||
@@ -50,109 +50,135 @@ export class EditSavingsAccountComponent {
|
||||
private dateUtils = inject(Dates);
|
||||
private savingsService = inject(SavingsService);
|
||||
private settingsService = inject(SettingsService);
|
||||
private alertService = inject(AlertService);
|
||||
private clientsService = inject(ClientsService);
|
||||
|
||||
/** Savings Account Template */
|
||||
/** Full account data + template from resolver */
|
||||
savingsAccountAndTemplate: any;
|
||||
/** Savings Account Product Template */
|
||||
/** Product template emitted by details step */
|
||||
savingsAccountProductTemplate: any;
|
||||
/** Currently visible wizard tab (0-indexed). */
|
||||
selectedTabIndex = 0;
|
||||
private readonly totalTabs = 4;
|
||||
|
||||
/** Savings Account Details Step */
|
||||
@ViewChild(SavingsAccountDetailsStepComponent, { static: true })
|
||||
savingsAccountDetailsStep: SavingsAccountDetailsStepComponent;
|
||||
/** Savings Account Terms Step */
|
||||
@ViewChild(SavingsAccountTermsStepComponent, { static: true })
|
||||
savingsAccountTermsStep: SavingsAccountTermsStepComponent;
|
||||
/** Savings Account Charges Step */
|
||||
@ViewChild(SavingsAccountChargesStepComponent, { static: true })
|
||||
savingsAccountChargesStep: SavingsAccountChargesStepComponent;
|
||||
|
||||
/**
|
||||
* Fetches savings account template from `resolve`
|
||||
* @param {ActivatedRoute} route Activated Route
|
||||
* @param {Router} router Router
|
||||
* @param {Dates} dateUtils Date Utils
|
||||
* @param {SavingsService} savingsService Savings Service
|
||||
* @param {SettingsService} settingsService Settings Service
|
||||
*/
|
||||
@ViewChild(SavingsAccountCurrencyStepComponent)
|
||||
savingsAccountCurrencyStep: SavingsAccountCurrencyStepComponent;
|
||||
|
||||
@ViewChild(SavingsAccountDigitalStepComponent)
|
||||
savingsAccountDigitalStep: SavingsAccountDigitalStepComponent;
|
||||
|
||||
constructor() {
|
||||
this.route.data.subscribe((data: { savingsAccountAndTemplate: any }) => {
|
||||
this.savingsAccountAndTemplate = data.savingsAccountAndTemplate;
|
||||
const account = data.savingsAccountAndTemplate;
|
||||
this.savingsAccountAndTemplate = account;
|
||||
if (account?.clientId && account?.accountNo) {
|
||||
this.clientsService
|
||||
.getClientNominees(String(account.clientId), account.accountNo, 'SB')
|
||||
.subscribe((nominees: any[]) => {
|
||||
if (Array.isArray(nominees) && nominees.length > 0) {
|
||||
this.savingsAccountAndTemplate = { ...this.savingsAccountAndTemplate, nominees };
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets savings account product template.
|
||||
* @param {any} $event API response
|
||||
*/
|
||||
setTemplate($event: any) {
|
||||
this.savingsAccountProductTemplate = $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves savings account details form.
|
||||
*/
|
||||
next(): void {
|
||||
if (this.selectedTabIndex < this.totalTabs - 1) {
|
||||
this.selectedTabIndex += 1;
|
||||
}
|
||||
}
|
||||
|
||||
prev(): void {
|
||||
if (this.selectedTabIndex > 0) {
|
||||
this.selectedTabIndex -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
get savingsAccountDetailsForm() {
|
||||
return this.savingsAccountDetailsStep.savingsAccountDetailsForm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves savings account terms form.
|
||||
*/
|
||||
get savingsAccountTermsForm() {
|
||||
return this.savingsAccountTermsStep.savingsAccountTermsForm;
|
||||
get savingsAccountTermsForm(): FormGroup {
|
||||
return new FormGroup({});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks validity and pristinity of overall savings account form .
|
||||
*/
|
||||
get savingsAccountFormValidAndNotPristine() {
|
||||
return (
|
||||
this.savingsAccountDetailsForm.valid &&
|
||||
this.savingsAccountTermsForm.valid &&
|
||||
(!this.savingsAccountDetailsForm.pristine ||
|
||||
!this.savingsAccountTermsForm.pristine ||
|
||||
!this.savingsAccountChargesStep.pristine)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves savings account object.
|
||||
*/
|
||||
get savingsAccount() {
|
||||
return {
|
||||
...this.savingsAccountDetailsStep.savingsAccountDetails,
|
||||
...this.savingsAccountTermsStep.savingsAccountTerms,
|
||||
...this.savingsAccountChargesStep.savingsAccountCharges
|
||||
};
|
||||
return { ...this.savingsAccountDetailsStep.savingsAccountDetails };
|
||||
}
|
||||
|
||||
get savingsAccountDigital() {
|
||||
return this.savingsAccountDigitalStep?.savingsAccountDigital ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new share account.
|
||||
*/
|
||||
submit() {
|
||||
// TODO: Update once language and date settings are setup
|
||||
const detailsStep = this.savingsAccountDetailsStep;
|
||||
if (detailsStep.selectedAccountStatus === 'JOINT' && detailsStep.totalJointSharePercentage !== 100) {
|
||||
this.alertService.alert({
|
||||
type: 'Error',
|
||||
message: `Total ownership is ${detailsStep.totalJointSharePercentage}%. It must equal 100% before submitting.`
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this.savingsAccountProductTemplate?.nominationFacility &&
|
||||
(this.savingsAccountCurrencyStep?.nominees ?? []).length === 0
|
||||
) {
|
||||
this.alertService.alert({
|
||||
type: 'Error',
|
||||
message: 'Nomination Facility is enabled for this product. Please add at least one nominee before submitting.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.savingsAccountProductTemplate?.introducerRequired && !this.savingsAccountDetailsStep.introducerId) {
|
||||
this.alertService.alert({
|
||||
type: 'Error',
|
||||
message: 'Introducer is required for this product. Please select an introducer before submitting.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
const locale = this.settingsService.language.code;
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
const monthDayFormat = 'dd MMMM';
|
||||
const savingsAccount = {
|
||||
const savingsAccount: any = {
|
||||
...this.savingsAccount,
|
||||
charges: this.savingsAccount.charges.map((charge: any) => ({
|
||||
chargeId: charge.id,
|
||||
amount: charge.amount,
|
||||
dueDate: this.dateUtils.formatDate(charge.dueDate, dateFormat),
|
||||
feeOnMonthDay: this.dateUtils.formatDate(charge.feeOnMonthDay, dateFormat),
|
||||
feeInterval: charge.feeInterval
|
||||
})),
|
||||
charges: [],
|
||||
submittedOnDate: this.dateUtils.formatDate(this.savingsAccount.submittedOnDate, dateFormat),
|
||||
dateFormat,
|
||||
monthDayFormat,
|
||||
locale
|
||||
};
|
||||
|
||||
if (this.savingsAccountDigital?.accountFacilityConfig) {
|
||||
savingsAccount.accountFacilityConfig = this.savingsAccountDigital.accountFacilityConfig;
|
||||
}
|
||||
|
||||
if (this.savingsAccountAndTemplate.clientId) {
|
||||
savingsAccount.clientId = this.savingsAccountAndTemplate.clientId;
|
||||
} else {
|
||||
savingsAccount.groupId = this.savingsAccountAndTemplate.groupId;
|
||||
}
|
||||
|
||||
const nominees = this.savingsAccountCurrencyStep?.nominees ?? [];
|
||||
if (nominees.length > 0) {
|
||||
savingsAccount.nominees = nominees.map((n: any) => ({
|
||||
nomineeClientId: n.nomineeClientId,
|
||||
nominationType: n.nominationType,
|
||||
relationshipId: n.relationshipId,
|
||||
percentageShare: n.percentageShare,
|
||||
nomineeIsMinor: n.nomineeIsMinor,
|
||||
...(n.guardianClientId ? { guardianClientId: n.guardianClientId } : {}),
|
||||
remarks: n.remarks
|
||||
}));
|
||||
}
|
||||
|
||||
this.savingsService
|
||||
.updateSavingsAccount(this.savingsAccountAndTemplate.id, savingsAccount)
|
||||
.subscribe((response: any) => {
|
||||
|
||||
+29
-2
@@ -6,7 +6,7 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component, Input, ViewChild } from '@angular/core';
|
||||
import { Component, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { NomineeFormComponent } from 'app/shared/nominee-form/nominee-form.component';
|
||||
import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dormancy-alert-panel.component';
|
||||
@@ -21,9 +21,10 @@ import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dor
|
||||
DormancyAlertPanelComponent
|
||||
]
|
||||
})
|
||||
export class SavingsAccountCurrencyStepComponent {
|
||||
export class SavingsAccountCurrencyStepComponent implements OnChanges {
|
||||
@Input() savingsAccountProductTemplate: any;
|
||||
@Input() primaryClientId: number | null = null;
|
||||
@Input() existingAccount: any = null;
|
||||
@ViewChild(NomineeFormComponent) nomineeFormComp: NomineeFormComponent;
|
||||
|
||||
nominees: any[] = [];
|
||||
@@ -31,6 +32,32 @@ export class SavingsAccountCurrencyStepComponent {
|
||||
showNomineeForm = false;
|
||||
nomineeError = '';
|
||||
|
||||
private existingAccountApplied = false;
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['existingAccount']?.currentValue) {
|
||||
const ea = changes['existingAccount'].currentValue;
|
||||
this.existingAccountApplied = true;
|
||||
const nominees =
|
||||
Array.isArray(ea.nominees) && ea.nominees.length > 0 ? ea.nominees : this.parseNomineeData(ea.nomineeData);
|
||||
if (nominees.length > 0) {
|
||||
this.nominees = [...nominees];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private parseNomineeData(nomineeData: any): any[] {
|
||||
if (!nomineeData) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const parsed = typeof nomineeData === 'string' ? JSON.parse(nomineeData) : nomineeData;
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
get totalShare(): number {
|
||||
return this.nominees.reduce((sum, n) => sum + (+n.percentageShare || 0), 0);
|
||||
}
|
||||
|
||||
+4
-1
@@ -91,7 +91,7 @@
|
||||
<mat-label>Account No</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[value]="productTemplate?.previewAccountNo || savingsAccountTemplate?.accountNo || 'Auto-generated'"
|
||||
[value]="savingsAccountTemplate?.accountNo || productTemplate?.previewAccountNo || 'Auto-generated'"
|
||||
readonly
|
||||
/>
|
||||
</mat-form-field>
|
||||
@@ -127,6 +127,9 @@
|
||||
@if (productTemplate?.introducerRequired) {
|
||||
<mifosx-introducer-search
|
||||
[excludedClientIds]="primaryClientId ? [primaryClientId] : []"
|
||||
[preselectedIntroducer]="
|
||||
introducerId ? { clientId: introducerId, displayName: introducerName, cifNumber: introducerCif } : null
|
||||
"
|
||||
(introducerChange)="onIntroducerChange($event)"
|
||||
></mifosx-introducer-search>
|
||||
}
|
||||
|
||||
+104
-5
@@ -65,6 +65,7 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
|
||||
introducerId: number | null = null;
|
||||
introducerName: string | null = null;
|
||||
introducerCif: string | null = null;
|
||||
private isEditMode = false;
|
||||
|
||||
@Output() savingsAccountProductTemplate = new EventEmitter();
|
||||
|
||||
@@ -166,11 +167,25 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
|
||||
this.buildDependencies();
|
||||
if (this.savingsAccountTemplate) {
|
||||
this.productData = this.savingsAccountTemplate.productOptions;
|
||||
if (this.savingsAccountTemplate.savingsProductId) {
|
||||
const existingProductId = this.savingsAccountTemplate.savingsProductId ?? this.savingsAccountTemplate.productId;
|
||||
// Edit mode: account response always has accountNo
|
||||
if (this.savingsAccountTemplate.accountNo) {
|
||||
this.isEditMode = true;
|
||||
}
|
||||
if ((!this.productData || this.productData.length === 0) && existingProductId) {
|
||||
this.isEditMode = true;
|
||||
this.productData = [
|
||||
{
|
||||
id: existingProductId,
|
||||
name: this.savingsAccountTemplate.productName ?? this.savingsAccountTemplate.savingsProductName ?? 'Product'
|
||||
}
|
||||
];
|
||||
}
|
||||
if (existingProductId) {
|
||||
this.savingsAccountDetailsForm.patchValue({
|
||||
productId: this.savingsAccountTemplate.savingsProductId,
|
||||
productId: existingProductId,
|
||||
submittedOnDate:
|
||||
this.savingsAccountTemplate.timeline.submittedOnDate &&
|
||||
this.savingsAccountTemplate.timeline?.submittedOnDate &&
|
||||
new Date(this.savingsAccountTemplate.timeline.submittedOnDate)
|
||||
});
|
||||
} else {
|
||||
@@ -280,6 +295,9 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
|
||||
|
||||
/** Auto-set status tile based on age; pre-fill minorDob if minor */
|
||||
private autoDetectAccountStatus(): void {
|
||||
if (this.isEditMode) {
|
||||
return;
|
||||
}
|
||||
if (this.clientAge === null) return;
|
||||
|
||||
if (this.clientAge < 18) {
|
||||
@@ -319,8 +337,6 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
|
||||
this.customerSegmentOptions = this.extractOptions(response.customerSegmentOptions, response.customerSegment);
|
||||
this.selectedAccountType = response.accountType ?? null;
|
||||
this.selectedCustomerSegment = response.customerSegment ?? null;
|
||||
// Override the product's default segment with the one inferred
|
||||
// from the client's entity type when an option matches.
|
||||
this.applyInferredCustomerSegment();
|
||||
this.savingsAccountProductTemplate.emit(response);
|
||||
this.fieldOfficerData = response.fieldOfficerOptions;
|
||||
@@ -332,10 +348,93 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
|
||||
} else {
|
||||
this.savingsAccountDetailsForm.get('fieldOfficerId').patchValue('');
|
||||
}
|
||||
// === Restore existing account values when in edit/modify mode ===
|
||||
this.restoreExistingAccountValues();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private restoreExistingAccountValues(): void {
|
||||
const ea = this.savingsAccountTemplate;
|
||||
if (!ea?.savingsProductId && !ea?.productId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const strVal = (v: any): string => (typeof v === 'object' && v !== null ? (v.value ?? '') : (v ?? ''));
|
||||
|
||||
const accountStatus = strVal(ea.accountStatus);
|
||||
if (accountStatus) {
|
||||
this.selectedAccountStatus = accountStatus;
|
||||
}
|
||||
|
||||
const customerSegment = strVal(ea.customerSegment);
|
||||
if (customerSegment) {
|
||||
this.selectedCustomerSegment = customerSegment;
|
||||
}
|
||||
|
||||
const accountType = ea.cbsAccountType ? strVal(ea.cbsAccountType) : null;
|
||||
if (accountType) {
|
||||
this.selectedAccountType = accountType;
|
||||
}
|
||||
|
||||
if (accountStatus === 'JOINT') {
|
||||
const modeOfOp = strVal(ea.modeOfOperation);
|
||||
if (modeOfOp) {
|
||||
this.modeOfOperation = modeOfOp;
|
||||
}
|
||||
if (Array.isArray(ea.jointHolders) && ea.jointHolders.length > 0) {
|
||||
const isPH = (h: any) => h.isPrimary ?? h.isprimary ?? false;
|
||||
const primary = ea.jointHolders.find((h: any) => isPH(h));
|
||||
const ps = primary?.sharePercentage ?? primary?.sharepercentage;
|
||||
if (ps) {
|
||||
this.primaryHolderShare = +ps;
|
||||
}
|
||||
this.jointHolders = ea.jointHolders
|
||||
.filter((h: any) => !isPH(h))
|
||||
.map((h: any) => ({
|
||||
clientId: h.clientId ?? h.clientid,
|
||||
clientName: h.displayName ?? h.displayname ?? h.clientName ?? '',
|
||||
clientCif: h.cifNumber ?? h.cifnumber ?? h.clientCif ?? '',
|
||||
sharePercentage: +(h.sharePercentage ?? h.sharepercentage ?? 0),
|
||||
isMandatory: !!(h.isMandatory ?? h.ismandatory)
|
||||
}));
|
||||
}
|
||||
}
|
||||
if (accountStatus === 'INDIVIDUAL_MALE' && ea.specialCategory) {
|
||||
this.selectedSpecialCategory = ea.specialCategory;
|
||||
}
|
||||
if (accountStatus === 'INDIVIDUAL_FEMALE' && ea.weakerSection) {
|
||||
this.selectedWeakerSection = ea.weakerSection;
|
||||
}
|
||||
if (accountStatus === 'MINOR') {
|
||||
if (ea.guardianClientId) {
|
||||
this.guardianClientId = ea.guardianClientId;
|
||||
}
|
||||
if (ea.guardianName) {
|
||||
this.guardianName = ea.guardianName;
|
||||
}
|
||||
if (ea.guardianCifNumber) {
|
||||
this.guardianCif = ea.guardianCifNumber;
|
||||
}
|
||||
if (ea.guardianRelationship) {
|
||||
this.guardianRelationship = ea.guardianRelationship;
|
||||
}
|
||||
if (ea.minorDateOfBirth) {
|
||||
const d = new Date(ea.minorDateOfBirth);
|
||||
if (!isNaN(d.getTime())) {
|
||||
this.minorDob = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
const introId = ea.introducerId ?? ea.introducerClientId;
|
||||
if (introId) {
|
||||
this.introducerId = introId;
|
||||
this.introducerName = ea.introducerName ?? null;
|
||||
this.introducerCif = ea.introducerCifNumber ?? null;
|
||||
}
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
|
||||
private extractOptions(options: any[], fallback?: string): string[] {
|
||||
if (Array.isArray(options) && options.length > 0) {
|
||||
return options.map((o: any) => o.value ?? o.name ?? o);
|
||||
|
||||
+10
-1
@@ -40,10 +40,19 @@ const DEFAULT_FACILITY_CONFIG = {
|
||||
})
|
||||
export class SavingsAccountDigitalStepComponent {
|
||||
facilityConfig = { ...DEFAULT_FACILITY_CONFIG };
|
||||
private hasExistingAccount = false;
|
||||
|
||||
@Input()
|
||||
set existingAccount(value: any) {
|
||||
if (value?.accountFacilityConfig) {
|
||||
this.hasExistingAccount = true;
|
||||
this.facilityConfig = { ...DEFAULT_FACILITY_CONFIG, ...value.accountFacilityConfig };
|
||||
}
|
||||
}
|
||||
|
||||
@Input()
|
||||
set savingsAccountProductTemplate(value: any) {
|
||||
if (value?.accountFacilityConfig) {
|
||||
if (!this.hasExistingAccount && value?.accountFacilityConfig) {
|
||||
this.facilityConfig = { ...DEFAULT_FACILITY_CONFIG, ...value.accountFacilityConfig };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +142,16 @@ export class SavingsButtonsConfiguration {
|
||||
}
|
||||
];
|
||||
break;
|
||||
case 'Rejected':
|
||||
case 'rejected':
|
||||
this.buttonsArray = [
|
||||
{
|
||||
name: 'Modify Application',
|
||||
icon: 'pen',
|
||||
taskPermissionName: 'UPDATE_SAVINGSACCOUNT'
|
||||
}
|
||||
];
|
||||
break;
|
||||
default:
|
||||
this.buttonsArray = [];
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component, Input, Output, EventEmitter, inject } from '@angular/core';
|
||||
import { Component, Input, Output, EventEmitter, inject, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { ClientsService } from 'app/clients/clients.service';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
@@ -27,11 +27,12 @@ import { FormsModule } from '@angular/forms';
|
||||
MatIcon
|
||||
]
|
||||
})
|
||||
export class IntroducerSearchComponent {
|
||||
export class IntroducerSearchComponent implements OnChanges {
|
||||
private clientsService = inject(ClientsService);
|
||||
private sanitizer = inject(DomSanitizer);
|
||||
|
||||
@Input() excludedClientIds: number[] = [];
|
||||
@Input() preselectedIntroducer: { clientId: number; displayName: string; cifNumber?: string } | null = null;
|
||||
@Output() introducerChange = new EventEmitter<{ clientId: number; displayName: string; cifNumber?: string } | null>();
|
||||
|
||||
entryMode: 'linked' = 'linked';
|
||||
@@ -50,6 +51,14 @@ export class IntroducerSearchComponent {
|
||||
|
||||
private searchTimer: any;
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['preselectedIntroducer']?.currentValue && !this.selectedClient) {
|
||||
const val = changes['preselectedIntroducer'].currentValue;
|
||||
this.selectedClient = { id: val.clientId, displayName: val.displayName, cifNumber: val.cifNumber ?? '' };
|
||||
this.fetchLinkedClientData(String(val.clientId));
|
||||
}
|
||||
}
|
||||
|
||||
onSearch(text: string): void {
|
||||
this.searchText = text;
|
||||
clearTimeout(this.searchTimer);
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
/* tslint:disable */
|
||||
export default {
|
||||
'mifos_x': {
|
||||
'version': '260527',
|
||||
'hash': '33b01d66'
|
||||
'version': '260602',
|
||||
'hash': '3be2f962'
|
||||
},
|
||||
'allow_switching_backend_instance': true
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user