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
+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;
|
||||
|
||||
Reference in New Issue
Block a user