feat: cash-only registers, day book sections and DD amendments
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s

This commit is contained in:
2026-09-03 18:26:22 +05:30
parent 6442562df0
commit fbce73fd17
111 changed files with 9076 additions and 3507 deletions
@@ -0,0 +1,497 @@
/**
* 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 { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } 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';
export interface CashierIssueDdDialogData {
leaflet: any;
branchName: string;
sponsorBankName: string;
faceValue: number;
chargesAmount: number;
gstPercentage: number;
gstAmount: number;
totalCashPayable: number;
/**
* How the draft is being paid for. Cash is a walk-in purchaser the bank may know nothing about,
* so their name, address and Aadhaar have to be captured here. An account debit is a member whose
* account is already on screen — those fields are not asked for, and must not be sent, because
* the identity is the account holder's rather than a walk-in's.
*/
paymentMethod?: 'CASH' | 'ACCOUNT_DEBIT';
/** The account being debited, shown so the clerk can see what is funding the draft. */
accountNo?: string;
/** The account holder, for the same reason. */
accountHolderName?: string;
}
@Component({
selector: 'mifosx-cashier-issue-dd-dialog',
standalone: true,
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
MatIconModule
],
template: `
<div class="dd-dialog-shell">
<div class="dd-dialog-header">
<div class="dd-dialog-header__title">
<mat-icon class="dd-icon">receipt_long</mat-icon>
<div>
<h2>{{ isAccountDebit ? 'Account-Debit Demand Draft (DD)' : 'Walk-in Cash Demand Draft (DD)' }}</h2>
<p>
Next available DD leaflet auto-selected ·
{{ isAccountDebit ? 'Enter beneficiary details' : 'Enter purchaser &amp; beneficiary details' }}
</p>
</div>
</div>
<button mat-icon-button type="button" (click)="onClose()" class="close-btn">
<mat-icon>close</mat-icon>
</button>
</div>
<mat-dialog-content class="dd-dialog-body">
<!-- Draft & Financials Summary Card -->
<div class="dd-summary-card">
<div class="summary-col">
<span class="label">Auto-Selected Leaflet</span>
<strong class="dd-number-highlight">{{ data.leaflet.ddNumber }}</strong>
<span class="sublabel">{{ data.sponsorBankName || 'Sponsor Bank' }} · {{ data.branchName }}</span>
</div>
<div class="summary-divider"></div>
<div class="summary-col">
<span class="label">Face Value</span>
<strong class="amount">₹{{ data.faceValue | number: '1.2-2' }}</strong>
<span class="sublabel">+ Charges ₹{{ data.chargesAmount | number: '1.2-2' }}</span>
</div>
<div class="summary-divider"></div>
<div class="summary-col">
<span class="label">GST ({{ data.gstPercentage }}%)</span>
<strong class="amount">₹{{ data.gstAmount | number: '1.2-2' }}</strong>
<span class="sublabel">on charges only</span>
</div>
<div class="summary-divider"></div>
<div class="summary-col highlight-col">
<span class="label">{{ isAccountDebit ? 'Total Debited' : 'Total Cash Collected' }}</span>
<strong class="total-amount">₹{{ data.totalCashPayable | number: '1.2-2' }}</strong>
<span class="sublabel">
@if (isAccountDebit) {
{{ data.accountNo || 'Account' }}
} @else {
Denominations verified
}
</span>
</div>
</div>
<!--
The account funding the draft, stated before the form.
A clerk reaching this dialog has an account loaded on the workstation behind it, and the
debit is irreversible once a checker approves. Naming the account and its holder here is
what makes a wrong account visible while it can still be cancelled.
-->
@if (isAccountDebit) {
<div class="dd-funding-note">
<mat-icon>account_balance_wallet</mat-icon>
<div>
<strong>Debiting {{ data.accountNo }}</strong>
@if (data.accountHolderName) {
· {{ data.accountHolderName }}
}
<div class="dd-funding-sub">
₹{{ data.totalCashPayable | number: '1.2-2' }} will be debited from this account when a checker
approves. No cash is taken at the counter.
</div>
</div>
</div>
}
<form [formGroup]="form" class="dd-form">
<!--
Purchaser identity is asked for only when the bank has none: a walk-in paying cash. On an
account debit the purchaser IS the account holder, already identified and KYC'd, and
re-capturing a name and Aadhaar here would invite a mismatch against the account on record.
-->
@if (!isAccountDebit) {
<div class="form-section-title">Walk-in Purchaser Details</div>
<div class="form-row">
<mat-form-field appearance="outline" class="form-field-flex">
<mat-label>Walk-in Purchaser Name *</mat-label>
<input
matInput
formControlName="walkinPurchaserName"
placeholder="e.g. RAMESH CHANDRA"
style="text-transform: uppercase;"
/>
<mat-error *ngIf="form.get('walkinPurchaserName')?.hasError('required')"
>Purchaser name is required</mat-error
>
<mat-error *ngIf="form.get('walkinPurchaserName')?.hasError('minlength')"
>Must be at least 3 characters</mat-error
>
</mat-form-field>
<mat-form-field appearance="outline" class="form-field-flex">
<mat-label>12-Digit Aadhaar Number *</mat-label>
<input
matInput
formControlName="walkinPurchaserAadhaar"
placeholder="12-digit Aadhaar"
maxlength="12"
/>
<mat-hint>Masked to last 4 digits before storage</mat-hint>
<mat-error *ngIf="form.get('walkinPurchaserAadhaar')?.hasError('required')"
>Aadhaar number is required</mat-error
>
<mat-error *ngIf="form.get('walkinPurchaserAadhaar')?.hasError('pattern')"
>Must be exactly 12 numeric digits</mat-error
>
</mat-form-field>
</div>
<mat-form-field appearance="outline" class="full-width">
<mat-label>Purchaser Address / Location *</mat-label>
<input
matInput
formControlName="walkinPurchaserAddress"
placeholder="e.g. 14, SECTOR 5, MAIN ROAD, MUMBAI"
style="text-transform: uppercase;"
/>
<mat-error *ngIf="form.get('walkinPurchaserAddress')?.hasError('required')"
>Address is required</mat-error
>
</mat-form-field>
}
<!-- Beneficiary Section -->
<div class="form-section-title">Beneficiary & Payment Details</div>
<mat-form-field appearance="outline" class="full-width">
<mat-label>Beneficiary Name (In Favour Of) *</mat-label>
<input
matInput
formControlName="beneficiaryName"
placeholder="e.g. UNIVERSITY OF MUMBAI"
maxlength="120"
style="text-transform: uppercase;"
/>
<mat-error *ngIf="form.get('beneficiaryName')?.hasError('required')"
>Beneficiary name is required</mat-error
>
<mat-error *ngIf="form.get('beneficiaryName')?.hasError('maxlength')">Max 120 characters</mat-error>
</mat-form-field>
<div class="form-row">
<mat-form-field appearance="outline" class="form-field-flex">
<mat-label>Payable At - Branch *</mat-label>
<input
matInput
formControlName="payableAtBranch"
placeholder="e.g. FORT BRANCH"
maxlength="150"
style="text-transform: uppercase;"
/>
<mat-error *ngIf="form.get('payableAtBranch')?.hasError('required')"
>Payable Branch is required</mat-error
>
</mat-form-field>
<mat-form-field appearance="outline" class="form-field-flex">
<mat-label>Payable At - City *</mat-label>
<input
matInput
formControlName="payableAtCity"
placeholder="e.g. MUMBAI"
maxlength="100"
style="text-transform: uppercase;"
/>
<mat-error *ngIf="form.get('payableAtCity')?.hasError('required')">Payable City is required</mat-error>
</mat-form-field>
</div>
</form>
</mat-dialog-content>
<mat-dialog-actions class="dd-dialog-actions" align="end">
<button mat-button type="button" (click)="onClose()">Cancel</button>
<button mat-flat-button color="primary" [disabled]="form.invalid" (click)="onSubmit()">
Submit for Manager Approval
</button>
</mat-dialog-actions>
</div>
`,
styles: [
`
.dd-dialog-shell {
display: flex;
flex-direction: column;
max-height: 90vh;
}
.dd-dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
background: #0f172a;
color: #ffffff;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.dd-dialog-header__title {
display: flex;
align-items: center;
gap: 12px;
}
.dd-dialog-header__title h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #ffffff;
}
.dd-dialog-header__title p {
margin: 2px 0 0 0;
font-size: 12px;
color: #94a3b8;
}
.dd-icon {
color: #38bdf8;
font-size: 28px;
width: 28px;
height: 28px;
}
.close-btn {
color: #94a3b8;
}
.close-btn:hover {
color: #ffffff;
}
.dd-dialog-body {
padding: 20px 24px;
overflow-y: auto;
}
.dd-summary-card {
display: flex;
align-items: center;
justify-content: space-between;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 14px 18px;
margin-bottom: 20px;
}
.summary-col {
display: flex;
flex-direction: column;
gap: 2px;
}
.summary-col.highlight-col {
background: #ecfdf5;
padding: 6px 12px;
border-radius: 6px;
border: 1px solid #a7f3d0;
}
.summary-divider {
width: 1px;
height: 36px;
background: #e2e8f0;
}
.summary-col .label {
font-size: 11px;
text-transform: uppercase;
color: #64748b;
font-weight: 600;
}
.dd-number-highlight {
font-size: 16px;
color: #0369a1;
font-family: monospace;
}
.summary-col .amount {
font-size: 15px;
color: #1e293b;
}
.summary-col .total-amount {
font-size: 16px;
color: #065f46;
}
.summary-col .sublabel {
font-size: 11px;
color: #94a3b8;
}
.form-section-title {
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
color: #475569;
margin: 16px 0 10px 0;
border-bottom: 1px solid #f1f5f9;
padding-bottom: 4px;
}
.form-section-title:first-child {
margin-top: 0;
}
.dd-form {
display: flex;
flex-direction: column;
gap: 8px;
}
.full-width {
width: 100%;
}
.form-row {
display: flex;
gap: 16px;
}
.form-field-flex {
flex: 1;
}
.dd-dialog-actions {
padding: 12px 24px;
border-top: 1px solid #e2e8f0;
}
/* The account about to be debited — read before the form, so a wrong one is caught here. */
.dd-funding-note {
display: flex;
gap: 10px;
align-items: flex-start;
margin: 14px 0 4px;
padding: 11px 14px;
background: #eff6ff;
border: 1px solid #bfdbfe;
border-left: 3px solid #1d4ed8;
border-radius: 6px;
font-size: 13px;
color: #334155;
}
.dd-funding-note mat-icon {
color: #1d4ed8;
flex-shrink: 0;
}
.dd-funding-sub {
margin-top: 3px;
font-size: 11.5px;
line-height: 1.5;
color: #64748b;
}
`
]
})
export class CashierIssueDdDialogComponent implements OnInit {
private fb = inject(FormBuilder);
private dialogRef = inject(MatDialogRef<CashierIssueDdDialogComponent>);
public data: CashierIssueDdDialogData = inject(MAT_DIALOG_DATA);
form!: FormGroup;
/** Whether the draft is funded by debiting a member's account rather than by cash at the counter. */
get isAccountDebit(): boolean {
return this.data.paymentMethod === 'ACCOUNT_DEBIT';
}
ngOnInit(): void {
// Walk-in identity is required for cash and rejected outright for an account debit — the server
// refuses a walk-in name alongside an account — so the controls are only built when they apply.
// Registering them unconditionally and merely hiding them would hold the form invalid on a
// required field the clerk cannot see.
this.form = this.fb.group(
this.isAccountDebit
? {
beneficiaryName: [
'',
[
Validators.required,
Validators.maxLength(120)
]
],
payableAtBranch: [
'',
[Validators.required]
],
payableAtCity: [
'',
[Validators.required]
]
}
: {
walkinPurchaserName: [
'',
[
Validators.required,
Validators.minLength(3)
]
],
walkinPurchaserAddress: [
'',
[Validators.required]
],
walkinPurchaserAadhaar: [
'',
[
Validators.required,
Validators.pattern(/^[0-9]{12}$/)
]
],
beneficiaryName: [
'',
[
Validators.required,
Validators.maxLength(120)
]
],
payableAtBranch: [
'',
[Validators.required]
],
payableAtCity: [
'',
[Validators.required]
]
}
);
}
onClose(): void {
this.dialogRef.close();
}
onSubmit(): void {
if (this.form.invalid) return;
const val = this.form.value;
const beneficiary = {
beneficiaryName: (val.beneficiaryName || '').trim().toUpperCase(),
payableAtBranch: (val.payableAtBranch || '').trim().toUpperCase(),
payableAtCity: (val.payableAtCity || '').trim().toUpperCase()
};
// Walk-in fields are left off entirely on an account debit rather than sent empty: the server
// rejects a purchaser name arriving alongside an account, and '' would be sent as one.
this.dialogRef.close(
this.isAccountDebit
? beneficiary
: {
walkinPurchaserName: (val.walkinPurchaserName || '').trim().toUpperCase(),
walkinPurchaserAddress: (val.walkinPurchaserAddress || '').trim().toUpperCase(),
walkinPurchaserAadhaar: (val.walkinPurchaserAadhaar || '').trim(),
...beneficiary
}
);
}
}
@@ -134,6 +134,23 @@
<option [ngValue]="27">027 · Industrial</option>
</select>
</div>
@if (row.typ === 'DD') {
<div class="ws-f ws-f--branch">
<label class="ws-f__lbl ws-f__lbl--accent" title="Sponsor Bank">SPONSOR BANK</label>
<select
class="ws-f__ctl ws-f__ctl--accent"
[(ngModel)]="row.sponsorBankId"
(click)="$event.stopPropagation()"
>
@if (sponsorBanks.length === 0) {
<option [ngValue]="null">-- Select Bank --</option>
}
@for (sb of sponsorBanks; track sb.id) {
<option [ngValue]="sb.id">{{ sb.name }}</option>
}
</select>
</div>
}
<div class="ws-f ws-f--paytype">
<label class="ws-f__lbl ws-f__lbl--accent" title="Transaction Type">TXN TP.</label>
<select
@@ -182,7 +199,22 @@
(input)="onAcNoInput(row, i, $any($event.target).value)"
(click)="$event.stopPropagation()"
(blur)="onAcNoBlur(row, i)"
placeholder="Account number"
[placeholder]="
isDdAccountDebitRow(row)
? 'Account to debit'
: row.typ === 'DD'
? ddGlMapping?.ddPayableGlCode || 'Auto GL'
: 'Account number'
"
[readonly]="row.typ === 'DD' && !isDdAccountDebitRow(row)"
[class.ws-f__ctl--readonly]="row.typ === 'DD' && !isDdAccountDebitRow(row)"
[title]="
isDdAccountDebitRow(row)
? 'Member account the draft is funded from'
: row.typ === 'DD'
? 'Configured Sponsor DD Payable GL (Auto-selected)'
: 'Account number'
"
/>
</div>
<div class="ws-f ws-f--ecsno">
@@ -340,7 +372,7 @@
</div>
<!-- ── Row 3: DD Charges & GST — only for TYPE=DD, TXN TP=Cash, AC.TP=GL ── -->
@if (isDdCashGlRow(row)) {
@if (isDdRow(row)) {
<div class="ws-group-row ws-group-row--mt">
<div class="ws-group ws-group--flex">
<span class="ws-caption">DD Charges &amp; GST — Admin Rate {{ ddGstPercentage }}%</span>
@@ -375,7 +407,7 @@
</div>
<div class="ws-f ws-f--amt">
<label class="ws-f__lbl" title="Amount + Charges + GST, rounded up to the next rupee">
TOTAL TO COLLECT
{{ isDdAccountDebitRow(row) ? 'TOTAL TO DEBIT' : 'TOTAL TO COLLECT' }}
</label>
<div class="ws-amt-wrap ws-amt-wrap--locked">
<span class="ws-amt-prefix">₹</span>
@@ -387,6 +419,23 @@
/>
</div>
</div>
<div class="ws-f" style="margin-left: auto; align-self: flex-end">
<button
type="button"
class="te-dd-issue-btn"
[disabled]="saving"
(click)="openIssueWalkinDd(row); $event.stopPropagation()"
[title]="
isDdAccountDebitRow(row)
? 'Auto-selects next available leaflet and opens the beneficiary form'
: 'Auto-selects next available leaflet and opens walk-in purchaser form'
"
>
<mat-icon style="font-size: 18px; width: 18px; height: 18px">post_add</mat-icon>
<span>Fill DD Details &amp; Send for Approval</span>
</button>
</div>
</div>
</div>
</div>
@@ -398,6 +447,18 @@
</div>
}
<!-- ── DD GL UNCONFIGURED WARNING BANNER ────────────────────────── -->
@if (selectedRow?.typ === 'DD' && !isDdGlMappingConfigured) {
<div class="ws-dd-unconfigured-banner">
<mat-icon>warning</mat-icon>
<div class="ws-dd-unconfigured-banner__text">
<strong>DD GL Mapping Required:</strong> Demand Draft GL accounts have not been configured yet. Please ask an
administrator to set up the mapping under <em>Admin &gt; Sponsor DD GST / GL Settings</em> before issuing
drafts.
</div>
</div>
}
<!-- ── APPROVED WITHDRAWAL BANNER (cashier disburse flow) ─────────── -->
@if (!isClerk && loadingWithdrawal) {
<div class="ws-withdrawal-banner ws-withdrawal-banner--loading">
@@ -1404,7 +1465,12 @@
<div class="ws-group__fields">
<div class="ws-f ws-f--type">
<label class="ws-f__lbl">TYPE</label>
<select class="ws-f__ctl" [(ngModel)]="row.typ">
<select
class="ws-f__ctl"
[(ngModel)]="row.typ"
(click)="$event.stopPropagation()"
(change)="onTypChange(row)"
>
@for (t of typOptions; track t.value) {
<option [value]="t.value">{{ t.label }}</option>
}
@@ -1412,18 +1478,36 @@
</div>
<div class="ws-f ws-f--branch">
<label class="ws-f__lbl">BRANCH</label>
<select class="ws-f__ctl" [(ngModel)]="row.br">
<select class="ws-f__ctl" [(ngModel)]="row.br" (click)="$event.stopPropagation()">
<option [ngValue]="1">001 · Main</option>
<option [ngValue]="2">002 · M.G. Road</option>
<option [ngValue]="14">014 · Station Rd</option>
<option [ngValue]="27">027 · Industrial</option>
</select>
</div>
@if (row.typ === 'DD') {
<div class="ws-f ws-f--branch">
<label class="ws-f__lbl ws-f__lbl--accent" title="Sponsor Bank">SPONSOR BANK</label>
<select
class="ws-f__ctl ws-f__ctl--accent"
[(ngModel)]="row.sponsorBankId"
(click)="$event.stopPropagation()"
>
@if (sponsorBanks.length === 0) {
<option [ngValue]="null">-- Select Bank --</option>
}
@for (sb of sponsorBanks; track sb.id) {
<option [ngValue]="sb.id">{{ sb.name }}</option>
}
</select>
</div>
}
<div class="ws-f ws-f--paytype">
<label class="ws-f__lbl ws-f__lbl--accent">TXN TP.</label>
<select
class="ws-f__ctl ws-f__ctl--accent"
[(ngModel)]="row.paymentTypeId"
(click)="$event.stopPropagation()"
[class.ws-f__ctl--req]="!row.paymentTypeId"
>
@for (pt of paymentTypesForRow(row); track pt.id) {
@@ -1436,6 +1520,7 @@
<select
class="ws-f__ctl"
[(ngModel)]="row.acTyp"
(click)="$event.stopPropagation()"
(change)="
row.acTyp === 'GL' &&
row.typ !== 'Disbursement' &&
@@ -1462,7 +1547,14 @@
[value]="row.acNo"
(input)="onAcNoInput(row, i, $any($event.target).value)"
(blur)="onAcNoBlur(row, i)"
placeholder="Account number"
[placeholder]="
row.typ === 'DD' ? ddGlMapping?.ddPayableGlCode || 'Auto GL' : 'Account number'
"
[readonly]="row.typ === 'DD'"
[class.ws-f__ctl--readonly]="row.typ === 'DD'"
[title]="
row.typ === 'DD' ? 'Configured Sponsor DD Payable GL (Auto-selected)' : 'Account number'
"
/>
</div>
<div class="ws-f ws-f--ecsno">
@@ -1591,14 +1683,19 @@
<div class="ws-group__fields">
<div class="ws-f ws-f--desc">
<label class="ws-f__lbl">DESCRIPTION</label>
<input class="ws-f__ctl" [(ngModel)]="row.description" placeholder="Narration / remarks" />
<input
class="ws-f__ctl"
[(ngModel)]="row.description"
(click)="$event.stopPropagation()"
placeholder="Narration / remarks"
/>
</div>
</div>
</div>
</div>
<!-- ── Row 3: DD Charges & GST — only for TYPE=DD, TXN TP=Cash, AC.TP=GL ── -->
@if (isDdCashGlRow(row)) {
@if (isDdRow(row)) {
<div class="ws-group-row ws-group-row--mt">
<div class="ws-group ws-group--flex">
<span class="ws-caption">DD Charges &amp; GST — Admin Rate {{ ddGstPercentage }}%</span>
@@ -1612,6 +1709,7 @@
type="number"
[value]="row.ddCharges ?? ''"
(input)="row.ddCharges = +$any($event.target).value"
(click)="$event.stopPropagation()"
placeholder="0.00"
/>
</div>
@@ -1632,7 +1730,7 @@
</div>
<div class="ws-f ws-f--amt">
<label class="ws-f__lbl" title="Amount + Charges + GST, rounded up to the next rupee">
TOTAL TO COLLECT
{{ isDdAccountDebitRow(row) ? 'TOTAL TO DEBIT' : 'TOTAL TO COLLECT' }}
</label>
<div class="ws-amt-wrap ws-amt-wrap--locked">
<span class="ws-amt-prefix">₹</span>
@@ -1644,6 +1742,23 @@
/>
</div>
</div>
<div class="ws-f" style="margin-left: auto; align-self: flex-end">
<button
type="button"
class="te-dd-issue-btn"
[disabled]="saving"
(click)="openIssueWalkinDd(row); $event.stopPropagation()"
[title]="
isDdAccountDebitRow(row)
? 'Auto-selects next available leaflet and opens the beneficiary form'
: 'Auto-selects next available leaflet and opens walk-in purchaser form'
"
>
<mat-icon style="font-size: 18px; width: 18px; height: 18px">post_add</mat-icon>
<span>Fill DD Details &amp; Send for Approval</span>
</button>
</div>
</div>
</div>
</div>
@@ -3384,3 +3384,67 @@ select.te-cell {
transform: rotate(360deg);
}
}
.ws-dd-unconfigured-banner {
display: flex;
align-items: center;
gap: 12px;
margin: 10px 16px;
padding: 12px 16px;
background: #fffbeb;
border: 1.5px solid #fcd34d;
border-radius: 8px;
color: #92400e;
font-size: 13px;
mat-icon {
color: #d97706;
font-size: 24px;
width: 24px;
height: 24px;
flex-shrink: 0;
}
&__text {
flex: 1;
line-height: 1.4;
strong {
color: #78350f;
}
}
}
.te-dd-issue-btn {
display: inline-flex;
align-items: center;
gap: 8px;
background: linear-gradient(135deg, #0284c7, #0ea5e9);
color: #fff;
border: none;
border-radius: 6px;
padding: 7px 16px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 2px 4px rgb(2 132 199 / 35%);
transition: all 0.15s ease-in-out;
letter-spacing: 0.2px;
&:hover:not(:disabled) {
background: linear-gradient(135deg, #0369a1, #0284c7);
box-shadow: 0 3px 8px rgb(2 132 199 / 50%);
transform: translateY(-1px);
}
&:active:not(:disabled) {
transform: translateY(0);
}
&:disabled {
background: #cbd5e1;
color: #64748b;
cursor: not-allowed;
box-shadow: none;
}
}
@@ -32,6 +32,7 @@ import {
} from '../../shared/customer-search-dialog/customer-search-dialog.component';
import { SponsorDDService } from '../../sponsor-dd/sponsor-dd.service';
import { OrganizationService } from '../../organization/organization.service';
import { CashierIssueDdDialogComponent } from './cashier-issue-dd-dialog.component';
interface TxnRow {
typ: string;
@@ -51,6 +52,7 @@ interface TxnRow {
routingCode: string;
/** DD issuance charges/commission — only used when TYPE=DD, TXN TP=Cash, AC.TP=GL */
ddCharges: number | null;
sponsorBankId?: number | null;
/** Only used when this row is AC.TP=SHR inside an Enter Bank Transfer to its linked savings
* account — governs the share redemption (see submitShareRedemptionTransfer()). */
resolutionNumber?: string;
@@ -1523,7 +1525,14 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
* the one that actually governs how the settlement credit posts (see submitShareRedemptionTransfer()).
*/
paymentTypesForRow(row: TxnRow): any[] {
const tellerFacing = this.paymentTypes.filter((pt: any) => !pt.isSystemDefined);
// Sponsor-bank payment types are excluded. They were added for the Demand Draft module, which
// picks its bank from m_sponsor_bank on the DD row itself, and they are not counter instruments
// — a clerk choosing "SBI" as the TXN TP. of a withdrawal says nothing about how the money
// moves. They are recognised by their description rather than by name so that adding another
// sponsor bank does not put it back on this list.
const tellerFacing = this.paymentTypes.filter(
(pt: any) => !pt.isSystemDefined && !/sponsor bank/i.test(pt.description ?? '')
);
if (!this.isClerk) {
// Cashier counter takes cash only — cheque/transfer/DD instruments go through their own
// clerk/back-office approval flows, never straight through the cash drawer.
@@ -1541,6 +1550,23 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
if (isShareSettlementCredit) {
return tellerFacing.filter((pt: any) => !pt.isCashPayment);
}
// A withdrawal slip is a cash instruction and nothing else: the clerk raises it, the cashier
// counts the notes out against the token. There is no non-cash way to settle one, so cash is
// the only choice offered rather than the default among several.
if (row.typ === 'Withdrawal Slip') {
return tellerFacing.filter((pt: any) => !!pt.isCashPayment);
}
// A clerk's Demand Draft is never funded by cash they take themselves. Cash for a draft is
// counted at the counter, so it comes either from the cashier's own DD screen or from an
// approved cashier deposit request the clerk points the draft at; the clerk's own route is to
// debit the member's account. Offering cash here let a draft be raised against notes no drawer
// had counted, which is exactly how the six cash DDs with no denomination breakdown arose.
if (row.typ === 'DD') {
return tellerFacing.filter((pt: any) => !pt.isCashPayment);
}
// Everything else can go either way, so both are offered and the clerk says which. A cheque or
// a bank transfer normally settles through the instrument, a disbursement or fund transfer
// between accounts — the type alone does not fix it to one or the other.
return tellerFacing;
}
@@ -1664,9 +1690,6 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
// wrong for crediting a GL amount into a savings/current account for any other reason (e.g.
// funding a newly-activated account's opening balance from a GL source).
{ value: 'Fund Transfer', label: 'Fund Transfer' },
// NOTE: the exact same TxnRow.typ string is used at every dispatch/default-setting site below —
// search for 'Disbursement' whenever this needs to change again, since 'Fund Transfer' always
// rides alongside it in every one of those checks rather than having its own separate case.
{ value: 'Enter Bank Transfer', label: 'Enter Bank Transfer' },
{ value: 'DD', label: 'DD (Demand Draft)' }
];
@@ -1692,6 +1715,9 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
return this.isClerk ? this.clerkTypOptions : this.cashierTypOptions;
}
sponsorBanks: any[] = [];
ddGlMapping: any = null;
// ── Denomination ──────────────────────────────────────────────────────────
notes: Denomination[] = [];
coins: Denomination[] = [];
@@ -1752,6 +1778,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
this.loadCashVaultGl();
this.loadDdGstPercentage();
this.loadSponsorBanks();
if (this.reviewMode) {
this.loadRequestIntoForm(this.reviewRequestId!, 'review');
@@ -1842,12 +1869,85 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
.getGlMappings()
.pipe(catchError(() => of([])))
.subscribe((mappings: any[]) => {
const mapping = mappings && mappings.length ? mappings[0] : null;
this.ddGstPercentage = Number(mapping?.gstPercentage ?? 18);
this.ddLimitAmount = mapping?.ddLimitAmount != null ? Number(mapping.ddLimitAmount) : null;
this.ddGlMapping = mappings && mappings.length ? mappings[0] : null;
this.ddGstPercentage = Number(this.ddGlMapping?.gstPercentage ?? 18);
this.ddLimitAmount = this.ddGlMapping?.ddLimitAmount != null ? Number(this.ddGlMapping.ddLimitAmount) : null;
if (this.ddGlMapping?.ddPayableGlAccountId) {
if (this.ddGlMapping.ddPayableGlCode) {
this.applyDdGlMappingToDdRows();
} else {
this.http
.get<any>(`/glaccounts/${this.ddGlMapping.ddPayableGlAccountId}`)
.pipe(catchError(() => of(null)))
.subscribe((glAcc: any) => {
if (glAcc) {
this.ddGlMapping.ddPayableGlCode = glAcc.glCode || String(glAcc.id);
this.ddGlMapping.ddPayableGlAccountName = glAcc.name;
}
this.applyDdGlMappingToDdRows();
});
}
}
});
}
applyDdGlMappingToRow(row: TxnRow): void {
if (row.typ !== 'DD' || !this.ddGlMapping) return;
// Leaves an account-funded DD alone: its A/C NO. is the member's account the clerk typed, and
// overwriting it with the DD Payable GL would silently turn the row back into a cash draft.
if (this.isDdAccountDebitRow(row)) return;
row.acTyp = 'GL';
row.dbCr = 'Cr';
if (this.ddGlMapping.ddPayableGlCode) {
row.acNo = this.ddGlMapping.ddPayableGlCode;
this.selectedRow = row;
this.loadAccountDetails(row.acNo, 'GL');
return;
}
if (this.ddGlMapping.ddPayableGlAccountId) {
this.http
.get<any>(`/glaccounts/${this.ddGlMapping.ddPayableGlAccountId}`)
.pipe(catchError(() => of(null)))
.subscribe((glAcc: any) => {
if (glAcc) {
this.ddGlMapping.ddPayableGlCode = glAcc.glCode || String(glAcc.id);
this.ddGlMapping.ddPayableGlAccountName = glAcc.name;
row.acNo = this.ddGlMapping.ddPayableGlCode;
this.selectedRow = row;
this.loadAccountDetails(row.acNo, 'GL');
}
});
}
}
private applyDdGlMappingToDdRows(): void {
for (const row of this.rows) {
if (row.typ === 'DD') {
this.applyDdGlMappingToRow(row);
}
}
}
private loadSponsorBanks(): void {
this.sponsorDDService
.getSponsorBanks()
.pipe(catchError(() => of([])))
.subscribe((banks: any[]) => {
this.sponsorBanks = banks || [];
for (const row of this.rows) {
if (row.typ === 'DD' && !row.sponsorBankId && this.sponsorBanks.length > 0) {
row.sponsorBankId = this.sponsorBanks[0].id;
}
}
});
}
get isDdGlMappingConfigured(): boolean {
return !!(this.ddGlMapping && this.ddGlMapping.ddPayableGlAccountId && this.ddGlMapping.cashGlAccountId);
}
private loadDayStatus(): void {
this.cashierService
.getCashierDayStatus()
@@ -2093,9 +2193,12 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
row.chequeNo = '';
row.cheqDate = '';
break;
// A cheque clears through the bank rather than the drawer, so it opens on the transfer
// instrument. Cash is still on its dropdown — a cheque can be cashed over the counter — but
// it is the exception, not what the field should start on.
case 'Cheque':
row.dbCr = 'Dr';
row.paymentTypeId = this.findCashPaymentTypeId();
row.paymentTypeId = this.findTransferPaymentTypeId();
break;
case 'Disbursement':
case 'Fund Transfer':
@@ -2121,16 +2224,25 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
row.chequeNo = '';
row.cheqDate = '';
break;
// A DD is bought with cash: the customer hands over Face Value + Charges + GST, so it is money
// coming IN (Cr) against the GL the draft is drawn on. All three of Cr, a cash payment type and
// AC.TP = GL are what isDdCashGlRow() looks for -- without them the DD Charges & GST panel never
// appears and the row would post as an ordinary entry with the charges silently dropped.
// A DD is bought with cash: the purchaser hands over Face Value + Charges + GST, so it is
// money coming IN (Cr) against the GL the draft is drawn on. All three of Cr, a cash payment
// type and AC.TP = GL are what isDdCashGlRow() looks for -- without them the DD Charges & GST
// panel never appears and the row would post as an ordinary entry with the charges silently
// dropped.
//
// A clerk funds a draft through an account or an instrument instead, and their TXN TP. list no
// longer offers cash, so defaulting to it would open the field on a value it cannot show.
// isDdCashGlRow() still holds for them through AC.TP = GL.
case 'DD':
row.dbCr = 'Cr';
row.paymentTypeId = this.findCashPaymentTypeId();
row.acTyp = 'GL';
row.paymentTypeId = this.isClerk ? this.findTransferPaymentTypeId() : this.findCashPaymentTypeId();
row.chequeNo = '';
row.cheqDate = '';
if (this.sponsorBanks.length > 0 && !row.sponsorBankId) {
row.sponsorBankId = this.sponsorBanks[0].id;
}
this.applyDdGlMappingToRow(row);
break;
}
}
@@ -2145,10 +2257,33 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
return pt?.id ?? null;
}
/** TYPE=DD + TXN TP=Cash + AC.TP=GL — the only combination that collects DD charges + GST from the customer. */
/** TYPE=DD + TXN TP=Cash */
isDdCashGlRow(row: TxnRow): boolean {
if (this.isDdAccountDebitRow(row)) return false;
const pt = this.paymentTypes.find((p: any) => p.id === row.paymentTypeId);
return row.typ === 'DD' && !!pt?.isCashPayment && row.acTyp === 'GL';
return row.typ === 'DD' && (row.acTyp === 'GL' || !row.acTyp || !!pt?.isCashPayment);
}
/**
* TYPE=DD funded by debiting a member's savings/current account rather than by cash.
*
* Clerk-only. A cashier's counter takes cash and nothing else, so offering them an account debit
* would let a draft be raised against an account with no cash to reconcile it against — and the
* clerk flow is the one that carries a maker-checker step over the member's balance.
*/
isDdAccountDebitRow(row: TxnRow): boolean {
return this.isClerk && row.typ === 'DD' && (row.acTyp === 'SD' || row.acTyp === 'CA' || row.acTyp === 'CB');
}
/**
* Any DD row the charges/GST panel applies to.
*
* Charges and GST come from the admin's DD configuration and are collected whichever way the
* draft is paid for — the difference is only where the money comes from, so the panel is the same
* for both and the label on the total changes rather than the arithmetic.
*/
isDdRow(row: TxnRow): boolean {
return this.isDdCashGlRow(row) || this.isDdAccountDebitRow(row);
}
/** GST on DD charges only (never on the DD face value), at the admin-configured rate. */
@@ -2179,6 +2314,258 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
return !!this.selectedRow && Math.abs(this.denominationTotal - this.ddTargetTotal) < 0.005;
}
branchNameFor(branchId: number): string {
const map: Record<number, string> = {
1: '001 · Main',
2: '002 · M.G. Road',
14: '014 · Station Rd',
27: '027 · Industrial'
};
return map[branchId] || `Branch ${branchId}`;
}
/**
* Cashier Walk-in DD creation flow:
* 1. Validates one-time persistent GL configuration
* 2. Auto-selects next available leaflet
* 3. Opens dialog to capture purchaser/beneficiary details
* 4. Submits issue request directly with physical denominations for manager approval
*/
openIssueWalkinDd(row: TxnRow): void {
if (!this.isDdGlMappingConfigured) {
this.showAlert('error', 'DD GL Mapping Not Configured', [
'DD GL mapping is not configured.',
'Please ask an administrator to set up the GL accounts in Admin > Sponsor DD GST / GL Settings before creating Demand Drafts.'
]);
return;
}
const faceValue = Number(row.amount);
if (!faceValue || faceValue <= 0) {
this.showAlert('error', 'Invalid Amount', ['Please enter the DD Face Value amount before proceeding.']);
return;
}
const chargesAmount = Number(row.ddCharges) || 0;
const gstAmount = this.ddGstAmountFor(row);
const totalCashPayable = this.ddTotalPayableFor(row);
const accountDebit = this.isDdAccountDebitRow(row);
// An account-funded draft needs the account resolved to a Fineract id before it can be sent:
// the server takes a savings/current account id and a client id, not the typed number. Both are
// filled in by the A/C NO. lookup, so an unresolved account means the number was never found.
let fundingAccountId: number | null = null;
let fundingClientId: number | null = null;
if (accountDebit) {
const acNo = (row.acNo || '').trim();
if (!acNo) {
this.showAlert('error', 'Account Required', [
'Enter the account number the Demand Draft is funded from.'
]);
return;
}
fundingAccountId = this.rowAccountIds[acNo] ?? null;
fundingClientId = this.rowClientIds[acNo] ?? null;
if (!fundingAccountId || !fundingClientId) {
this.showAlert('error', 'Account Not Found', [
`Account ${acNo} could not be resolved.`,
'Check the account number and wait for its details to load before raising the draft.'
]);
return;
}
}
// Only a cash draft has notes to reconcile. An account debit takes no cash at the counter, so a
// denomination count left over from an earlier row must not be checked against it.
if (!accountDebit && this.denominationTotal > 0 && Math.abs(this.denominationTotal - totalCashPayable) > 0.005) {
this.showAlert('error', 'Denomination Mismatch', [
`Cash denomination counted (₹${this.denominationTotal}) does not match total payable cash (₹${totalCashPayable}).`,
'Please verify and match the physical cash denomination count.'
]);
return;
}
this.saving = true;
this.sponsorDDService.getNextAvailableLeaflet(row.br, row.sponsorBankId ?? undefined).subscribe({
next: (leaflet) => {
this.saving = false;
if (!leaflet) {
this.showAlert('error', 'No Available Leaflets', [
'No available Demand Draft leaflets found for this branch and sponsor bank.',
'Please register a DD inventory series first.'
]);
return;
}
const selectedBank = this.sponsorBanks.find((b: any) => b.id === row.sponsorBankId);
const branchName = this.branchNameFor(row.br) || 'Branch ' + row.br;
const dialogRef = this.dialog.open(CashierIssueDdDialogComponent, {
width: '600px',
disableClose: true,
data: {
leaflet,
branchName,
sponsorBankName: selectedBank?.name || 'Sponsor Bank',
faceValue,
chargesAmount,
gstPercentage: this.ddGstPercentage,
gstAmount,
totalCashPayable,
paymentMethod: accountDebit ? ('ACCOUNT_DEBIT' as const) : ('CASH' as const),
accountNo: accountDebit ? row.acNo : undefined,
accountHolderName: accountDebit ? this.rowAccountNames[(row.acNo || '').trim()] : undefined
}
});
dialogRef.afterClosed().subscribe((result) => {
if (!result) return;
if (accountDebit) {
this.submitAccountDebitDd(row, leaflet, result, fundingAccountId!, fundingClientId!);
} else {
this.submitWalkinCashDd(row, leaflet, result);
}
});
},
error: (err) => {
this.saving = false;
this.showAlert('error', 'Error Auto-Selecting DD Leaflet', [err.error?.message || err.message]);
}
});
}
/**
* Raises a Demand Draft funded by debiting a member's savings or current account.
*
* Clerk-only, and a maker-checker request like the cash flow: nothing is debited here. The member's
* account is only touched when a checker approves, which is also when the balance is re-checked —
* so a draft raised against a sufficient balance can still be refused if the balance has since
* gone. No denominations are sent, because no cash is taken.
*
* The account goes in savingsAccountId or currentAccountId, never both: the server insists on
* exactly one, since which table the account lives in decides which control GL is debited.
*/
private submitAccountDebitDd(
row: TxnRow,
leaflet: any,
beneficiaryData: any,
fundingAccountId: number,
fundingClientId: number
): void {
const d = new Date();
const issuedDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
const isCurrent = row.acTyp === 'CA' || row.acTyp === 'CB';
const payload = {
clientId: fundingClientId,
beneficiaryName: beneficiaryData.beneficiaryName,
payableAtBranch: beneficiaryData.payableAtBranch,
payableAtCity: beneficiaryData.payableAtCity,
faceValue: Number(row.amount),
chargesAmount: Number(row.ddCharges) || 0,
issuingBranchId: row.br,
issuedDate,
paymentMethod: 'ACCOUNT_DEBIT' as const,
savingsAccountId: isCurrent ? undefined : fundingAccountId,
currentAccountId: isCurrent ? fundingAccountId : undefined
};
this.saving = true;
this.sponsorDDService.submitIssueRequest(leaflet.id, payload).subscribe({
next: () => {
this.saving = false;
this.showAlert('success', 'DD Issuance Request Submitted', [
`Demand Draft request for DD No. ${leaflet.ddNumber} (Face Value: ₹${row.amount}) submitted successfully!`,
`₹${this.ddTotalPayableFor(row)} will be debited from ${row.acNo} on approval.`,
'Sent to Manager / Head Cashier for maker-checker approval.'
]);
row.amount = null;
row.ddCharges = null;
row.description = '';
},
error: (err) => {
this.saving = false;
this.showAlert('error', 'DD Submission Failed', [err.error?.message || err.message]);
}
});
}
private submitWalkinCashDd(row: TxnRow, leaflet: any, purchaserData: any): void {
// Read through countFor, which knows the map is keyed by kind:value. Indexing by d.id looked
// right but matched nothing, so every cash DD went in with no breakdown at all and its money
// vanished from the closing report -- the notes were counted on screen and then dropped.
const denominationMap: { [key: string]: number } = {};
for (const d of [
...this.notes,
...this.coins
]) {
const count = this.countFor(d);
if (count > 0) {
denominationMap[this.denomKey(d)] = count;
}
}
// The breakdown is the only record of which notes were taken, and the server insists it add up
// to the cash collected. Checked here so the cashier is told which figure is wrong while the
// counts are still on screen, rather than getting a rejection after the fact.
//
// ddTotalPayableFor rounds up to the whole rupee actually collected, which is what the notes
// can add to; the exact figure with paise is accepted too, for a draft that lands on a rupee.
const totalCollected = Number(this.ddTotalPayableFor(row));
const exactPayable = Number(row.amount || 0) + (Number(row.ddCharges) || 0) + this.ddGstAmountFor(row);
const countedTotal = Object.entries(denominationMap).reduce(
(sum, [
key,
count
]) => sum + Number(key.split(':')[1]) * count,
0
);
if (countedTotal !== totalCollected && countedTotal !== exactPayable) {
this.showAlert('error', 'Denomination Entry Does Not Match', [
countedTotal === 0
? 'Enter the notes and coins taken from the purchaser before submitting.'
: `The notes and coins entered come to ₹${countedTotal.toFixed(2)}, but ₹${totalCollected.toFixed(2)} is collected (face value + charges + GST).`,
'The breakdown must equal the cash taken — it is what the closing report and the drawer are checked against.'
]);
return;
}
const d = new Date();
const issuedDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
const payload = {
walkinPurchaserName: purchaserData.walkinPurchaserName,
walkinPurchaserAddress: purchaserData.walkinPurchaserAddress,
walkinPurchaserAadhaar: purchaserData.walkinPurchaserAadhaar,
beneficiaryName: purchaserData.beneficiaryName,
payableAtBranch: purchaserData.payableAtBranch,
payableAtCity: purchaserData.payableAtCity,
faceValue: Number(row.amount),
chargesAmount: Number(row.ddCharges) || 0,
issuingBranchId: row.br,
issuedDate,
paymentMethod: 'CASH' as const,
denominationData: Object.keys(denominationMap).length > 0 ? JSON.stringify(denominationMap) : undefined
};
this.saving = true;
this.sponsorDDService.submitIssueRequest(leaflet.id, payload).subscribe({
next: () => {
this.saving = false;
this.showAlert('success', 'DD Issuance Request Submitted', [
`Demand Draft request for DD No. ${leaflet.ddNumber} (Face Value: ₹${row.amount}, Total Cash: ₹${this.ddTotalPayableFor(row)}) submitted successfully!`,
'Sent to Manager / Head Cashier for maker-checker approval.'
]);
row.amount = null;
row.ddCharges = null;
row.description = '';
this.clearDenominations();
},
error: (err) => {
this.saving = false;
this.showAlert('error', 'DD Submission Failed', [err.error?.message || err.message]);
}
});
}
/**
* Submits a clerk double-entry request (maker-checker, PENDING until manager approves).
* Disbursement: 1 DR [GL] source → N CR savings/current/GL destinations.
@@ -2811,16 +3198,23 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
const f = cfg.matchField;
// Exact match first, then suffix/partial match. Some account types (e.g. Share — its
// list endpoint only accepts offset/limit, no accountNo filter at all) always return the
// FULL unfiltered list rather than a server-side-narrowed one, so a partial match can't be
// gated on "exactly one row came back" — it never would be for those types.
const exactMatch = allItems.find((x: any) => x[f] === acNo);
const exactMatch = allItems.find((x: any) => String(x[f] ?? '').trim() === String(acNo).trim());
const partialMatch = !exactMatch
? (allItems.find((x: any) => x[f]?.endsWith(acNo)) ?? allItems.find((x: any) => x[f]?.includes(acNo)))
? (allItems.find((x: any) => String(x[f] ?? '').endsWith(acNo)) ??
allItems.find((x: any) => String(x[f] ?? '').includes(acNo)))
: null;
const match = exactMatch ?? partialMatch ?? null;
const idMatch =
!exactMatch && !partialMatch && acTyp === 'GL'
? allItems.find((x: any) => String(x.id) === String(acNo))
: null;
const match = exactMatch ?? partialMatch ?? idMatch ?? null;
if (match?.id) {
const matchedGlCode = match[f] ?? acNo;
if (this.selectedRow && this.selectedRow.typ === 'DD' && match.glCode) {
this.selectedRow.acNo = match.glCode;
}
const clientIdFromSearch = acTyp !== 'GL' && match.clientId != null ? String(match.clientId) : null;
this.fetchAllDetails(match.id, match[f] ?? acNo, cfg, token, acTyp, clientIdFromSearch);
this.fetchAllDetails(match.id, matchedGlCode, cfg, token, acTyp, clientIdFromSearch);
return;
}
this.loadingAccount = false;
@@ -3048,7 +3442,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
return false;
}
const returnedId = acc.glCode ?? acc.accountNo;
if (returnedId && returnedId !== acNo) {
if (returnedId && returnedId !== acNo && String(acc.id) !== acNo) {
this.loadingAccount = false;
this.accountError = 'Account mismatch. Please re-enter account number and try again.';
return false;
@@ -3294,7 +3688,6 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
{ label: 'NEFT Facility', active: !!f.neftFacility },
{ label: 'RTGS Facility', active: !!f.rtgsFacility },
{ label: 'Inter-Bank Cap', active: !!f.interBankTotalCapFacility },
{ label: 'Withdrawal', active: !!f.withdrawalFacility },
{ label: 'Deposit (KYC)', active: !!f.depositFacilityKyc },
{ label: 'Virtual Debit Card', active: !!f.virtualDebitCard },
{ label: 'OD', active: !!this.accountDetails?.allowOverdraft }
@@ -4579,6 +4972,10 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
confirmSubmit(): void {
this.showConfirmDialog = false;
const row = this.selectedRow!;
if (row.typ === 'DD') {
this.openIssueWalkinDd(row);
return;
}
const amount = this.isDdCashGlRow(row) ? this.ddTotalPayableFor(row) : (row.amount ?? this.denominationTotal);
const isDeposit = row.dbCr === 'Cr';
const command = isDeposit ? 'deposit' : 'withdrawal';