DDS: Agent settlement (Agent Tran), Close Party account UI, reports UI, Party-Savings link field

This commit is contained in:
tarunmalviya
2026-08-25 10:56:03 +05:30
parent 7ef75c902d
commit 57a61b3057
78 changed files with 12711 additions and 2 deletions
@@ -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);
});
}
}