DDS: Agent settlement (Agent Tran), Close Party account UI, reports UI, Party-Savings link field
This commit is contained in:
@@ -0,0 +1,527 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
/** Angular Imports */
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { UntypedFormBuilder, UntypedFormGroup, UntypedFormArray, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { MatCheckbox } from '@angular/material/checkbox';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/** Custom Services */
|
||||
import { OrganizationService } from 'app/organization/organization.service';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
|
||||
/**
|
||||
* Party Master Entry component.
|
||||
*
|
||||
* The customer's DDS account — linked to an Agent for daily collection,
|
||||
* carrying its own scheme terms, KYC, two-stage overdue flags, and a
|
||||
* nominee list. Modelled directly on the legacy Party Master Entry screen
|
||||
* this replaces.
|
||||
*
|
||||
* Backed by its own DDS Party API (`/dds/parties`). The underlying account
|
||||
* does not yet reuse the Recurring Deposit account engine (see Product
|
||||
* Setup) — that linkage is a follow-up once the RD-account-specific fields
|
||||
* this screen doesn't collect (interest/compounding periods, chart
|
||||
* selection, ...) have a home. Until then the scheme/balance fields here
|
||||
* are the Party's own.
|
||||
*
|
||||
* "Close Account" (command=close) permanently closes the Party once its
|
||||
* balance is zero - the backend rejects the command otherwise. A closed
|
||||
* Party can no longer be edited or receive new Party Tran collections.
|
||||
*
|
||||
* savingsAccountNo optionally links this Party to an existing, non-closed real Savings Account of
|
||||
* the same client (resolved by account number, since that's what an officer has to hand) -
|
||||
* clarified with the business (2026-08-24) as "link it if the client already has one, otherwise
|
||||
* leave it". There is no unlink once linked.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-party-master',
|
||||
templateUrl: './party-master.component.html',
|
||||
styleUrls: ['./party-master.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatTabsModule,
|
||||
MatCheckbox,
|
||||
MatRadioModule
|
||||
]
|
||||
})
|
||||
export class PartyMasterComponent {
|
||||
private formBuilder = inject(UntypedFormBuilder);
|
||||
private router = inject(Router);
|
||||
private organizationService = inject(OrganizationService);
|
||||
private settingsService = inject(SettingsService);
|
||||
private dateUtils = inject(Dates);
|
||||
|
||||
partyId: number | null = null;
|
||||
selectedTabIndex = 0;
|
||||
partyStatus = 'ACTIVE';
|
||||
linkedSavingsAccountId: number | null = null;
|
||||
|
||||
get isClosed(): boolean {
|
||||
return this.partyStatus === 'CLOSED';
|
||||
}
|
||||
|
||||
subTypes = [
|
||||
'Daily Deposit Scheme',
|
||||
'Masik Dhan Varsha Deposit'
|
||||
];
|
||||
operations = [
|
||||
'Self',
|
||||
'Guardian',
|
||||
'Joint'
|
||||
];
|
||||
occupations = [
|
||||
'Business',
|
||||
'Service',
|
||||
'Agriculture',
|
||||
'Homemaker',
|
||||
'Student',
|
||||
'Retired',
|
||||
'Other'
|
||||
];
|
||||
riskLevels = [
|
||||
'Low',
|
||||
'Medium',
|
||||
'High'
|
||||
];
|
||||
adultMinorOptions = [
|
||||
'Adult',
|
||||
'Minor'
|
||||
];
|
||||
withdrawTypes = [
|
||||
'Cash',
|
||||
'Transfer',
|
||||
'Clearing'
|
||||
];
|
||||
freezeCodes = [
|
||||
'Normal',
|
||||
'Debit Freeze',
|
||||
'Credit Freeze',
|
||||
'Total Freeze'
|
||||
];
|
||||
advCodes = [
|
||||
'None',
|
||||
'Transfer To Saving',
|
||||
'Renew'
|
||||
];
|
||||
|
||||
partyForm: UntypedFormGroup = this.formBuilder.group({
|
||||
clientId: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
subType: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
agentId: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
savingsAccountNo: [''],
|
||||
lastAccount: [
|
||||
{
|
||||
value: '',
|
||||
disabled: true
|
||||
}
|
||||
],
|
||||
partyCode: [
|
||||
{
|
||||
value: '',
|
||||
disabled: true
|
||||
}
|
||||
],
|
||||
nominal: [
|
||||
'No',
|
||||
Validators.required
|
||||
],
|
||||
aadharNo: [''],
|
||||
partyName: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
partyName1: [''],
|
||||
address1: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
address2: [''],
|
||||
address3: [''],
|
||||
city: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
pincode: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
gender: [''],
|
||||
panCardNo: [''],
|
||||
telephone: [''],
|
||||
mobileNoSms: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
operation: [
|
||||
'Self',
|
||||
Validators.required
|
||||
],
|
||||
birthDate: [''],
|
||||
occupation: [''],
|
||||
risk: [''],
|
||||
ckycId: [''],
|
||||
adultMinor: [
|
||||
'Adult',
|
||||
Validators.required
|
||||
],
|
||||
minorDate: [''],
|
||||
guardianClientId: [''],
|
||||
minAccountOpt: [''],
|
||||
relation: [''],
|
||||
openingDate: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
month: [''],
|
||||
maturityDate: [
|
||||
{
|
||||
value: '',
|
||||
disabled: true
|
||||
}
|
||||
],
|
||||
reopenDate: [''],
|
||||
depositAmount: [0],
|
||||
dailyDepositAmount: [
|
||||
0,
|
||||
Validators.required
|
||||
],
|
||||
days: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
ledgerNo: [
|
||||
{
|
||||
value: '',
|
||||
disabled: true
|
||||
}
|
||||
],
|
||||
maturityAmount: [
|
||||
{
|
||||
value: 0,
|
||||
disabled: true
|
||||
}
|
||||
],
|
||||
interestRate: [
|
||||
{
|
||||
value: 0,
|
||||
disabled: true
|
||||
}
|
||||
],
|
||||
withdrawType: [''],
|
||||
od1Flag: [
|
||||
{
|
||||
value: false,
|
||||
disabled: true
|
||||
}
|
||||
],
|
||||
od2Flag: [
|
||||
{
|
||||
value: false,
|
||||
disabled: true
|
||||
}
|
||||
],
|
||||
lien: ['No'],
|
||||
payDate: [''],
|
||||
paymentAmount: [0],
|
||||
currentBalance: [
|
||||
{
|
||||
value: 0,
|
||||
disabled: true
|
||||
}
|
||||
],
|
||||
openingBalance: [0],
|
||||
isSmsActive: [true],
|
||||
isEmailActive: [false],
|
||||
interestCalc: [
|
||||
{
|
||||
value: 0,
|
||||
disabled: true
|
||||
}
|
||||
],
|
||||
interestDate: [''],
|
||||
spDescription: [''],
|
||||
freezeCode: ['Normal'],
|
||||
advCode: ['None'],
|
||||
creditToBranch: [''],
|
||||
creditToAccountType: [''],
|
||||
creditToAccount: [''],
|
||||
nomineeApplicable: ['No'],
|
||||
nomineeSplitMode: ['Percentage'],
|
||||
nominees: this.formBuilder.array([])
|
||||
});
|
||||
|
||||
get nominees(): UntypedFormArray {
|
||||
return this.partyForm.get('nominees') as UntypedFormArray;
|
||||
}
|
||||
|
||||
private buildNomineeGroup(): UntypedFormGroup {
|
||||
return this.formBuilder.group({
|
||||
nominationDate: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
name: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
mobileNo: [''],
|
||||
birthDate: [''],
|
||||
minor: [
|
||||
'No',
|
||||
Validators.required
|
||||
],
|
||||
relation: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
gender: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
guardianName: ['']
|
||||
});
|
||||
}
|
||||
|
||||
addNominee(): void {
|
||||
this.nominees.push(this.buildNomineeGroup());
|
||||
}
|
||||
|
||||
removeNominee(index: number): void {
|
||||
this.nominees.removeAt(index);
|
||||
}
|
||||
|
||||
retrieve(): void {
|
||||
const clientId = this.partyForm.get('clientId')?.value;
|
||||
if (!clientId) {
|
||||
return;
|
||||
}
|
||||
this.organizationService.getParties().subscribe((parties: any[]) => {
|
||||
const match = (parties || []).find((party) => String(party.clientId) === String(clientId));
|
||||
if (match) {
|
||||
this.patchFormFromParty(match);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.partyId = null;
|
||||
this.partyStatus = 'ACTIVE';
|
||||
this.linkedSavingsAccountId = null;
|
||||
this.partyForm.reset();
|
||||
this.partyForm.patchValue({
|
||||
nominal: 'No',
|
||||
operation: 'Self',
|
||||
adultMinor: 'Adult',
|
||||
lien: 'No',
|
||||
isSmsActive: true,
|
||||
isEmailActive: false,
|
||||
freezeCode: 'Normal',
|
||||
advCode: 'None',
|
||||
nomineeApplicable: 'No',
|
||||
nomineeSplitMode: 'Percentage',
|
||||
currentBalance: 0,
|
||||
maturityAmount: 0,
|
||||
interestRate: 0
|
||||
});
|
||||
this.nominees.clear();
|
||||
}
|
||||
|
||||
exit(): void {
|
||||
this.router.navigate(['/accounts/savings']);
|
||||
}
|
||||
|
||||
save(): void {
|
||||
const raw = this.partyForm.getRawValue();
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
const locale = this.settingsService.language.code;
|
||||
const asDate = (value: any) => (value ? this.dateUtils.formatDate(value, dateFormat) : undefined);
|
||||
|
||||
const payload: any = {
|
||||
clientId: Number(raw.clientId),
|
||||
agentId: Number(raw.agentId),
|
||||
savingsAccountNo: this.linkedSavingsAccountId ? undefined : raw.savingsAccountNo || undefined,
|
||||
subType: raw.subType,
|
||||
lastAccount: raw.lastAccount || undefined,
|
||||
nominal: raw.nominal === 'Yes',
|
||||
aadharNo: raw.aadharNo || undefined,
|
||||
partyName: raw.partyName,
|
||||
partyName1: raw.partyName1 || undefined,
|
||||
address1: raw.address1,
|
||||
address2: raw.address2 || undefined,
|
||||
address3: raw.address3 || undefined,
|
||||
city: raw.city,
|
||||
pincode: raw.pincode,
|
||||
gender: raw.gender || undefined,
|
||||
panCardNo: raw.panCardNo || undefined,
|
||||
telephone: raw.telephone || undefined,
|
||||
mobileNoSms: raw.mobileNoSms,
|
||||
operation: raw.operation,
|
||||
birthDate: asDate(raw.birthDate),
|
||||
occupation: raw.occupation || undefined,
|
||||
risk: raw.risk || undefined,
|
||||
ckycId: raw.ckycId || undefined,
|
||||
adultMinor: raw.adultMinor,
|
||||
minorDate: asDate(raw.minorDate),
|
||||
guardianClientId: raw.guardianClientId ? Number(raw.guardianClientId) : undefined,
|
||||
minAccountOpt: raw.minAccountOpt || undefined,
|
||||
relation: raw.relation || undefined,
|
||||
openingDate: asDate(raw.openingDate),
|
||||
month: raw.month || undefined,
|
||||
reopenDate: asDate(raw.reopenDate),
|
||||
depositAmount: raw.depositAmount || undefined,
|
||||
dailyDepositAmount: raw.dailyDepositAmount,
|
||||
days: raw.days,
|
||||
withdrawType: raw.withdrawType || undefined,
|
||||
lien: raw.lien === 'Yes',
|
||||
payDate: asDate(raw.payDate),
|
||||
paymentAmount: raw.paymentAmount || undefined,
|
||||
openingBalance: raw.openingBalance || 0,
|
||||
isSmsActive: !!raw.isSmsActive,
|
||||
isEmailActive: !!raw.isEmailActive,
|
||||
interestDate: asDate(raw.interestDate),
|
||||
spDescription: raw.spDescription || undefined,
|
||||
freezeCode: raw.freezeCode || undefined,
|
||||
advCode: raw.advCode || undefined,
|
||||
creditToBranch: raw.creditToBranch || undefined,
|
||||
creditToAccountType: raw.creditToAccountType || undefined,
|
||||
creditToAccount: raw.creditToAccount || undefined,
|
||||
nomineeApplicable: raw.nomineeApplicable === 'Yes',
|
||||
nomineeSplitMode: raw.nomineeSplitMode || undefined,
|
||||
dateFormat,
|
||||
locale,
|
||||
nominees: (raw.nominees || []).map((nominee: any) => ({
|
||||
nominationDate: this.dateUtils.getDate(nominee.nominationDate),
|
||||
name: nominee.name,
|
||||
mobileNo: nominee.mobileNo || undefined,
|
||||
birthDate: nominee.birthDate ? this.dateUtils.getDate(nominee.birthDate) : undefined,
|
||||
minor: nominee.minor === 'Yes',
|
||||
relation: nominee.relation,
|
||||
gender: nominee.gender,
|
||||
guardianName: nominee.guardianName || undefined
|
||||
}))
|
||||
};
|
||||
|
||||
if (this.partyId) {
|
||||
this.organizationService.updateParty(this.partyId, payload).subscribe(() => this.exit());
|
||||
} else {
|
||||
this.organizationService.createParty(payload).subscribe(() => this.exit());
|
||||
}
|
||||
}
|
||||
|
||||
closeAccount(): void {
|
||||
if (!this.partyId) {
|
||||
return;
|
||||
}
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
const locale = this.settingsService.language.code;
|
||||
this.organizationService
|
||||
.closeParty(this.partyId, {
|
||||
closeDate: this.dateUtils.formatDate(new Date(), dateFormat),
|
||||
dateFormat,
|
||||
locale
|
||||
})
|
||||
.subscribe(() => this.exit());
|
||||
}
|
||||
|
||||
private patchFormFromParty(party: any): void {
|
||||
this.partyId = party.id;
|
||||
this.partyStatus = party.status || 'ACTIVE';
|
||||
this.linkedSavingsAccountId = party.savingsAccountId || null;
|
||||
this.partyForm.patchValue({
|
||||
clientId: party.clientId,
|
||||
subType: party.subType,
|
||||
agentId: party.agentId,
|
||||
partyCode: party.partyCode,
|
||||
lastAccount: party.lastAccount,
|
||||
nominal: party.nominal ? 'Yes' : 'No',
|
||||
aadharNo: party.aadharNo,
|
||||
partyName: party.partyName,
|
||||
partyName1: party.partyName1,
|
||||
address1: party.address1,
|
||||
address2: party.address2,
|
||||
address3: party.address3,
|
||||
city: party.city,
|
||||
pincode: party.pincode,
|
||||
gender: party.gender,
|
||||
panCardNo: party.panCardNo,
|
||||
telephone: party.telephone,
|
||||
mobileNoSms: party.mobileNoSms,
|
||||
operation: party.operation,
|
||||
birthDate: party.birthDate ? new Date(party.birthDate) : null,
|
||||
occupation: party.occupation,
|
||||
risk: party.risk,
|
||||
ckycId: party.ckycId,
|
||||
adultMinor: party.adultMinor,
|
||||
minorDate: party.minorDate ? new Date(party.minorDate) : null,
|
||||
guardianClientId: party.guardianClientId,
|
||||
minAccountOpt: party.minAccountOpt,
|
||||
relation: party.relation,
|
||||
openingDate: party.openingDate ? new Date(party.openingDate) : null,
|
||||
month: party.month,
|
||||
maturityDate: party.maturityDate,
|
||||
reopenDate: party.reopenDate ? new Date(party.reopenDate) : null,
|
||||
depositAmount: party.depositAmount,
|
||||
dailyDepositAmount: party.dailyDepositAmount,
|
||||
days: party.days,
|
||||
ledgerNo: party.ledgerNo,
|
||||
maturityAmount: party.maturityAmount,
|
||||
interestRate: party.interestRate,
|
||||
withdrawType: party.withdrawType,
|
||||
od1Flag: party.od1Flag,
|
||||
od2Flag: party.od2Flag,
|
||||
lien: party.lien ? 'Yes' : 'No',
|
||||
payDate: party.payDate ? new Date(party.payDate) : null,
|
||||
paymentAmount: party.paymentAmount,
|
||||
currentBalance: party.currentBalance,
|
||||
openingBalance: party.openingBalance,
|
||||
isSmsActive: party.isSmsActive,
|
||||
isEmailActive: party.isEmailActive,
|
||||
interestCalc: party.interestCalc,
|
||||
interestDate: party.interestDate ? new Date(party.interestDate) : null,
|
||||
spDescription: party.spDescription,
|
||||
freezeCode: party.freezeCode,
|
||||
advCode: party.advCode,
|
||||
creditToBranch: party.creditToBranch,
|
||||
creditToAccountType: party.creditToAccountType,
|
||||
creditToAccount: party.creditToAccount,
|
||||
nomineeApplicable: party.nomineeApplicable ? 'Yes' : 'No',
|
||||
nomineeSplitMode: party.nomineeSplitMode
|
||||
});
|
||||
|
||||
this.nominees.clear();
|
||||
(party.nominees || []).forEach((nominee: any) => {
|
||||
const group = this.buildNomineeGroup();
|
||||
group.patchValue({
|
||||
nominationDate: nominee.nominationDate ? new Date(nominee.nominationDate) : null,
|
||||
name: nominee.name,
|
||||
mobileNo: nominee.mobileNo,
|
||||
birthDate: nominee.birthDate ? new Date(nominee.birthDate) : null,
|
||||
minor: nominee.minor ? 'Yes' : 'No',
|
||||
relation: nominee.relation,
|
||||
gender: nominee.gender,
|
||||
guardianName: nominee.guardianName
|
||||
});
|
||||
this.nominees.push(group);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user