feat(cashier): enhance transaction entry TYPE/TXN TP options, auto-search, and account type configs
- Replace TYPE dropdown options with Withdrawal Slip, Cheque, Disbursement, Enter Bank Transfer, DD - Move TXN TP. (payment type) into Classification row between BRANCH and AC. TP. - Add GL and LA account types; fix SHR endpoint to /accounts/share - Auto-search A/C NO. on 4+ digits with 400ms debounce - Auto-fill ECS A/C NO. from savingsAccountNumber after account load - Accept suffix/partial accountNo match from list API - Disable broken global /search fallback across all account types - Increase field widths: TYPE 160px, TXN TP. 160px, CHEQUE NO. 165px, TOK. NO. 120px Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
40a436dabe
commit
010adcb44f
@@ -208,6 +208,87 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ── DENOMINATION (Cash Only) ── -->
|
||||||
|
@if (isCashPayment && allDenominations.length > 0) {
|
||||||
|
<div class="pg-card sc-denom">
|
||||||
|
<div class="sc-breakdown__header">
|
||||||
|
<mat-icon>payments</mat-icon>
|
||||||
|
<span>Denomination Entry</span>
|
||||||
|
@if (denominationTotal > 0) {
|
||||||
|
<span
|
||||||
|
class="sc-denom-badge"
|
||||||
|
[class.sc-denom-badge--match]="denomMatchesTotal"
|
||||||
|
[class.sc-denom-badge--mismatch]="!denomMatchesTotal"
|
||||||
|
>
|
||||||
|
₹{{ denominationTotal | number: '1.2-2' }}
|
||||||
|
{{ denomMatchesTotal ? '✓ Matches' : '≠ Mismatch' }}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
@if (denominationTotal === 0) {
|
||||||
|
<span class="sc-denom-required">⚠ Required for Cash</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="sc-denom-body">
|
||||||
|
<table class="pg-table sc-denom-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Denomination</th>
|
||||||
|
<th class="num">Count</th>
|
||||||
|
<th class="num">Amount</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (d of allDenominations; track d.id) {
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span class="sc-denom-chip" [class.sc-denom-chip--coin]="d.kind === 'coin'">{{ d.label }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="num">
|
||||||
|
<div class="sc-denom-stepper">
|
||||||
|
<button type="button" (click)="bumpDenom(d, -1)" [disabled]="countFor(d) === 0">−</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
[value]="countFor(d) || ''"
|
||||||
|
placeholder="0"
|
||||||
|
(input)="onDenomChange(d, $any($event.target).value)"
|
||||||
|
/>
|
||||||
|
<button type="button" (click)="bumpDenom(d, 1)">+</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="num">{{ subtotalFor(d) > 0 ? (subtotalFor(d) | number: '1.2-2') : '—' }}</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Total</strong></td>
|
||||||
|
<td></td>
|
||||||
|
<td class="num">
|
||||||
|
<strong>₹{{ denominationTotal | number: '1.2-2' }}</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
@if (!denomMatchesTotal && denominationTotal > 0) {
|
||||||
|
<div class="sc-denom-mismatch">
|
||||||
|
<mat-icon>warning</mat-icon>
|
||||||
|
Denomination ₹{{ denominationTotal | number: '1.2-2' }} must equal Total Payable ₹{{
|
||||||
|
totalAmount | number: '1.2-2'
|
||||||
|
}}.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (denominationTotal > 0) {
|
||||||
|
<div class="sc-denom-actions">
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="clearDenominations()">
|
||||||
|
<mat-icon>clear</mat-icon> Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Transaction Details -->
|
<!-- Transaction Details -->
|
||||||
<div class="pg-toolbar pg-toolbar--mat">
|
<div class="pg-toolbar pg-toolbar--mat">
|
||||||
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||||
|
|||||||
@@ -428,6 +428,166 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Denomination Panel ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.sc-denom {
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-denom-body {
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-denom-table {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 7px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tfoot td {
|
||||||
|
border-top: 2px solid #e2e8f0;
|
||||||
|
background: #f8fafc;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-denom-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #1d4ed8;
|
||||||
|
border: 1px solid #bfdbfe;
|
||||||
|
|
||||||
|
&--coin {
|
||||||
|
background: #fffbeb;
|
||||||
|
color: #d97706;
|
||||||
|
border-color: #fde68a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-denom-stepper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border: 1.5px solid #e2e8f0;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: #e0f2fe;
|
||||||
|
border-color: #7dd3fc;
|
||||||
|
color: #0369a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.35;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 62px;
|
||||||
|
height: 28px;
|
||||||
|
border: 1.5px solid #e2e8f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: inherit;
|
||||||
|
text-align: right;
|
||||||
|
outline: none;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: #1353a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-outer-spin-button,
|
||||||
|
&::-webkit-inner-spin-button {
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-denom-badge {
|
||||||
|
margin-left: auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 22px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
|
||||||
|
&--match {
|
||||||
|
background: #f0fdf4;
|
||||||
|
color: #15803d;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--mismatch {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #dc2626;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-denom-required {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-denom-mismatch {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
background: #fef2f2;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #dc2626;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-denom-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Success ──────────────────────────────────────────────────────────────────
|
// ── Success ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
.sc-success {
|
.sc-success {
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ import { CashierService } from '../cashier.service';
|
|||||||
|
|
||||||
type PageState = 'idle' | 'searching' | 'loaded' | 'submitting' | 'success';
|
type PageState = 'idle' | 'searching' | 'loaded' | 'submitting' | 'success';
|
||||||
|
|
||||||
|
interface Denomination {
|
||||||
|
id: number;
|
||||||
|
value: number;
|
||||||
|
kind: 'note' | 'coin';
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface ShareCharge {
|
interface ShareCharge {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -68,6 +75,11 @@ export class ShareCollectionComponent implements OnInit {
|
|||||||
requestedSharesInput = 0;
|
requestedSharesInput = 0;
|
||||||
pricePerShare = 0;
|
pricePerShare = 0;
|
||||||
|
|
||||||
|
// ── Denomination ────────────────────────────────────────────────────────────
|
||||||
|
notes: Denomination[] = [];
|
||||||
|
coins: Denomination[] = [];
|
||||||
|
denominationCounts: Record<string, number> = {};
|
||||||
|
|
||||||
get shareAmount(): number {
|
get shareAmount(): number {
|
||||||
return this.requestedSharesInput * this.pricePerShare;
|
return this.requestedSharesInput * this.pricePerShare;
|
||||||
}
|
}
|
||||||
@@ -84,8 +96,34 @@ export class ShareCollectionComponent implements OnInit {
|
|||||||
return this.account?.currency?.displaySymbol ?? '₹';
|
return this.account?.currency?.displaySymbol ?? '₹';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isCashPayment(): boolean {
|
||||||
|
if (this.paymentTypeId === null) return true;
|
||||||
|
const pt = this.paymentTypes.find((p: any) => p.id === this.paymentTypeId);
|
||||||
|
return !!pt?.isCashPayment;
|
||||||
|
}
|
||||||
|
|
||||||
|
get allDenominations(): Denomination[] {
|
||||||
|
const desc = (a: Denomination, b: Denomination) => b.value - a.value;
|
||||||
|
return [
|
||||||
|
...[...this.notes].sort(desc),
|
||||||
|
...[...this.coins].sort(desc)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
get denominationTotal(): number {
|
||||||
|
return this.allDenominations.reduce((s, d) => s + this.subtotalFor(d), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
get denomMatchesTotal(): boolean {
|
||||||
|
return this.denominationTotal === this.totalAmount;
|
||||||
|
}
|
||||||
|
|
||||||
get isValid(): boolean {
|
get isValid(): boolean {
|
||||||
return !!this.account && this.totalAmount > 0 && !!this.transactionDate;
|
if (!this.account || this.totalAmount <= 0 || !this.transactionDate) return false;
|
||||||
|
if (this.isCashPayment && this.allDenominations.length > 0) {
|
||||||
|
return this.denomMatchesTotal;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -95,6 +133,50 @@ export class ShareCollectionComponent implements OnInit {
|
|||||||
.getPaymentTypes()
|
.getPaymentTypes()
|
||||||
.pipe(catchError(() => of([])))
|
.pipe(catchError(() => of([])))
|
||||||
.subscribe((types: any[]) => (this.paymentTypes = types || []));
|
.subscribe((types: any[]) => (this.paymentTypes = types || []));
|
||||||
|
this.loadDenominations();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadDenominations(): void {
|
||||||
|
this.service
|
||||||
|
.getActiveDenominations()
|
||||||
|
.pipe(catchError(() => of([])))
|
||||||
|
.subscribe((list: any[]) => {
|
||||||
|
const rows: Denomination[] = (list || []).map((d: any) => ({
|
||||||
|
id: d.id,
|
||||||
|
value: d.denomination ?? d.value,
|
||||||
|
kind: d.type === 'coin' ? 'coin' : 'note',
|
||||||
|
label: '₹' + (d.denomination ?? d.value)
|
||||||
|
}));
|
||||||
|
const order = (a: Denomination, b: Denomination) => b.value - a.value;
|
||||||
|
this.notes = rows.filter((r) => r.kind === 'note').sort(order);
|
||||||
|
this.coins = rows.filter((r) => r.kind === 'coin').sort(order);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Denomination helpers ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
denomKey(d: Denomination): string {
|
||||||
|
return `${d.kind}:${d.value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
countFor(d: Denomination): number {
|
||||||
|
return this.denominationCounts[this.denomKey(d)] || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
subtotalFor(d: Denomination): number {
|
||||||
|
return this.countFor(d) * d.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
onDenomChange(d: Denomination, raw: any): void {
|
||||||
|
this.denominationCounts[this.denomKey(d)] = Math.max(0, Math.floor(Number(raw) || 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
bumpDenom(d: Denomination, delta: number): void {
|
||||||
|
this.denominationCounts[this.denomKey(d)] = Math.max(0, this.countFor(d) + delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearDenominations(): void {
|
||||||
|
this.denominationCounts = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
searchAccount(): void {
|
searchAccount(): void {
|
||||||
@@ -195,5 +277,6 @@ export class ShareCollectionComponent implements OnInit {
|
|||||||
this.pricePerShare = 0;
|
this.pricePerShare = 0;
|
||||||
this.collectionResult = null;
|
this.collectionResult = null;
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
|
this.clearDenominations();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,10 +83,11 @@
|
|||||||
<label class="ws-f__lbl" title="Transaction Type">TYPE</label>
|
<label class="ws-f__lbl" title="Transaction Type">TYPE</label>
|
||||||
<select class="ws-f__ctl" [(ngModel)]="row.typ" (click)="$event.stopPropagation()">
|
<select class="ws-f__ctl" [(ngModel)]="row.typ" (click)="$event.stopPropagation()">
|
||||||
<option value="~">~</option>
|
<option value="~">~</option>
|
||||||
<option value="Cash">Cash</option>
|
<option value="Withdrawal Slip">Withdrawal Slip</option>
|
||||||
<option value="Transfer">Transfer</option>
|
<option value="Cheque">Cheque</option>
|
||||||
<option value="Clearing">Clearing</option>
|
<option value="Disbursement">Disbursement</option>
|
||||||
<option value="Journal">Journal</option>
|
<option value="Enter Bank Transfer">Enter Bank Transfer</option>
|
||||||
|
<option value="DD">DD (Demand Draft)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="ws-f ws-f--branch">
|
<div class="ws-f ws-f--branch">
|
||||||
@@ -98,12 +99,17 @@
|
|||||||
<option [ngValue]="27">027 · Industrial</option>
|
<option [ngValue]="27">027 · Industrial</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="ws-f ws-f--trtp">
|
<div class="ws-f ws-f--paytype">
|
||||||
<label class="ws-f__lbl" title="Transaction Type">TXN TP.</label>
|
<label class="ws-f__lbl ws-f__lbl--accent" title="Transaction Type">TXN TP.</label>
|
||||||
<select class="ws-f__ctl" [(ngModel)]="row.trTp" (click)="$event.stopPropagation()">
|
<select
|
||||||
<option value=""></option>
|
class="ws-f__ctl ws-f__ctl--accent"
|
||||||
@for (t of trTpOptions; track t) {
|
[(ngModel)]="row.paymentTypeId"
|
||||||
<option [value]="t">{{ t }}</option>
|
(click)="$event.stopPropagation()"
|
||||||
|
[class.ws-f__ctl--req]="!row.paymentTypeId"
|
||||||
|
>
|
||||||
|
<option [ngValue]="null">— Select —</option>
|
||||||
|
@for (pt of paymentTypes; track pt.id) {
|
||||||
|
<option [ngValue]="pt.id">{{ pt.name }}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,7 +133,8 @@
|
|||||||
<label class="ws-f__lbl" title="Account Number">A/C NO.</label>
|
<label class="ws-f__lbl" title="Account Number">A/C NO.</label>
|
||||||
<input
|
<input
|
||||||
class="ws-f__ctl ws-f__ctl--mono"
|
class="ws-f__ctl ws-f__ctl--mono"
|
||||||
[(ngModel)]="row.acNo"
|
[value]="row.acNo"
|
||||||
|
(input)="onAcNoInput(row, i, $any($event.target).value)"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
(blur)="onAcNoBlur(row, i)"
|
(blur)="onAcNoBlur(row, i)"
|
||||||
placeholder="Account number"
|
placeholder="Account number"
|
||||||
@@ -172,20 +179,6 @@
|
|||||||
<div class="ws-group">
|
<div class="ws-group">
|
||||||
<span class="ws-caption">Payment instrument</span>
|
<span class="ws-caption">Payment instrument</span>
|
||||||
<div class="ws-group__fields">
|
<div class="ws-group__fields">
|
||||||
<div class="ws-f ws-f--paytype">
|
|
||||||
<label class="ws-f__lbl ws-f__lbl--accent" title="Payment Type">PAY TYPE</label>
|
|
||||||
<select
|
|
||||||
class="ws-f__ctl ws-f__ctl--accent"
|
|
||||||
[(ngModel)]="row.paymentTypeId"
|
|
||||||
(click)="$event.stopPropagation()"
|
|
||||||
[class.ws-f__ctl--req]="!row.paymentTypeId"
|
|
||||||
>
|
|
||||||
<option [ngValue]="null">— Select —</option>
|
|
||||||
@for (pt of paymentTypes; track pt.id) {
|
|
||||||
<option [ngValue]="pt.id">{{ pt.name }}</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="ws-f ws-f--amt">
|
<div class="ws-f ws-f--amt">
|
||||||
<label class="ws-f__lbl" title="Transaction Amount">AMOUNT</label>
|
<label class="ws-f__lbl" title="Transaction Amount">AMOUNT</label>
|
||||||
<div class="ws-amt-wrap" [class.ws-amt-wrap--cash]="isCashPayment && selectedRow === row">
|
<div class="ws-amt-wrap" [class.ws-amt-wrap--cash]="isCashPayment && selectedRow === row">
|
||||||
@@ -1059,10 +1052,11 @@
|
|||||||
<label class="ws-f__lbl">TYPE</label>
|
<label class="ws-f__lbl">TYPE</label>
|
||||||
<select class="ws-f__ctl" [(ngModel)]="row.typ">
|
<select class="ws-f__ctl" [(ngModel)]="row.typ">
|
||||||
<option value="~">~</option>
|
<option value="~">~</option>
|
||||||
<option value="Cash">Cash</option>
|
<option value="Withdrawal Slip">Withdrawal Slip</option>
|
||||||
<option value="Transfer">Transfer</option>
|
<option value="Cheque">Cheque</option>
|
||||||
<option value="Clearing">Clearing</option>
|
<option value="Disbursement">Disbursement</option>
|
||||||
<option value="Journal">Journal</option>
|
<option value="Enter Bank Transfer">Enter Bank Transfer</option>
|
||||||
|
<option value="DD">DD (Demand Draft)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="ws-f ws-f--branch">
|
<div class="ws-f ws-f--branch">
|
||||||
@@ -1074,12 +1068,16 @@
|
|||||||
<option [ngValue]="27">027 · Industrial</option>
|
<option [ngValue]="27">027 · Industrial</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="ws-f ws-f--trtp">
|
<div class="ws-f ws-f--paytype">
|
||||||
<label class="ws-f__lbl">TXN TP.</label>
|
<label class="ws-f__lbl ws-f__lbl--accent">TXN TP.</label>
|
||||||
<select class="ws-f__ctl" [(ngModel)]="row.trTp">
|
<select
|
||||||
<option value=""></option>
|
class="ws-f__ctl ws-f__ctl--accent"
|
||||||
@for (t of trTpOptions; track t) {
|
[(ngModel)]="row.paymentTypeId"
|
||||||
<option [value]="t">{{ t }}</option>
|
[class.ws-f__ctl--req]="!row.paymentTypeId"
|
||||||
|
>
|
||||||
|
<option [ngValue]="null">— Select —</option>
|
||||||
|
@for (pt of paymentTypes; track pt.id) {
|
||||||
|
<option [ngValue]="pt.id">{{ pt.name }}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -1101,7 +1099,8 @@
|
|||||||
<label class="ws-f__lbl">A/C NO.</label>
|
<label class="ws-f__lbl">A/C NO.</label>
|
||||||
<input
|
<input
|
||||||
class="ws-f__ctl ws-f__ctl--mono"
|
class="ws-f__ctl ws-f__ctl--mono"
|
||||||
[(ngModel)]="row.acNo"
|
[value]="row.acNo"
|
||||||
|
(input)="onAcNoInput(row, i, $any($event.target).value)"
|
||||||
(blur)="onAcNoBlur(row, i)"
|
(blur)="onAcNoBlur(row, i)"
|
||||||
placeholder="Account number"
|
placeholder="Account number"
|
||||||
/>
|
/>
|
||||||
@@ -1138,19 +1137,6 @@
|
|||||||
<div class="ws-group">
|
<div class="ws-group">
|
||||||
<span class="ws-caption">Payment instrument</span>
|
<span class="ws-caption">Payment instrument</span>
|
||||||
<div class="ws-group__fields">
|
<div class="ws-group__fields">
|
||||||
<div class="ws-f ws-f--paytype">
|
|
||||||
<label class="ws-f__lbl ws-f__lbl--accent">PAY TYPE</label>
|
|
||||||
<select
|
|
||||||
class="ws-f__ctl"
|
|
||||||
[(ngModel)]="row.paymentTypeId"
|
|
||||||
[class.ws-f__ctl--req]="!row.paymentTypeId"
|
|
||||||
>
|
|
||||||
<option [ngValue]="null">— Select —</option>
|
|
||||||
@for (pt of paymentTypes; track pt.id) {
|
|
||||||
<option [ngValue]="pt.id">{{ pt.name }}</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="ws-f ws-f--amt">
|
<div class="ws-f ws-f--amt">
|
||||||
<label class="ws-f__lbl">AMOUNT</label>
|
<label class="ws-f__lbl">AMOUNT</label>
|
||||||
<div class="ws-amt-wrap" [class.ws-amt-wrap--cash]="isCashPayment && selectedRow === row">
|
<div class="ws-amt-wrap" [class.ws-amt-wrap--cash]="isCashPayment && selectedRow === row">
|
||||||
|
|||||||
@@ -620,7 +620,7 @@ $ws-fld-bd: #c8d8eb;
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
&--type {
|
&--type {
|
||||||
width: 90px;
|
width: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--branch {
|
&--branch {
|
||||||
@@ -636,13 +636,13 @@ $ws-fld-bd: #c8d8eb;
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--acno {
|
&--acno {
|
||||||
flex: 1.6;
|
flex: 1;
|
||||||
min-width: 150px;
|
min-width: 110px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--ecsno {
|
&--ecsno {
|
||||||
flex: 1;
|
flex: 1.6;
|
||||||
min-width: 110px;
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--dbcr {
|
&--dbcr {
|
||||||
@@ -651,7 +651,7 @@ $ws-fld-bd: #c8d8eb;
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--paytype {
|
&--paytype {
|
||||||
width: 110px;
|
width: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--amt {
|
&--amt {
|
||||||
@@ -659,7 +659,7 @@ $ws-fld-bd: #c8d8eb;
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--cheqno {
|
&--cheqno {
|
||||||
width: 100px;
|
width: 165px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--cheqdt {
|
&--cheqdt {
|
||||||
@@ -667,7 +667,7 @@ $ws-fld-bd: #c8d8eb;
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--tok {
|
&--tok {
|
||||||
width: 72px;
|
width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--desc {
|
&--desc {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import { Component, OnInit, OnDestroy, inject } from '@angular/core';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { catchError } from 'rxjs/operators';
|
import { catchError, debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||||
import { forkJoin, of } from 'rxjs';
|
import { forkJoin, of, Subject } from 'rxjs';
|
||||||
import { MatDialog, MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MatDialog, MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { CashierService } from '../cashier.service';
|
import { CashierService } from '../cashier.service';
|
||||||
import { AuthenticationService } from '../../core/authentication/authentication.service';
|
import { AuthenticationService } from '../../core/authentication/authentication.service';
|
||||||
@@ -659,6 +659,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
// Search token — cancels stale async results
|
// Search token — cancels stale async results
|
||||||
private searchToken = 0;
|
private searchToken = 0;
|
||||||
|
private acNoInput$ = new Subject<{ row: TxnRow; index: number; value: string }>();
|
||||||
|
|
||||||
// Joint holder expanded details (keyed by index)
|
// Joint holder expanded details (keyed by index)
|
||||||
expandedHolder: number | null = null;
|
expandedHolder: number | null = null;
|
||||||
@@ -710,7 +711,8 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
'FD',
|
'FD',
|
||||||
'RD',
|
'RD',
|
||||||
'SHR',
|
'SHR',
|
||||||
'CC'
|
'GL',
|
||||||
|
'LA'
|
||||||
];
|
];
|
||||||
dbCrOptions = [
|
dbCrOptions = [
|
||||||
'Dr',
|
'Dr',
|
||||||
@@ -753,6 +755,18 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
.pipe(catchError(() => of([])))
|
.pipe(catchError(() => of([])))
|
||||||
.subscribe((types: any[]) => (this.paymentTypes = types ?? []));
|
.subscribe((types: any[]) => (this.paymentTypes = types ?? []));
|
||||||
|
|
||||||
|
this.acNoInput$
|
||||||
|
.pipe(
|
||||||
|
debounceTime(400),
|
||||||
|
distinctUntilChanged((a, b) => a.value === b.value && a.row === b.row)
|
||||||
|
)
|
||||||
|
.subscribe(({ row, index, value }) => {
|
||||||
|
if (value.trim().length >= 4) {
|
||||||
|
this.selectedRow = row;
|
||||||
|
this.loadAccountDetails(value.trim(), row.acTyp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (this.isClerk) {
|
if (this.isClerk) {
|
||||||
const saved = this.clerkUiState.snapshot.savedAccount;
|
const saved = this.clerkUiState.snapshot.savedAccount;
|
||||||
if (saved) {
|
if (saved) {
|
||||||
@@ -1022,6 +1036,11 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
this.loadAccountDetails(row.acNo.trim(), row.acTyp);
|
this.loadAccountDetails(row.acNo.trim(), row.acTyp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onAcNoInput(row: TxnRow, index: number, value: string): void {
|
||||||
|
row.acNo = value;
|
||||||
|
this.acNoInput$.next({ row, index, value });
|
||||||
|
}
|
||||||
|
|
||||||
onRowSelect(row: TxnRow, index: number): void {
|
onRowSelect(row: TxnRow, index: number): void {
|
||||||
this.selectedRow = row;
|
this.selectedRow = row;
|
||||||
// Only reload if account number changed or nothing is loaded yet
|
// Only reload if account number changed or nothing is loaded yet
|
||||||
@@ -1055,32 +1074,24 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
// Step 1: search by accountNo on the correct endpoint for this account type
|
// Step 1: search by accountNo on the correct endpoint for this account type
|
||||||
this.http
|
this.http
|
||||||
.get<any>(`${cfg.list}?accountNo=${encodeURIComponent(acNo)}&offset=0&limit=1`)
|
.get<any>(`${cfg.list}?accountNo=${encodeURIComponent(acNo)}&offset=0&limit=${cfg.maxLimit}`)
|
||||||
.pipe(catchError(() => of(null)))
|
.pipe(catchError(() => of(null)))
|
||||||
.subscribe((res: any) => {
|
.subscribe((res: any) => {
|
||||||
if (token !== this.searchToken) return; // stale result — newer search started
|
if (token !== this.searchToken) return; // stale result — newer search started
|
||||||
const allItems: any[] = res?.pageItems ?? (Array.isArray(res) ? res : []);
|
const allItems: any[] = res?.pageItems ?? (Array.isArray(res) ? res : []);
|
||||||
// Find exact match by accountNo — prevents taking wrong first item when API
|
// Exact match first, then suffix/partial match (backend may return by last-N-digits)
|
||||||
// returns all accounts as an array (e.g. recurringdepositaccounts)
|
const exactMatch = allItems.find((x: any) => x.accountNo === acNo);
|
||||||
const match = allItems.find((x: any) => x.accountNo === acNo) ?? allItems[0];
|
const partialMatch =
|
||||||
|
!exactMatch && allItems.length === 1
|
||||||
|
? allItems.find((x: any) => x.accountNo?.endsWith(acNo) || x.accountNo?.includes(acNo))
|
||||||
|
: null;
|
||||||
|
const match = exactMatch ?? partialMatch ?? null;
|
||||||
if (match?.id) {
|
if (match?.id) {
|
||||||
this.fetchAllDetails(match.id, acNo, cfg, token, acTyp);
|
this.fetchAllDetails(match.id, match.accountNo, cfg, token, acTyp);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Fallback: global search
|
this.loadingAccount = false;
|
||||||
this.http
|
this.accountError = 'Account not found for: ' + acNo;
|
||||||
.get<any>(`/search?query=${encodeURIComponent(acNo)}&resource=${cfg.search}`)
|
|
||||||
.pipe(catchError(() => of([])))
|
|
||||||
.subscribe((found: any) => {
|
|
||||||
if (token !== this.searchToken) return;
|
|
||||||
const list: any[] = Array.isArray(found) ? found : [];
|
|
||||||
if (list.length > 0 && list[0].entityId) {
|
|
||||||
this.fetchAllDetails(list[0].entityId, acNo, cfg, token, acTyp);
|
|
||||||
} else {
|
|
||||||
this.loadingAccount = false;
|
|
||||||
this.accountError = 'Account not found for: ' + acNo;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1093,10 +1104,52 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
siAccountType: number;
|
siAccountType: number;
|
||||||
txnBase: string;
|
txnBase: string;
|
||||||
txnSearchSupported: boolean;
|
txnSearchSupported: boolean;
|
||||||
|
maxLimit: number;
|
||||||
|
skipGlobalSearch: boolean;
|
||||||
} {
|
} {
|
||||||
const t = acTyp?.toUpperCase();
|
const t = acTyp?.toUpperCase();
|
||||||
if (t === 'CA' || t === 'CB' || t === 'CC') {
|
if (t === 'LA') {
|
||||||
// Current account: custom /transactions/search endpoint added to backend
|
return {
|
||||||
|
list: '/loans',
|
||||||
|
detail: '/loans',
|
||||||
|
nominee: 'LA',
|
||||||
|
search: 'loan',
|
||||||
|
siAccountType: 1,
|
||||||
|
txnBase: '/loans',
|
||||||
|
txnSearchSupported: true,
|
||||||
|
maxLimit: 1,
|
||||||
|
skipGlobalSearch: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (t === 'SHR') {
|
||||||
|
// Backend ignores accountNo filter on list — fetch all and match client-side
|
||||||
|
// Global /search?resource=share returns 500 — skip it
|
||||||
|
return {
|
||||||
|
list: '/accounts/share',
|
||||||
|
detail: '/accounts/share',
|
||||||
|
nominee: 'SHR',
|
||||||
|
search: 'share',
|
||||||
|
siAccountType: 4,
|
||||||
|
txnBase: '/accounts/share',
|
||||||
|
txnSearchSupported: false,
|
||||||
|
maxLimit: 500,
|
||||||
|
skipGlobalSearch: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (t === 'GL') {
|
||||||
|
return {
|
||||||
|
list: '/glaccounts',
|
||||||
|
detail: '/glaccounts',
|
||||||
|
nominee: 'GL',
|
||||||
|
search: 'glaccount',
|
||||||
|
siAccountType: 0,
|
||||||
|
txnBase: '/glaccounts',
|
||||||
|
txnSearchSupported: false,
|
||||||
|
maxLimit: 500,
|
||||||
|
skipGlobalSearch: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (t === 'CA' || t === 'CB') {
|
||||||
return {
|
return {
|
||||||
list: '/currentaccounts',
|
list: '/currentaccounts',
|
||||||
detail: '/currentaccounts',
|
detail: '/currentaccounts',
|
||||||
@@ -1104,11 +1157,12 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
search: 'currentaccount',
|
search: 'currentaccount',
|
||||||
siAccountType: 3,
|
siAccountType: 3,
|
||||||
txnBase: '/currentaccounts',
|
txnBase: '/currentaccounts',
|
||||||
txnSearchSupported: true
|
txnSearchSupported: true,
|
||||||
|
maxLimit: 1,
|
||||||
|
skipGlobalSearch: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (t === 'FD') {
|
if (t === 'FD') {
|
||||||
// FD stored in savings table → use /savingsaccounts for transaction search
|
|
||||||
return {
|
return {
|
||||||
list: '/savingsaccounts',
|
list: '/savingsaccounts',
|
||||||
detail: '/fixeddepositaccounts',
|
detail: '/fixeddepositaccounts',
|
||||||
@@ -1116,11 +1170,12 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
search: 'fixeddeposit',
|
search: 'fixeddeposit',
|
||||||
siAccountType: 2,
|
siAccountType: 2,
|
||||||
txnBase: '/savingsaccounts',
|
txnBase: '/savingsaccounts',
|
||||||
txnSearchSupported: true
|
txnSearchSupported: true,
|
||||||
|
maxLimit: 1,
|
||||||
|
skipGlobalSearch: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (t === 'RD') {
|
if (t === 'RD') {
|
||||||
// RD stored in savings table → use /savingsaccounts for transaction search
|
|
||||||
return {
|
return {
|
||||||
list: '/savingsaccounts',
|
list: '/savingsaccounts',
|
||||||
detail: '/recurringdepositaccounts',
|
detail: '/recurringdepositaccounts',
|
||||||
@@ -1128,10 +1183,12 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
search: 'recurringdeposit',
|
search: 'recurringdeposit',
|
||||||
siAccountType: 2,
|
siAccountType: 2,
|
||||||
txnBase: '/savingsaccounts',
|
txnBase: '/savingsaccounts',
|
||||||
txnSearchSupported: true
|
txnSearchSupported: true,
|
||||||
|
maxLimit: 1,
|
||||||
|
skipGlobalSearch: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// SD — savings with full search support
|
// SD — savings
|
||||||
return {
|
return {
|
||||||
list: '/savingsaccounts',
|
list: '/savingsaccounts',
|
||||||
detail: '/savingsaccounts',
|
detail: '/savingsaccounts',
|
||||||
@@ -1139,7 +1196,9 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
search: 'saving',
|
search: 'saving',
|
||||||
siAccountType: 2,
|
siAccountType: 2,
|
||||||
txnBase: '/savingsaccounts',
|
txnBase: '/savingsaccounts',
|
||||||
txnSearchSupported: true
|
txnSearchSupported: true,
|
||||||
|
maxLimit: 1,
|
||||||
|
skipGlobalSearch: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1182,6 +1241,10 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
this.accountDetails = acc;
|
this.accountDetails = acc;
|
||||||
this.jointHolders = acc.jointHolders ?? [];
|
this.jointHolders = acc.jointHolders ?? [];
|
||||||
|
// Auto-fill ECS A/C NO. with savings account number (SHR) or account number
|
||||||
|
if (this.selectedRow) {
|
||||||
|
this.selectedRow.ecsAcNo = acc.savingsAccountNumber ?? acc.accountNo ?? '';
|
||||||
|
}
|
||||||
if (this.isClerk) this.clerkUiState.setFacilities(this.facilities, acNo, acTyp);
|
if (this.isClerk) this.clerkUiState.setFacilities(this.facilities, acNo, acTyp);
|
||||||
// Compute same-day compliance indicators from included transactions
|
// Compute same-day compliance indicators from included transactions
|
||||||
this.computeSameDayNeftRtgs(acc.transactions);
|
this.computeSameDayNeftRtgs(acc.transactions);
|
||||||
|
|||||||
Reference in New Issue
Block a user