feat(cashier): cashier-only cash operations, exchange PAN/denomination checks and Branch Cash Position
- Cash Exchange Entry: customer details with PAN (mandatory above Rs 50,000), cashier card, denomination-wise validation against the master and real-time impact preview. - Inter-Cashier Transfer: sender fixed to the logged-in cashier, both sides' details and denomination holdings shown, blocked until both are allocated cash. - Reconciliation, exchange and inter-cashier operate on the logged-in cashier only. - New Branch Cash Position screen (vault + cashier drawers, denomination-wise). - Customer search dialog shows the KYC PAN. - Cashier menu cleanup: Excess/Closing report and Exchange Detail removed, nav compacted to one row. - Reverse actions removed from cash transfer, settlement, exchange and inter-cashier. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,12 +10,12 @@ 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 { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||
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 { MatTableModule } from '@angular/material/table';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
|
||||
export interface CustomerSearchResult {
|
||||
@@ -30,6 +30,17 @@ export interface CustomerSearchResult {
|
||||
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;
|
||||
}
|
||||
|
||||
type AccountTypeKey = 'savings' | 'current' | 'fixedDeposit' | 'recurringDeposit';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-customer-search-dialog',
|
||||
standalone: true,
|
||||
@@ -41,173 +52,479 @@ export interface CustomerSearchResult {
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatButtonModule,
|
||||
MatTableModule,
|
||||
MatCheckboxModule,
|
||||
MatIconModule,
|
||||
MatProgressBarModule,
|
||||
MatSnackBarModule
|
||||
],
|
||||
template: `
|
||||
<h2 mat-dialog-title>Search Customer</h2>
|
||||
<mat-dialog-content>
|
||||
<p class="hint-text">
|
||||
Fill in any one field, or combine several to narrow the search (e.g. name + account number).
|
||||
</p>
|
||||
<form [formGroup]="form" class="search-form">
|
||||
<mat-form-field appearance="outline" class="field">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput formControlName="name" placeholder="Customer name" />
|
||||
</mat-form-field>
|
||||
<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>
|
||||
|
||||
<mat-form-field appearance="outline" class="field">
|
||||
<mat-label>Account Number</mat-label>
|
||||
<input matInput formControlName="accountNumber" placeholder="Savings / Current / FD / RD" />
|
||||
</mat-form-field>
|
||||
@if (loading) {
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
}
|
||||
|
||||
<mat-form-field appearance="outline" class="field">
|
||||
<mat-label>Aadhaar Number</mat-label>
|
||||
<input matInput formControlName="aadhaarNumber" placeholder="12-digit Aadhaar" maxlength="12" />
|
||||
</mat-form-field>
|
||||
<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 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" class="field">
|
||||
<mat-label>PAN Number</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="panNumber"
|
||||
placeholder="e.g. ABCDE1234F"
|
||||
maxlength="10"
|
||||
style="text-transform: uppercase;"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="csd-field">
|
||||
<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" class="field field--wide">
|
||||
<mat-label>Address</mat-label>
|
||||
<input matInput formControlName="address" placeholder="Street, town, city, or postal code" />
|
||||
</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>
|
||||
|
||||
<div class="account-type-filter" [formGroup]="form">
|
||||
<span class="account-type-filter__label">Account Type</span>
|
||||
<mat-checkbox formControlName="savings">Savings</mat-checkbox>
|
||||
<mat-checkbox formControlName="current">Current</mat-checkbox>
|
||||
<mat-checkbox formControlName="fixedDeposit">Fixed Deposit</mat-checkbox>
|
||||
<mat-checkbox formControlName="recurringDeposit">Recurring Deposit</mat-checkbox>
|
||||
<span class="account-type-filter__hint">(leave all unchecked to search every account type)</span>
|
||||
</div>
|
||||
@if (searched) {
|
||||
<div class="csd-results">
|
||||
<div class="csd-results__head">{{ results.length }} {{ results.length === 1 ? 'result' : 'results' }}</div>
|
||||
|
||||
<div class="results" *ngIf="searched">
|
||||
<p *ngIf="!loading && results.length === 0" class="no-results">No matching customer found.</p>
|
||||
@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>
|
||||
}
|
||||
|
||||
<table mat-table [dataSource]="results" *ngIf="results.length > 0">
|
||||
<ng-container matColumnDef="displayName">
|
||||
<th mat-header-cell *matHeaderCellDef>Name</th>
|
||||
<td mat-cell *matCellDef="let r">{{ r.displayName }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="account">
|
||||
<th mat-header-cell *matHeaderCellDef>Account</th>
|
||||
<td mat-cell *matCellDef="let r">
|
||||
<span *ngIf="r.accountNo">{{ r.accountNo }} ({{ accountTypeLabel(r.accountType) }})</span>
|
||||
<span *ngIf="!r.accountNo" class="muted">No account</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="mobileNo">
|
||||
<th mat-header-cell *matHeaderCellDef>Mobile</th>
|
||||
<td mat-cell *matCellDef="let r">{{ r.mobileNo || '-' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="officeName">
|
||||
<th mat-header-cell *matHeaderCellDef>Branch</th>
|
||||
<td mat-cell *matCellDef="let r">{{ r.officeName || '-' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="panNumber">
|
||||
<th mat-header-cell *matHeaderCellDef>PAN</th>
|
||||
<td mat-cell *matCellDef="let r">{{ r.panNumber || '-' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="aadhaarNumber">
|
||||
<th mat-header-cell *matHeaderCellDef>Aadhaar</th>
|
||||
<td mat-cell *matCellDef="let r">{{ r.aadhaarNumber || '-' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="address">
|
||||
<th mat-header-cell *matHeaderCellDef>Address</th>
|
||||
<td mat-cell *matCellDef="let r">{{ r.address || '-' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let r">
|
||||
<button mat-flat-button color="primary" [disabled]="!r.accountNo" (click)="onSelect(r)">Select</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
</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">
|
||||
<button mat-button (click)="onClose()">Close</button>
|
||||
<button mat-flat-button color="primary" [disabled]="loading" (click)="onSearch()">Search</button>
|
||||
|
||||
<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: [
|
||||
`
|
||||
.hint-text {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
margin: 0 0 12px 0;
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.search-form {
|
||||
|
||||
.csd-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 16px;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 20px 16px 12px 24px;
|
||||
}
|
||||
.field {
|
||||
flex: 1 1 220px;
|
||||
|
||||
.csd-head__icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: 0 0 auto;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
background: #e6f4ee;
|
||||
color: #0f4f3c;
|
||||
}
|
||||
.field--wide {
|
||||
flex-basis: 100%;
|
||||
|
||||
.csd-head__icon mat-icon {
|
||||
font-size: 26px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
.account-type-filter {
|
||||
|
||||
.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 {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.csd-field--address {
|
||||
grid-column: span 3;
|
||||
}
|
||||
|
||||
.csd-upper {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.csd-types {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 16px;
|
||||
margin: 4px 0 16px 0;
|
||||
gap: 8px 12px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.account-type-filter__label {
|
||||
|
||||
.csd-types__label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
}
|
||||
.account-type-filter__hint {
|
||||
|
||||
.csd-types__pills {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.csd-types__hint {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.results {
|
||||
margin-top: 16px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
th {
|
||||
font-weight: 600;
|
||||
|
||||
.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;
|
||||
background-color: #f8fafc;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
border-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
td {
|
||||
color: #334155;
|
||||
}
|
||||
.muted {
|
||||
|
||||
.csd-pill mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.no-results {
|
||||
text-align: center;
|
||||
|
||||
.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;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.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--address {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
`
|
||||
]
|
||||
@@ -217,20 +534,18 @@ export class CustomerSearchDialogComponent implements OnInit {
|
||||
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[] = [];
|
||||
displayedColumns = [
|
||||
'displayName',
|
||||
'account',
|
||||
'mobileNo',
|
||||
'officeName',
|
||||
'panNumber',
|
||||
'aadhaarNumber',
|
||||
'address',
|
||||
'select'
|
||||
|
||||
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 {
|
||||
@@ -247,6 +562,48 @@ export class CustomerSearchDialogComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/** 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':
|
||||
@@ -297,6 +654,22 @@ export class CustomerSearchDialogComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
onClear(): void {
|
||||
this.form.reset({
|
||||
name: '',
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user