DDS: approvals tab, vouchers, payout & close, Sundry GL search, closure tab, fixes
This commit is contained in:
@@ -26,7 +26,9 @@
|
||||
<div class="pm-section__header">
|
||||
<i class="fas fa-id-card"></i>
|
||||
<h3>Customer & 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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user