- Agent master: tabbed layout, customer search dialog, GL dropdown, account card, view page - Party master: tabbed layout, expected party code, nominee cards, relation from code values - Daily transaction entry: compact style, collections popup, Apply DDS Receipt placement - Approvals: new review modal, Agent Approval tab, DDS routing into product/pending account - DDS product: view page and list buttons; drop Auto Interest Post Day Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
775 lines
22 KiB
TypeScript
775 lines
22 KiB
TypeScript
/**
|
|
* 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/.
|
|
*/
|
|
|
|
import { Component, OnInit, inject } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
|
import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup } from '@angular/forms';
|
|
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
import { MatInputModule } from '@angular/material/input';
|
|
import { MatButtonModule } from '@angular/material/button';
|
|
import { MatIconModule } from '@angular/material/icon';
|
|
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
|
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
|
|
|
export interface CustomerSearchResult {
|
|
clientId: number;
|
|
displayName: string;
|
|
mobileNo: string | null;
|
|
officeName: string | null;
|
|
accountNo: string | null;
|
|
accountType: 'SAVINGS' | 'CURRENT' | 'FIXED_DEPOSIT' | 'RECURRING_DEPOSIT' | null;
|
|
panNumber: string | null;
|
|
aadhaarNumber: string | null;
|
|
address: string | null;
|
|
}
|
|
|
|
/** Optional data for the dialog. */
|
|
export interface CustomerSearchDialogData {
|
|
/**
|
|
* Whether a customer must have an account to be picked. True by default, because the account screens
|
|
* need one; a screen that only needs the customer (cash exchange) passes false.
|
|
*/
|
|
requireAccount?: boolean;
|
|
/**
|
|
* Whether each customer is listed once. The search returns a row per account, which an account
|
|
* screen wants; a screen that is choosing the customer itself (an agent, a guarantor) wants each
|
|
* person once however many accounts they hold.
|
|
*/
|
|
uniqueCustomers?: boolean;
|
|
/**
|
|
* What the caller had already typed, to search for on opening. All digits is read as a client ID,
|
|
* or a mobile number once it is as long as one; anything else is a name.
|
|
*/
|
|
prefill?: string;
|
|
}
|
|
|
|
type AccountTypeKey = 'savings' | 'current' | 'fixedDeposit' | 'recurringDeposit';
|
|
|
|
/** An Indian mobile number is ten digits; a client ID is never that long. */
|
|
const MOBILE_NUMBER_LENGTH = 10;
|
|
|
|
@Component({
|
|
selector: 'mifosx-customer-search-dialog',
|
|
standalone: true,
|
|
imports: [
|
|
CommonModule,
|
|
FormsModule,
|
|
ReactiveFormsModule,
|
|
MatDialogModule,
|
|
MatFormFieldModule,
|
|
MatInputModule,
|
|
MatButtonModule,
|
|
MatIconModule,
|
|
MatProgressBarModule,
|
|
MatSnackBarModule
|
|
],
|
|
template: `
|
|
<div class="csd-head">
|
|
<div class="csd-head__icon"><mat-icon>person_search</mat-icon></div>
|
|
<div class="csd-head__text">
|
|
<h2 class="csd-head__title">Search Customer</h2>
|
|
<p class="csd-head__sub">
|
|
Fill in any one field, or combine several to narrow the search (e.g. name + account number).
|
|
</p>
|
|
</div>
|
|
<button mat-icon-button type="button" class="csd-head__close" (click)="onClose()" aria-label="Close">
|
|
<mat-icon>close</mat-icon>
|
|
</button>
|
|
</div>
|
|
|
|
@if (loading) {
|
|
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
|
}
|
|
|
|
<mat-dialog-content class="csd-content">
|
|
<form [formGroup]="form" class="csd-form" (keydown.enter)="onEnter($event)">
|
|
<div class="csd-grid">
|
|
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="csd-field">
|
|
<mat-label>Client ID</mat-label>
|
|
<mat-icon matPrefix>badge</mat-icon>
|
|
<input
|
|
matInput
|
|
formControlName="clientId"
|
|
placeholder="Client ID"
|
|
inputmode="numeric"
|
|
autocomplete="off"
|
|
(input)="digitsOnly('clientId')"
|
|
/>
|
|
</mat-form-field>
|
|
|
|
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="csd-field csd-field--name">
|
|
<mat-label>Name</mat-label>
|
|
<mat-icon matPrefix>person</mat-icon>
|
|
<input matInput formControlName="name" placeholder="Customer name" autocomplete="off" />
|
|
</mat-form-field>
|
|
|
|
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="csd-field">
|
|
<mat-label>Mobile Number</mat-label>
|
|
<mat-icon matPrefix>call</mat-icon>
|
|
<input
|
|
matInput
|
|
formControlName="mobileNo"
|
|
placeholder="Mobile number"
|
|
maxlength="10"
|
|
inputmode="numeric"
|
|
autocomplete="off"
|
|
(input)="digitsOnly('mobileNo')"
|
|
/>
|
|
</mat-form-field>
|
|
|
|
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="csd-field csd-field--wide">
|
|
<mat-label>Account Number</mat-label>
|
|
<mat-icon matPrefix>account_balance_wallet</mat-icon>
|
|
<input matInput formControlName="accountNumber" placeholder="Any account" autocomplete="off" />
|
|
</mat-form-field>
|
|
|
|
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="csd-field">
|
|
<mat-label>Aadhaar Number</mat-label>
|
|
<mat-icon matPrefix>fingerprint</mat-icon>
|
|
<input
|
|
matInput
|
|
formControlName="aadhaarNumber"
|
|
placeholder="12-digit Aadhaar"
|
|
maxlength="12"
|
|
inputmode="numeric"
|
|
autocomplete="off"
|
|
/>
|
|
</mat-form-field>
|
|
|
|
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="csd-field">
|
|
<mat-label>PAN Number</mat-label>
|
|
<mat-icon matPrefix>credit_card</mat-icon>
|
|
<input
|
|
matInput
|
|
formControlName="panNumber"
|
|
placeholder="ABCDE1234F"
|
|
maxlength="10"
|
|
autocomplete="off"
|
|
spellcheck="false"
|
|
(input)="upperCase('panNumber')"
|
|
class="csd-upper"
|
|
/>
|
|
</mat-form-field>
|
|
|
|
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="csd-field csd-field--address">
|
|
<mat-label>Address</mat-label>
|
|
<mat-icon matPrefix>place</mat-icon>
|
|
<input
|
|
matInput
|
|
formControlName="address"
|
|
placeholder="Street, town, city, or postal code"
|
|
autocomplete="off"
|
|
/>
|
|
</mat-form-field>
|
|
</div>
|
|
|
|
<div class="csd-types">
|
|
<span class="csd-types__label">Account Type</span>
|
|
<div class="csd-types__pills">
|
|
@for (t of accountTypeOptions; track t.key) {
|
|
<button
|
|
type="button"
|
|
class="csd-pill"
|
|
[class.csd-pill--on]="isTypeOn(t.key)"
|
|
[attr.aria-pressed]="isTypeOn(t.key)"
|
|
(click)="toggleType(t.key)"
|
|
>
|
|
<mat-icon>{{ isTypeOn(t.key) ? 'check' : t.icon }}</mat-icon
|
|
>{{ t.label }}
|
|
</button>
|
|
}
|
|
</div>
|
|
<span class="csd-types__hint">Leave all unselected to search every account type</span>
|
|
</div>
|
|
</form>
|
|
|
|
@if (searched) {
|
|
<div class="csd-results">
|
|
<div class="csd-results__head">{{ results.length }} {{ results.length === 1 ? 'result' : 'results' }}</div>
|
|
|
|
@if (!loading && results.length === 0) {
|
|
<div class="csd-empty">
|
|
<mat-icon>search_off</mat-icon>
|
|
<div class="csd-empty__title">No matching customer found</div>
|
|
<div class="csd-empty__sub">Check the spelling, or try fewer fields.</div>
|
|
</div>
|
|
}
|
|
|
|
@for (r of results; track $index) {
|
|
<div class="csd-result" [class.csd-result--noacc]="!r.accountNo">
|
|
<div class="csd-avatar">{{ initials(r.displayName) }}</div>
|
|
<div class="csd-result__main">
|
|
<div class="csd-result__name">
|
|
{{ r.displayName }}
|
|
@if (r.accountNo) {
|
|
<span class="csd-badge csd-badge--type">{{ accountTypeLabel(r.accountType) }}</span>
|
|
<span class="csd-result__acc">{{ r.accountNo }}</span>
|
|
} @else {
|
|
<span class="csd-badge csd-badge--muted">No account</span>
|
|
}
|
|
</div>
|
|
<div class="csd-result__meta">
|
|
<span><mat-icon>call</mat-icon>{{ r.mobileNo || '—' }}</span>
|
|
<span><mat-icon>account_balance</mat-icon>{{ r.officeName || '—' }}</span>
|
|
<span><mat-icon>credit_card</mat-icon>PAN {{ r.panNumber || '—' }}</span>
|
|
<span><mat-icon>fingerprint</mat-icon>Aadhaar {{ r.aadhaarNumber || '—' }}</span>
|
|
</div>
|
|
@if (r.address) {
|
|
<div class="csd-result__addr"><mat-icon>place</mat-icon>{{ r.address }}</div>
|
|
}
|
|
</div>
|
|
<button mat-flat-button color="primary" [disabled]="!canSelect(r)" (click)="onSelect(r)">Select</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</mat-dialog-content>
|
|
|
|
<mat-dialog-actions align="end" class="csd-actions">
|
|
<button mat-button type="button" (click)="onClose()">Close</button>
|
|
<button mat-stroked-button type="button" [disabled]="loading" (click)="onClear()">Clear</button>
|
|
<button mat-flat-button color="primary" type="button" [disabled]="loading" (click)="onSearch()">
|
|
<mat-icon>search</mat-icon> Search
|
|
</button>
|
|
</mat-dialog-actions>
|
|
`,
|
|
styles: [
|
|
`
|
|
:host {
|
|
display: block;
|
|
}
|
|
|
|
.csd-head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
padding: 20px 16px 12px 24px;
|
|
}
|
|
|
|
.csd-head__icon {
|
|
display: grid;
|
|
place-items: center;
|
|
flex: 0 0 auto;
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 12px;
|
|
background: #e6f4ee;
|
|
color: #0f4f3c;
|
|
}
|
|
|
|
.csd-head__icon mat-icon {
|
|
font-size: 26px;
|
|
width: 26px;
|
|
height: 26px;
|
|
}
|
|
|
|
.csd-head__text {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
}
|
|
|
|
.csd-head__title {
|
|
margin: 0;
|
|
font-size: 20px;
|
|
font-weight: 700;
|
|
color: #0f172a;
|
|
}
|
|
|
|
.csd-head__sub {
|
|
margin: 2px 0 0;
|
|
font-size: 13px;
|
|
color: #64748b;
|
|
}
|
|
|
|
.csd-head__close {
|
|
flex: 0 0 auto;
|
|
color: #64748b;
|
|
}
|
|
|
|
.csd-content {
|
|
padding-top: 12px;
|
|
}
|
|
|
|
.csd-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
gap: 12px 14px;
|
|
}
|
|
|
|
.csd-field {
|
|
width: 100%;
|
|
}
|
|
|
|
.csd-field--name,
|
|
.csd-field--wide {
|
|
grid-column: span 2;
|
|
}
|
|
|
|
.csd-field--address {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.csd-upper {
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.csd-types {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 8px 12px;
|
|
margin-top: 14px;
|
|
}
|
|
|
|
.csd-types__label {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: #475569;
|
|
}
|
|
|
|
.csd-types__pills {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
|
|
.csd-types__hint {
|
|
font-size: 12px;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.csd-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
height: 32px;
|
|
padding: 0 14px 0 10px;
|
|
border: 1px solid #cbd5e1;
|
|
border-radius: 999px;
|
|
background: #fff;
|
|
font: inherit;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: #475569;
|
|
cursor: pointer;
|
|
transition:
|
|
background 0.15s ease,
|
|
border-color 0.15s ease,
|
|
color 0.15s ease;
|
|
}
|
|
|
|
.csd-pill mat-icon {
|
|
font-size: 18px;
|
|
width: 18px;
|
|
height: 18px;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.csd-pill:hover {
|
|
border-color: #94a3b8;
|
|
background: #f8fafc;
|
|
}
|
|
|
|
.csd-pill--on,
|
|
.csd-pill--on:hover {
|
|
border-color: #1f7a4d;
|
|
background: #e6f4ee;
|
|
font-weight: 600;
|
|
color: #0f4f3c;
|
|
}
|
|
|
|
.csd-pill--on mat-icon {
|
|
color: #1f7a4d;
|
|
}
|
|
|
|
.csd-results {
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.csd-results__head {
|
|
margin-bottom: 8px;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.04em;
|
|
text-transform: uppercase;
|
|
color: #64748b;
|
|
}
|
|
|
|
.csd-result {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
margin-bottom: 8px;
|
|
padding: 12px 14px;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 12px;
|
|
background: #fff;
|
|
transition:
|
|
border-color 0.15s ease,
|
|
box-shadow 0.15s ease;
|
|
}
|
|
|
|
.csd-result:hover {
|
|
border-color: #1f7a4d;
|
|
box-shadow: 0 2px 8px rgb(15 23 42 / 8%);
|
|
}
|
|
|
|
.csd-result--noacc {
|
|
background: #f8fafc;
|
|
}
|
|
|
|
.csd-avatar {
|
|
display: grid;
|
|
place-items: center;
|
|
flex: 0 0 auto;
|
|
width: 42px;
|
|
height: 42px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, #1f7a4d, #0f4f3c);
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
color: #fff;
|
|
}
|
|
|
|
.csd-result__main {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
}
|
|
|
|
.csd-result__name {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
color: #0f172a;
|
|
}
|
|
|
|
.csd-result__acc {
|
|
font-family: ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: #475569;
|
|
}
|
|
|
|
.csd-badge {
|
|
padding: 2px 8px;
|
|
border-radius: 6px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.csd-badge--type {
|
|
background: #eef2ff;
|
|
color: #3730a3;
|
|
}
|
|
|
|
.csd-badge--muted {
|
|
background: #f1f5f9;
|
|
color: #64748b;
|
|
}
|
|
|
|
.csd-result__meta {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px 16px;
|
|
margin-top: 4px;
|
|
font-size: 12px;
|
|
color: #475569;
|
|
}
|
|
|
|
.csd-result__meta span,
|
|
.csd-result__addr {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.csd-result__meta mat-icon,
|
|
.csd-result__addr mat-icon {
|
|
font-size: 14px;
|
|
width: 14px;
|
|
height: 14px;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.csd-result__addr {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
margin-top: 4px;
|
|
font-size: 12px;
|
|
color: #64748b;
|
|
}
|
|
|
|
.csd-empty {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 2px;
|
|
padding: 28px 16px;
|
|
border: 1.5px dashed #cbd5e1;
|
|
border-radius: 12px;
|
|
background: #f8fafc;
|
|
text-align: center;
|
|
}
|
|
|
|
.csd-empty mat-icon {
|
|
font-size: 36px;
|
|
width: 36px;
|
|
height: 36px;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.csd-empty__title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #334155;
|
|
}
|
|
|
|
.csd-empty__sub {
|
|
font-size: 12px;
|
|
color: #64748b;
|
|
}
|
|
|
|
.csd-actions {
|
|
gap: 8px;
|
|
padding: 12px 24px 20px;
|
|
}
|
|
|
|
@media (max-width: 720px) {
|
|
.csd-grid {
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
|
|
.csd-field--address {
|
|
grid-column: span 2;
|
|
}
|
|
|
|
.csd-result {
|
|
flex-wrap: wrap;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.csd-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.csd-field--name,
|
|
.csd-field--wide,
|
|
.csd-field--address {
|
|
grid-column: span 1;
|
|
}
|
|
}
|
|
`
|
|
]
|
|
})
|
|
export class CustomerSearchDialogComponent implements OnInit {
|
|
private fb = inject(FormBuilder);
|
|
private http = inject(HttpClient);
|
|
private dialogRef = inject(MatDialogRef<CustomerSearchDialogComponent>);
|
|
private snackBar = inject(MatSnackBar);
|
|
private data = inject<CustomerSearchDialogData | null>(MAT_DIALOG_DATA, { optional: true });
|
|
|
|
form!: FormGroup;
|
|
loading = false;
|
|
searched = false;
|
|
results: CustomerSearchResult[] = [];
|
|
|
|
readonly accountTypeOptions: Array<{ key: AccountTypeKey; label: string; icon: string }> = [
|
|
{ key: 'savings', label: 'Savings', icon: 'savings' },
|
|
{ key: 'current', label: 'Current', icon: 'account_balance_wallet' },
|
|
{ key: 'fixedDeposit', label: 'Fixed Deposit', icon: 'lock' },
|
|
{ key: 'recurringDeposit', label: 'Recurring Deposit', icon: 'autorenew' }
|
|
];
|
|
|
|
ngOnInit(): void {
|
|
this.form = this.fb.group({
|
|
clientId: [''],
|
|
name: [''],
|
|
mobileNo: [''],
|
|
accountNumber: [''],
|
|
aadhaarNumber: [''],
|
|
panNumber: [''],
|
|
address: [''],
|
|
savings: [false],
|
|
current: [false],
|
|
fixedDeposit: [false],
|
|
recurringDeposit: [false]
|
|
});
|
|
|
|
// What the caller had typed already is searched for straight away, so opening the dialog from a
|
|
// half-filled field does not mean typing it a second time.
|
|
const typed = (this.data?.prefill ?? '').trim();
|
|
if (typed) {
|
|
const field = this.fieldFor(typed);
|
|
this.form.get(field)?.setValue(typed);
|
|
this.onSearch();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Which field something typed belongs in. Digits alone are a client ID - they are short - until
|
|
* they are as long as a mobile number, and anything with letters in it is a name.
|
|
*/
|
|
private fieldFor(typed: string): 'clientId' | 'mobileNo' | 'name' {
|
|
if (!/^\d+$/.test(typed)) {
|
|
return 'name';
|
|
}
|
|
return typed.length >= MOBILE_NUMBER_LENGTH ? 'mobileNo' : 'clientId';
|
|
}
|
|
|
|
/** Keeps a numeric field to digits however it was filled - typed, pasted or dropped. */
|
|
digitsOnly(name: string): void {
|
|
const control = this.form.get(name);
|
|
const value = String(control?.value ?? '');
|
|
const digits = value.replace(/\D/g, '');
|
|
if (value !== digits) {
|
|
control?.setValue(digits, { emitEvent: false });
|
|
}
|
|
}
|
|
|
|
/** The account screens need an account to open; a caller that only needs the customer opts out. */
|
|
private get requireAccount(): boolean {
|
|
return this.data?.requireAccount !== false;
|
|
}
|
|
|
|
canSelect(r: CustomerSearchResult): boolean {
|
|
return !this.requireAccount || !!r.accountNo;
|
|
}
|
|
|
|
isTypeOn(key: AccountTypeKey): boolean {
|
|
return !!this.form?.get(key)?.value;
|
|
}
|
|
|
|
toggleType(key: AccountTypeKey): void {
|
|
const control = this.form.get(key);
|
|
control?.setValue(!control.value);
|
|
}
|
|
|
|
upperCase(name: string): void {
|
|
const control = this.form.get(name);
|
|
const value = String(control?.value ?? '');
|
|
const upper = value.toUpperCase();
|
|
if (value !== upper) {
|
|
control?.setValue(upper, { emitEvent: false });
|
|
}
|
|
}
|
|
|
|
/** Enter in a text box runs the search; on a button it keeps its own meaning. */
|
|
onEnter(event: Event): void {
|
|
if ((event.target as HTMLElement)?.tagName !== 'INPUT') return;
|
|
event.preventDefault();
|
|
if (!this.loading) this.onSearch();
|
|
}
|
|
|
|
initials(name: string | null): string {
|
|
const words = (name || '').trim().split(/\s+/).filter(Boolean);
|
|
if (words.length === 0) return '?';
|
|
const first = words[0].charAt(0);
|
|
const last = words.length > 1 ? words[words.length - 1].charAt(0) : '';
|
|
return (first + last).toUpperCase();
|
|
}
|
|
|
|
accountTypeLabel(type: string | null): string {
|
|
switch (type) {
|
|
case 'SAVINGS':
|
|
return 'Savings';
|
|
case 'CURRENT':
|
|
return 'Current';
|
|
case 'FIXED_DEPOSIT':
|
|
return 'FD';
|
|
case 'RECURRING_DEPOSIT':
|
|
return 'RD';
|
|
default:
|
|
return '-';
|
|
}
|
|
}
|
|
|
|
onSearch(): void {
|
|
const v = this.form.value;
|
|
const accountTypes: string[] = [];
|
|
if (v.savings) accountTypes.push('SAVINGS');
|
|
if (v.current) accountTypes.push('CURRENT');
|
|
if (v.fixedDeposit) accountTypes.push('FIXED_DEPOSIT');
|
|
if (v.recurringDeposit) accountTypes.push('RECURRING_DEPOSIT');
|
|
|
|
if (
|
|
!v.clientId &&
|
|
!v.name &&
|
|
!v.mobileNo &&
|
|
!v.accountNumber &&
|
|
!v.aadhaarNumber &&
|
|
!v.panNumber &&
|
|
!v.address &&
|
|
accountTypes.length === 0
|
|
) {
|
|
this.snackBar.open('Enter at least one search field, or check an account type.', 'Close', { duration: 3000 });
|
|
return;
|
|
}
|
|
|
|
this.loading = true;
|
|
let params = new HttpParams();
|
|
if (v.clientId) params = params.set('clientId', v.clientId);
|
|
if (v.name) params = params.set('name', v.name);
|
|
if (v.mobileNo) params = params.set('mobileNo', v.mobileNo);
|
|
if (v.accountNumber) params = params.set('accountNumber', v.accountNumber);
|
|
if (v.aadhaarNumber) params = params.set('aadhaarNumber', v.aadhaarNumber);
|
|
if (v.panNumber) params = params.set('panNumber', v.panNumber);
|
|
if (v.address) params = params.set('address', v.address);
|
|
if (accountTypes.length > 0) params = params.set('accountTypes', accountTypes.join(','));
|
|
|
|
this.http.get<CustomerSearchResult[]>('/customer-search', { params }).subscribe({
|
|
next: (data) => {
|
|
this.results = this.data?.uniqueCustomers ? this.onePerCustomer(data || []) : data || [];
|
|
this.searched = true;
|
|
this.loading = false;
|
|
},
|
|
error: (err) => {
|
|
this.loading = false;
|
|
this.snackBar.open('Error: ' + (err.error?.message || err.message), 'Close', { duration: 5000 });
|
|
}
|
|
});
|
|
}
|
|
|
|
/** The search answers per account; one row per customer is the first of each person's rows. */
|
|
private onePerCustomer(rows: CustomerSearchResult[]): CustomerSearchResult[] {
|
|
const seen = new Set<number>();
|
|
return rows.filter((row) => !seen.has(row.clientId) && !!seen.add(row.clientId));
|
|
}
|
|
|
|
onClear(): void {
|
|
this.form.reset({
|
|
clientId: '',
|
|
name: '',
|
|
mobileNo: '',
|
|
accountNumber: '',
|
|
aadhaarNumber: '',
|
|
panNumber: '',
|
|
address: '',
|
|
savings: false,
|
|
current: false,
|
|
fixedDeposit: false,
|
|
recurringDeposit: false
|
|
});
|
|
this.results = [];
|
|
this.searched = false;
|
|
}
|
|
|
|
onSelect(result: CustomerSearchResult): void {
|
|
this.dialogRef.close(result);
|
|
}
|
|
|
|
onClose(): void {
|
|
this.dialogRef.close();
|
|
}
|
|
}
|