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:
Jaanu423
2026-06-03 10:36:40 +05:30
co-authored by Claude Sonnet 4.6
parent 3be2f96210
commit 260fabaa48
37 changed files with 2744 additions and 730 deletions
@@ -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);
}
@@ -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>
}
@@ -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);
@@ -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 };
}
}