DDS: approvals tab, vouchers, payout & close, Sundry GL search, closure tab, fixes

This commit is contained in:
tarunmalviya
2026-10-01 13:08:52 +05:30
parent d47e0860f6
commit 885b39d970
48 changed files with 5878 additions and 374 deletions
@@ -26,7 +26,9 @@
<div class="pm-section__header">
<i class="fas fa-id-card"></i>
<h3>Customer &amp; Scheme</h3>
<span class="pm-section__sub">Who this account is for, which scheme it runs under, and which agent collects for it</span>
<span class="pm-section__sub"
>Who this account is for, which scheme it runs under, and which agent collects for it</span
>
</div>
<div class="pm-grid">
<div class="pm-f">
@@ -42,7 +44,11 @@
autocomplete="off"
/>
<fa-icon matSuffix icon="search" class="pm-search-ic"></fa-icon>
<mat-autocomplete #clientAuto="matAutocomplete" panelWidth="min(30rem, 92vw)" [displayWith]="displayClient">
<mat-autocomplete
#clientAuto="matAutocomplete"
panelWidth="min(30rem, 92vw)"
[displayWith]="displayClient"
>
@for (client of clientOptions; track client.id) {
<mat-option [value]="client">
<div class="pm-opt">
@@ -81,7 +87,11 @@
autocomplete="off"
/>
<fa-icon matSuffix icon="search" class="pm-search-ic"></fa-icon>
<mat-autocomplete #subTypeAuto="matAutocomplete" panelWidth="min(30rem, 92vw)" [displayWith]="displaySubType">
<mat-autocomplete
#subTypeAuto="matAutocomplete"
panelWidth="min(30rem, 92vw)"
[displayWith]="displaySubType"
>
@for (type of subTypeOptions; track type.id) {
<mat-option [value]="type.accountTypeCode">
<div class="pm-opt">
@@ -114,7 +124,11 @@
autocomplete="off"
/>
<fa-icon matSuffix icon="search" class="pm-search-ic"></fa-icon>
<mat-autocomplete #agentAuto="matAutocomplete" panelWidth="min(30rem, 92vw)" [displayWith]="displayAgent">
<mat-autocomplete
#agentAuto="matAutocomplete"
panelWidth="min(30rem, 92vw)"
[displayWith]="displayAgent"
>
@for (agent of agentOptions; track agent.id) {
<mat-option [value]="agent">
<div class="pm-opt">
@@ -163,20 +177,37 @@
</mat-form-field>
</div>
<!--
<!--
The customer's real deposit account. Three states, and the difference matters: an
account is open, this party predates DDS opening them, or the party is new and the
account is about to be opened. It used to read "Linked (id 36)", which is a number
only the database means anything by.
-->
@if (savingsAccountNo) {
@if (accountNo) {
<!-- The party is the DDS account now, so its number, scheme and balance are its own. -->
<div class="pm-f">
<label class="pm-f-lbl">DDS Account</label>
<div class="pm-account">
<span class="pm-account-no">{{ accountNo }}</span>
<span class="pm-account-meta">
{{ accountSchemeName }} ·
<span [class.pm-account-live]="partyStatus === 'ACTIVE'">{{
partyStatus === 'ACTIVE' ? 'Active' : partyStatus
}}</span>
· {{ accountBalance | number: '1.2-2' }}
</span>
</div>
</div>
} @else if (savingsAccountNo) {
<div class="pm-f">
<label class="pm-f-lbl">DDS Account</label>
<div class="pm-account">
<span class="pm-account-no">{{ savingsAccountNo }}</span>
<span class="pm-account-meta">
{{ savingsProductName }} ·
<span [class.pm-account-live]="savingsAccountStatus === 'Active'">{{ savingsAccountStatus }}</span>
<span [class.pm-account-live]="savingsAccountStatus === 'Active'">{{
savingsAccountStatus
}}</span>
· {{ savingsAccountBalance | number: '1.2-2' }}
</span>
</div>
@@ -224,7 +255,7 @@
<div class="pm-f pm-f--wide">
<label class="pm-f-lbl">Party Name<b>*</b></label>
<mat-form-field appearance="outline" class="pm-field">
<input matInput required formControlName="partyName" />
<input matInput required formControlName="partyName" [readonly]="isFromClient('partyName')" />
</mat-form-field>
</div>
@@ -234,39 +265,45 @@
<input matInput formControlName="partyName1" />
</mat-form-field>
</div>
<div class="pm-f pm-f--wide">
<label class="pm-f-lbl">Address 1<b>*</b></label>
<mat-form-field appearance="outline" class="pm-field">
<input matInput required formControlName="address1" />
<input matInput required formControlName="address1" [readonly]="isFromClient('address1')" />
</mat-form-field>
</div>
<div class="pm-f pm-f--wide">
<label class="pm-f-lbl">Address 2</label>
<mat-form-field appearance="outline" class="pm-field">
<input matInput formControlName="address2" />
<input matInput formControlName="address2" [readonly]="isFromClient('address2')" />
</mat-form-field>
</div>
<div class="pm-f pm-f--wide">
<label class="pm-f-lbl">Address 3</label>
<mat-form-field appearance="outline" class="pm-field">
<input matInput formControlName="address3" />
<input matInput formControlName="address3" [readonly]="isFromClient('address3')" />
</mat-form-field>
</div>
<div class="pm-f">
<label class="pm-f-lbl">City<b>*</b></label>
<mat-form-field appearance="outline" class="pm-field">
<input matInput required formControlName="city" />
<input matInput required formControlName="city" [readonly]="isFromClient('city')" />
</mat-form-field>
</div>
<div class="pm-f">
<label class="pm-f-lbl">Pincode<b>*</b></label>
<mat-form-field appearance="outline" class="pm-field">
<input matInput required formControlName="pincode" mifosxRestrictInput="digits" />
<input
matInput
required
formControlName="pincode"
[readonly]="isFromClient('pincode')"
mifosxRestrictInput="digits"
/>
</mat-form-field>
</div>
@@ -284,35 +321,61 @@
<div class="pm-f">
<label class="pm-f-lbl">Telephone</label>
<mat-form-field appearance="outline" class="pm-field">
<input matInput formControlName="telephone" mifosxRestrictInput="phone" />
<input
matInput
formControlName="telephone"
[readonly]="isFromClient('telephone')"
mifosxRestrictInput="phone"
/>
</mat-form-field>
</div>
<div class="pm-f">
<label class="pm-f-lbl">Mobile No (SMS)<b>*</b></label>
<mat-form-field appearance="outline" class="pm-field">
<input matInput required formControlName="mobileNoSms" mifosxRestrictInput="digits" />
<input
matInput
required
formControlName="mobileNoSms"
[readonly]="isFromClient('mobileNoSms')"
mifosxRestrictInput="digits"
/>
</mat-form-field>
</div>
<div class="pm-f">
<label class="pm-f-lbl">Aadhar No</label>
<mat-form-field appearance="outline" class="pm-field">
<input matInput formControlName="aadharNo" mifosxRestrictInput="aadhaar" />
<input
matInput
formControlName="aadharNo"
[readonly]="isFromClient('aadharNo')"
mifosxRestrictInput="aadhaar"
/>
</mat-form-field>
</div>
<div class="pm-f">
<label class="pm-f-lbl">PAN Card No</label>
<mat-form-field appearance="outline" class="pm-field">
<input matInput formControlName="panCardNo" mifosxRestrictInput="pan" />
<input
matInput
formControlName="panCardNo"
[readonly]="isFromClient('panCardNo')"
mifosxRestrictInput="pan"
/>
</mat-form-field>
</div>
<div class="pm-f">
<label class="pm-f-lbl">CKYC ID</label>
<mat-form-field appearance="outline" class="pm-field">
<input matInput formControlName="ckycId" mifosxRestrictInput="digits" />
<input
matInput
formControlName="ckycId"
[readonly]="isFromClient('ckycId')"
mifosxRestrictInput="digits"
/>
</mat-form-field>
</div>
@@ -330,7 +393,12 @@
<div class="pm-f">
<label class="pm-f-lbl">Birth Date</label>
<mat-form-field appearance="outline" class="pm-field">
<input matInput [matDatepicker]="birthPicker" formControlName="birthDate" />
<input
matInput
[matDatepicker]="birthPicker"
formControlName="birthDate"
[readonly]="isFromClient('birthDate')"
/>
<mat-datepicker-toggle matIconSuffix [for]="birthPicker"></mat-datepicker-toggle>
<mat-datepicker #birthPicker></mat-datepicker>
</mat-form-field>
@@ -364,7 +432,9 @@
<div class="pm-section__header">
<i class="fas fa-user-friends"></i>
<h3>Minor / Guardian</h3>
<span class="pm-section__sub">Only for a minor's account — the guardian who operates it until they come of age</span>
<span class="pm-section__sub"
>Only for a minor's account — the guardian who operates it until they come of age</span
>
</div>
<div class="pm-grid">
<div class="pm-f">
@@ -382,7 +452,13 @@
<div class="pm-f">
<label class="pm-f-lbl">Minor Date<b>*</b></label>
<mat-form-field appearance="outline" class="pm-field">
<input matInput required [matDatepicker]="minorPicker" formControlName="minorDate" />
<input
matInput
required
[matDatepicker]="minorPicker"
formControlName="minorDate"
[readonly]="isFromClient('minorDate')"
/>
<mat-datepicker-toggle matIconSuffix [for]="minorPicker"></mat-datepicker-toggle>
<mat-datepicker #minorPicker></mat-datepicker>
<mat-hint>Turns 18 on</mat-hint>
@@ -396,12 +472,17 @@
matInput
required
formControlName="guardianClientId"
[readonly]="isFromClient('guardianClientId')"
[matAutocomplete]="guardianAuto"
placeholder="Type an ID or search by name…"
autocomplete="off"
/>
<fa-icon matSuffix icon="search" class="pm-search-ic"></fa-icon>
<mat-autocomplete #guardianAuto="matAutocomplete" panelWidth="min(30rem, 92vw)" [displayWith]="displayClient">
<mat-autocomplete
#guardianAuto="matAutocomplete"
panelWidth="min(30rem, 92vw)"
[displayWith]="displayClient"
>
@for (client of guardianOptions; track client.id) {
<mat-option [value]="client">
<div class="pm-opt">
@@ -428,7 +509,7 @@
<div class="pm-f">
<label class="pm-f-lbl">Relation<b>*</b></label>
<mat-form-field appearance="outline" class="pm-field">
<input matInput required formControlName="relation" />
<input matInput required formControlName="relation" [readonly]="isFromClient('relation')" />
<mat-hint>To the minor</mat-hint>
</mat-form-field>
</div>
@@ -535,24 +616,8 @@
</mat-form-field>
</div>
<div class="pm-f">
<label class="pm-f-lbl">Interest Rate</label>
<mat-form-field appearance="outline" class="pm-field">
<input matInput type="number" formControlName="interestRate" />
<span matTextSuffix>%</span>
</mat-form-field>
</div>
<div class="pm-f">
<label class="pm-f-lbl">Withdraw Type</label>
<mat-form-field appearance="outline" class="pm-field">
<mat-select formControlName="withdrawType">
@for (type of withdrawTypes; track type) {
<mat-option [value]="type">{{ type }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<!-- Interest Rate is the product's (Sub Type), not typed here. -->
<!-- Withdraw Type is decided when the account is paid out, not when it is opened. -->
</div>
</div>
@@ -610,7 +675,13 @@
<div class="pm-f">
<label class="pm-f-lbl">Please specify<b>*</b></label>
<mat-form-field appearance="outline" class="pm-field">
<input matInput required maxlength="200" formControlName="lienReasonOther" placeholder="e.g. Loan #1234" />
<input
matInput
required
maxlength="200"
formControlName="lienReasonOther"
placeholder="e.g. Loan #1234"
/>
<mat-hint>Write the reason in your own words</mat-hint>
</mat-form-field>
</div>
@@ -687,17 +758,7 @@
</mat-select>
</mat-form-field>
</div>
<div class="pm-f">
<label class="pm-f-lbl">Adv Code</label>
<mat-form-field appearance="outline" class="pm-field">
<mat-select formControlName="advCode">
@for (code of advCodes; track code) {
<mat-option [value]="code">{{ code }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<!-- Adv Code is decided when the account is paid out, not when it is opened. -->
</div>
</div>
@@ -723,7 +784,7 @@
</mat-form-field>
</div>
<!--
<!--
Picked from the customer's own savings accounts, never typed. The whole matured
amount lands here, and a number that exists only as text is a payout with
nowhere to go - which is what one party was already carrying.
@@ -790,7 +851,10 @@
@if (splitByPercentage && nominees.length > 0) {
<span class="pm-share-total" [class.pm-share-total-bad]="nomineeShareError">
{{ nomineeShareTotal }}% of 100% allocated@if (nomineeShareError) { — {{ nomineeShareError }} }
{{ nomineeShareTotal }}% of 100% allocated
@if (nomineeShareError) {
— {{ nomineeShareError }}
}
</span>
} @else if (splitBySequence && nominees.length > 0) {
<span class="pm-share-total" [class.pm-share-total-bad]="nomineeOrderError">
@@ -865,7 +929,10 @@
</div>
<div class="pm-f">
<label class="pm-f-lbl">
Guar. Name@if (isNomineeMinor(nominee)) {<b>*</b>}
Guar. Name
@if (isNomineeMinor(nominee)) {
<b>*</b>
}
</label>
<mat-form-field appearance="outline" class="pm-field">
<input matInput [required]="isNomineeMinor(nominee)" formControlName="guardianName" />
@@ -878,7 +945,15 @@
<div class="pm-f pm-f--tight">
<label class="pm-f-lbl">Share %<b>*</b></label>
<mat-form-field appearance="outline" class="pm-field">
<input matInput type="number" min="0.01" max="100" step="0.01" required formControlName="sharePercent" />
<input
matInput
type="number"
min="0.01"
max="100"
step="0.01"
required
formControlName="sharePercent"
/>
</mat-form-field>
</div>
} @else {
@@ -922,8 +997,9 @@
<i class="fas fa-user-clock"></i>
<span>
This account was opened for a minor who came of age on
<strong>{{ majorOn | dateFormat }}</strong>. The guardian can no longer
operate it — take fresh KYC, a signature and a new mandate from the account holder.
<strong>{{ majorOn | dateFormat }}</strong
>. The guardian can no longer operate it — take fresh KYC, a signature and a new mandate from the account
holder.
</span>
</div>
}
@@ -32,6 +32,8 @@ import { ProductsService } from 'app/products/products.service';
import { ClientsService } from 'app/clients/clients.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
import { AlertService } from 'app/core/alert/alert.service';
import { searchClientsAnyCase } from '../client-search';
/** Custom Components */
import { PrematureCloseDialogComponent } from '../premature-close-dialog/premature-close-dialog.component';
@@ -84,6 +86,7 @@ export class CreatePartyComponent implements OnInit {
private route = inject(ActivatedRoute);
private organizationService = inject(OrganizationService);
private productsService = inject(ProductsService);
private alertService = inject(AlertService);
private clientsService = inject(ClientsService);
private settingsService = inject(SettingsService);
private dateUtils = inject(Dates);
@@ -130,6 +133,11 @@ export class CreatePartyComponent implements OnInit {
savingsAccountStatus: string | null = null;
savingsAccountBalance: number | null = null;
/** The DDS account's own number, scheme and balance - the party is the account. */
accountNo: string | null = null;
accountSchemeName: string | null = null;
accountBalance: number | null = null;
/**
* A minor account holder who has come of age. The guardian's authority ended on that day, so the
* branch has to take fresh KYC and a new mandate - shown as a call to action rather than a field.
@@ -271,9 +279,20 @@ export class CreatePartyComponent implements OnInit {
'gender',
'birthDate',
'telephone',
'mobileNoSms'
'mobileNoSms',
'occupation',
'risk',
'aadharNo',
'panCardNo',
'ckycId',
'relation',
'minorDate',
'guardianClientId'
];
/** Fields holding a value from the client record, shown read-only - see fillFromClientRecord. */
private filledFromClient = new Set<string>();
private static readonly MINOR_FIELDS = [
'minorDate',
'guardianClientId',
@@ -323,7 +342,10 @@ export class CreatePartyComponent implements OnInit {
'No',
Validators.required
],
aadharNo: ['', Validators.pattern(CreatePartyComponent.AADHAAR_RE)],
aadharNo: [
'',
Validators.pattern(CreatePartyComponent.AADHAAR_RE)
],
partyName: [
'',
Validators.required
@@ -347,8 +369,14 @@ export class CreatePartyComponent implements OnInit {
]
],
gender: [''],
panCardNo: ['', Validators.pattern(CreatePartyComponent.PAN_RE)],
telephone: ['', Validators.pattern(CreatePartyComponent.TELEPHONE_RE)],
panCardNo: [
'',
Validators.pattern(CreatePartyComponent.PAN_RE)
],
telephone: [
'',
Validators.pattern(CreatePartyComponent.TELEPHONE_RE)
],
mobileNoSms: [
'',
[
@@ -363,7 +391,10 @@ export class CreatePartyComponent implements OnInit {
birthDate: [''],
occupation: [''],
risk: [''],
ckycId: ['', Validators.pattern(CreatePartyComponent.CKYC_RE)],
ckycId: [
'',
Validators.pattern(CreatePartyComponent.CKYC_RE)
],
adultMinor: [
'Adult',
Validators.required
@@ -511,8 +542,10 @@ export class CreatePartyComponent implements OnInit {
this.officesError = this.offices.length === 0 ? 'No branches on file' : '';
},
error: (error: any) => {
this.officesError = error?.error?.defaultUserMessage || error?.error?.developerMessage
|| `Could not load branches (${error?.status || 'no response'})`;
this.officesError =
error?.error?.defaultUserMessage ||
error?.error?.developerMessage ||
`Could not load branches (${error?.status || 'no response'})`;
}
});
@@ -547,6 +580,8 @@ export class CreatePartyComponent implements OnInit {
for (const controlName of CreatePartyComponent.CLIENT_DERIVED_FIELDS) {
this.partyForm.get(controlName)?.setValue('');
}
this.filledFromClient.clear();
this.partyForm.get('adultMinor')?.enable({ emitEvent: false });
this.loadedClientName = '';
this.clientLookupFailed = false;
@@ -574,13 +609,9 @@ export class CreatePartyComponent implements OnInit {
}
private searchClients(term: string): void {
this.clientsService.getFilteredClients('displayName', 'ASC', false, term).subscribe({
next: (response: any) => {
const found = response?.pageItems || response || [];
this.showingClientList = false;
this.clientOptions = Array.isArray(found) ? found.slice(0, 25) : [];
},
error: () => (this.clientOptions = [])
searchClientsAnyCase(this.clientsService, term).subscribe((found: any[]) => {
this.showingClientList = false;
this.clientOptions = found.slice(0, 25);
});
}
@@ -624,13 +655,54 @@ export class CreatePartyComponent implements OnInit {
next: (client: any) => {
this.loadedClientName = client?.displayName || knownName || '';
this.clientLookupFailed = false;
this.setField('partyName', client?.displayName);
this.setField('mobileNoSms', client?.mobileNo);
this.setField('gender', client?.gender?.name || client?.gender);
this.fillFromClientRecord('partyName', client?.displayName);
this.fillFromClientRecord('mobileNoSms', client?.mobileNo);
this.fillFromClientRecord('telephone', client?.alternateMobileNo);
// Alerts go where the client can actually be reached: SMS with a mobile on record, email with
// an email address on record.
this.partyForm.get('isSmsActive')?.setValue(!!client?.mobileNo);
this.partyForm.get('isEmailActive')?.setValue(!!client?.emailAddress);
// The client record says FEMALE; this form's list says Female.
this.fillFromClientRecord('gender', titleCase(client?.gender?.name || client?.gender));
const dateOfBirth = this.toDate(client?.dateOfBirth);
if (dateOfBirth) {
this.partyForm.get('birthDate')?.setValue(dateOfBirth);
this.filledFromClient.add('birthDate');
}
// The client's own occupation, offered even when this form's short list does not carry it.
const occupation = client?.occupation?.name || client?.occupationCategory?.name;
if (occupation) {
if (!this.occupations.includes(occupation)) {
this.occupations = [
...this.occupations,
occupation
];
}
this.fillFromClientRecord('occupation', occupation);
}
// A minor on the client record is a minor account here, with the same guardian.
this.partyForm.get('adultMinor')?.setValue(client?.isMinor ? 'Minor' : 'Adult');
// Adult or minor is the client record's, checked when the client was opened - not changed here.
this.partyForm.get('adultMinor')?.disable({ emitEvent: false });
this.filledFromClient.add('adultMinor');
if (client?.isMinor) {
if (dateOfBirth) {
// The day they come of age - eighteen is majority in India.
const majority = new Date(dateOfBirth);
majority.setFullYear(majority.getFullYear() + 18);
this.partyForm.get('minorDate')?.setValue(majority);
this.filledFromClient.add('minorDate');
}
if (client?.guardianClientId) {
this.partyForm
.get('guardianClientId')
?.setValue({ id: client.guardianClientId, displayName: client.guardianClientDisplayName || '' });
this.filledFromClient.add('guardianClientId');
}
this.fillFromClientRecord('relation', titleCase(client?.guardianRelation));
}
this.loadClientRisk(String(clientId));
// Nominees are not brought across: the officer records them for this account.
// Proceeds usually go back to an account at the customer's own branch, so that is offered
// first - any branch can still be picked, since the account may well be at another.
const creditToBranch = this.partyForm.get('creditToBranch');
@@ -692,11 +764,11 @@ export class CreatePartyComponent implements OnInit {
continue;
}
if (words.includes('aadhaar') || words.includes('aadhar')) {
this.setField('aadharNo', documentKey);
this.fillFromClientRecord('aadharNo', documentKey);
} else if (words.includes('pan')) {
this.setField('panCardNo', documentKey);
this.fillFromClientRecord('panCardNo', documentKey);
} else if (words.includes('ckyc')) {
this.setField('ckycId', documentKey);
this.fillFromClientRecord('ckycId', documentKey);
}
}
},
@@ -713,6 +785,39 @@ export class CreatePartyComponent implements OnInit {
return typeof value === 'object' ? `${value.id} — ${value.displayName}` : String(value);
};
/**
* Takes a value from the client record, which was checked when the client was opened, and locks
* the field so it is not typed - and checked - a second time. A value the client does not have is
* left for the officer to enter.
*/
private fillFromClientRecord(controlName: string, value: any): void {
if (value === null || value === undefined || String(value).trim() === '') {
return;
}
this.setField(controlName, value);
this.filledFromClient.add(controlName);
}
/** Whether a field holds a value taken from the client record, and so is shown read-only. */
isFromClient(controlName: string): boolean {
return this.filledFromClient.has(controlName);
}
/** The client's risk category, from their KYC record. */
private loadClientRisk(clientId: string): void {
this.clientsService.getClientKyc(clientId, true).subscribe({
next: (kyc: any) => {
// kycRiskCategory.medium -> Medium
const code = String(kyc?.riskCategoryCode || '')
.split('.')
.pop();
this.fillFromClientRecord('risk', titleCase(code));
},
// A client without a KYC record just leaves the risk to be chosen.
error: () => undefined
});
}
private loadClientAddress(clientId: string): void {
this.clientsService.getClientAddressData(clientId, true).subscribe({
next: (addresses: any) => {
@@ -720,11 +825,11 @@ export class CreatePartyComponent implements OnInit {
if (!address) {
return;
}
this.setField('address1', address.addressLine1 || address.street);
this.setField('address2', address.addressLine2);
this.setField('address3', address.addressLine3);
this.setField('city', address.city);
this.setField('pincode', address.postalCode);
this.fillFromClientRecord('address1', address.addressLine1 || address.street);
this.fillFromClientRecord('address2', address.addressLine2);
this.fillFromClientRecord('address3', address.addressLine3);
this.fillFromClientRecord('city', address.city);
this.fillFromClientRecord('pincode', address.postalCode);
},
// A client with no address on file is normal - the officer just types it.
error: () => undefined
@@ -802,8 +907,12 @@ export class CreatePartyComponent implements OnInit {
private filterAgents(term: string): void {
const needle = term.toLowerCase();
const match = (agent: any) =>
String(agent.agentCode || '').toLowerCase().includes(needle) ||
String(agent.agentNameId || '').toLowerCase().includes(needle) ||
String(agent.agentCode || '')
.toLowerCase()
.includes(needle) ||
String(agent.agentNameId || '')
.toLowerCase()
.includes(needle) ||
String(agent.id) === term;
if (this.allAgents.length > 0) {
@@ -868,14 +977,28 @@ export class CreatePartyComponent implements OnInit {
*/
private wireSubTypeSearch(): void {
this.partyForm.get('subType')?.valueChanges.subscribe((value) => {
const term = String(value ?? '').toLowerCase().trim();
// A new account takes its product's rate, which the maturity preview is worked out on. The
// server sets the same rate when it saves; an account being edited already has it.
const scheme = this.subTypes.find((type) => type.accountTypeCode === value);
if (scheme && !this.partyId) {
this.partyForm.get('interestRate')?.setValue(Number(scheme.interestRate) || 0);
}
const term = String(value ?? '')
.toLowerCase()
.trim();
this.subTypeOptions = !term
? this.subTypes
: this.subTypes.filter(
(type) =>
String(type.accountTypeCode || '').toLowerCase().includes(term) ||
String(type.accountTypeDescription || '').toLowerCase().includes(term) ||
String(type.accountTypeGroup || '').toLowerCase().includes(term)
String(type.accountTypeCode || '')
.toLowerCase()
.includes(term) ||
String(type.accountTypeDescription || '')
.toLowerCase()
.includes(term) ||
String(type.accountTypeGroup || '')
.toLowerCase()
.includes(term)
);
});
}
@@ -970,8 +1093,6 @@ export class CreatePartyComponent implements OnInit {
return this.partyForm.get('nomineeApplicable')?.value === 'Yes';
}
/**
* Which entry in the list a stored reason belongs to.
*
@@ -1051,7 +1172,10 @@ export class CreatePartyComponent implements OnInit {
*/
private wireMaturityCalculation(): void {
// The term drives everything downstream, so changing it refills Days as well.
for (const controlName of ['openingDate', 'month']) {
for (const controlName of [
'openingDate',
'month'
]) {
this.partyForm.get(controlName)?.valueChanges.subscribe(() => this.recalculateTerm());
}
// Days typed by hand stands: it only moves the maturity date when no month was given.
@@ -1059,7 +1183,10 @@ export class CreatePartyComponent implements OnInit {
this.recalculateMaturityDate();
this.recalculateMaturityAmount();
});
for (const controlName of ['dailyDepositAmount', 'interestRate']) {
for (const controlName of [
'dailyDepositAmount',
'interestRate'
]) {
this.partyForm.get(controlName)?.valueChanges.subscribe(() => this.recalculateMaturityAmount());
}
}
@@ -1131,7 +1258,11 @@ export class CreatePartyComponent implements OnInit {
this.partyForm
.get('maturityAmount')
?.setValue(
this.maturityAmountOf(Number(raw.dailyDepositAmount) || 0, Number(raw.days) || 0, Number(raw.interestRate) || 0),
this.maturityAmountOf(
Number(raw.dailyDepositAmount) || 0,
Number(raw.days) || 0,
Number(raw.interestRate) || 0
),
{ emitEvent: false }
);
}
@@ -1372,6 +1503,9 @@ export class CreatePartyComponent implements OnInit {
this.savingsProductName = null;
this.savingsAccountStatus = null;
this.savingsAccountBalance = null;
this.accountNo = null;
this.accountSchemeName = null;
this.accountBalance = null;
this.majorOn = null;
this.minorTurnedMajor = false;
this.loadedClientName = '';
@@ -1451,7 +1585,7 @@ export class CreatePartyComponent implements OnInit {
dailyDepositAmount: raw.dailyDepositAmount,
days: raw.days,
ledgerNo: raw.ledgerNo || undefined,
interestRate: raw.interestRate || 0,
// No interest rate: the server gives the account its product's rate.
withdrawType: raw.withdrawType || undefined,
lien: raw.lien === 'Yes',
// Sent only when the money is actually held. Releasing it clears the reason on the
@@ -1489,10 +1623,24 @@ export class CreatePartyComponent implements OnInit {
}))
};
const partyLabel = raw.partyCode || raw.partyName;
const done = (response: any) => {
// Under maker-checker the command is only recorded, and nothing has been saved yet.
if (!!response?.commandId && response?.resourceId == null) {
this.alertService.alert({
type: 'Sent for Approval',
message: this.partyId
? `Changes to party ${partyLabel} were sent for a manager's approval and will take effect once approved.`
: `New party ${partyLabel} was sent for a manager's approval and will be created once approved.`
});
}
this.exit();
};
if (this.partyId) {
this.organizationService.updateParty(this.partyId, payload).subscribe(() => this.exit());
this.organizationService.updateParty(this.partyId, payload).subscribe(done);
} else {
this.organizationService.createParty(payload).subscribe(() => this.exit());
this.organizationService.createParty(payload).subscribe(done);
}
}
@@ -1504,7 +1652,7 @@ export class CreatePartyComponent implements OnInit {
const locale = this.settingsService.language.code;
this.organizationService
.closeParty(this.partyId, {
closeDate: this.dateUtils.formatDate(new Date(), dateFormat),
closeDate: this.dateUtils.formatDate(this.settingsService.businessDate || new Date(), dateFormat),
dateFormat,
locale
})
@@ -1533,7 +1681,12 @@ export class CreatePartyComponent implements OnInit {
private get agentCodeOfForm(): string {
const value = this.partyForm.get('agentId')?.value;
return value && typeof value === 'object' ? value.agentCode : String(value ?? '');
if (value && typeof value === 'object') {
return value.agentCode;
}
// The form holds the agent's id; the dialog names them by code.
const agent = (this.allAgents || []).find((option: any) => String(option.id) === String(value));
return agent?.agentCode ?? String(value ?? '');
}
get canCloseEarly(): boolean {
@@ -1562,6 +1715,7 @@ export class CreatePartyComponent implements OnInit {
interestRate: this.partyForm.get('interestRate')?.value,
maturityDate: this.partyForm.get('maturityDate')?.value,
currentBalance: this.partyForm.get('currentBalance')?.value,
openingDate: this.partyForm.get('openingDate')?.value,
clientId: this.clientIdOfForm,
agentCode: this.agentCodeOfForm
},
@@ -1573,20 +1727,35 @@ export class CreatePartyComponent implements OnInit {
if (!closeCommand) {
return;
}
const partyCode = this.partyForm.get('partyCode')?.value;
// Inside the lock-in it is a request with a reason, on its own permission, for a manager to pass.
const withinLockin = !!closeCommand.lockinOverrideReason;
// The date goes up the way every other date on this system does - formatted to the user's own
// setting, with that setting and their locale alongside so the server reads it back the same
// way. An ordinary close sends it like this too; two ways of closing an account should not
// disagree about something as plain as what day it is.
const dateFormat = this.settingsService.dateFormat;
const locale = this.settingsService.language.code;
this.organizationService
.prematureCloseParty(this.partyId as number, {
...closeCommand,
closeDate: this.dateUtils.formatDate(new Date(), dateFormat),
dateFormat,
locale
})
.subscribe(() => this.exit());
const request = {
...closeCommand,
closeDate: this.dateUtils.formatDate(this.settingsService.businessDate || new Date(), dateFormat),
dateFormat,
locale
};
(withinLockin
? this.organizationService.prematureCloseOverrideParty(this.partyId as number, request)
: this.organizationService.prematureCloseParty(this.partyId as number, request)
).subscribe((response: any) => {
// Under maker-checker the command is only recorded, and nothing has closed yet.
const awaitingApproval = !!response?.commandId && response?.resourceId == null;
this.alertService.alert({
type: awaitingApproval ? 'Sent for Approval' : 'Party Closed',
message: awaitingApproval
? `Early closure of ${partyCode} is waiting for a manager's approval. The account stays open until then.`
: `${partyCode} is closed.`
});
this.exit();
});
// A refusal - inside the lock-in, held under lien, KYC outstanding - is shown by the platform's
// own error handler, which names the reason. Repeating it here would say the same thing twice.
});
@@ -1662,6 +1831,9 @@ export class CreatePartyComponent implements OnInit {
this.savingsProductName = party.savingsProductName || null;
this.savingsAccountStatus = party.savingsAccountStatus || null;
this.savingsAccountBalance = party.savingsAccountBalance ?? null;
this.accountNo = party.accountNo || null;
this.accountSchemeName = party.subTypeDescription || party.subType || null;
this.accountBalance = party.currentBalance ?? null;
this.majorOn = party.majorOn || null;
this.minorTurnedMajor = party.minorTurnedMajor === true;
this.loadedClientName = party.clientName || '';
@@ -1762,3 +1934,11 @@ export class CreatePartyComponent implements OnInit {
}
}
}
/** FEMALE or female -> Female; each word, so "SELF EMPLOYED" -> "Self Employed". */
function titleCase(value: any): string {
return String(value ?? '')
.toLowerCase()
.replace(/(^|\s)(\S)/g, (_match, space, letter) => space + letter.toUpperCase())
.trim();
}