feat(dds): upgrade DDS UI - agent, party, daily entry, approvals, product view
- 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>
This commit is contained in:
@@ -37,10 +37,24 @@ export interface CustomerSearchDialogData {
|
||||
* 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,
|
||||
@@ -77,6 +91,19 @@ type AccountTypeKey = 'savings' | 'current' | 'fixedDeposit' | 'recurringDeposit
|
||||
<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>
|
||||
@@ -84,6 +111,20 @@ type AccountTypeKey = 'savings' | 'current' | 'fixedDeposit' | 'recurringDeposit
|
||||
</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" />
|
||||
@@ -266,12 +307,13 @@ type AccountTypeKey = 'savings' | 'current' | 'fixedDeposit' | 'recurringDeposit
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.csd-field--name {
|
||||
.csd-field--name,
|
||||
.csd-field--wide {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.csd-field--address {
|
||||
grid-column: span 3;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.csd-upper {
|
||||
@@ -522,6 +564,7 @@ type AccountTypeKey = 'savings' | 'current' | 'fixedDeposit' | 'recurringDeposit
|
||||
}
|
||||
|
||||
.csd-field--name,
|
||||
.csd-field--wide,
|
||||
.csd-field--address {
|
||||
grid-column: span 1;
|
||||
}
|
||||
@@ -550,7 +593,9 @@ export class CustomerSearchDialogComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.form = this.fb.group({
|
||||
clientId: [''],
|
||||
name: [''],
|
||||
mobileNo: [''],
|
||||
accountNumber: [''],
|
||||
aadhaarNumber: [''],
|
||||
panNumber: [''],
|
||||
@@ -560,6 +605,36 @@ export class CustomerSearchDialogComponent implements OnInit {
|
||||
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. */
|
||||
@@ -627,14 +702,25 @@ export class CustomerSearchDialogComponent implements OnInit {
|
||||
if (v.fixedDeposit) accountTypes.push('FIXED_DEPOSIT');
|
||||
if (v.recurringDeposit) accountTypes.push('RECURRING_DEPOSIT');
|
||||
|
||||
if (!v.name && !v.accountNumber && !v.aadhaarNumber && !v.panNumber && !v.address && accountTypes.length === 0) {
|
||||
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);
|
||||
@@ -643,7 +729,7 @@ export class CustomerSearchDialogComponent implements OnInit {
|
||||
|
||||
this.http.get<CustomerSearchResult[]>('/customer-search', { params }).subscribe({
|
||||
next: (data) => {
|
||||
this.results = data || [];
|
||||
this.results = this.data?.uniqueCustomers ? this.onePerCustomer(data || []) : data || [];
|
||||
this.searched = true;
|
||||
this.loading = false;
|
||||
},
|
||||
@@ -654,9 +740,17 @@ export class CustomerSearchDialogComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/** 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: '',
|
||||
|
||||
Reference in New Issue
Block a user