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