fix: update sponsor DD approvals, cashier transaction entry, FIU and dividend declaration
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, FormArray, 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';
|
||||
@@ -35,6 +35,35 @@ export interface CashierIssueDdDialogData {
|
||||
accountNo?: string;
|
||||
/** The account holder, for the same reason. */
|
||||
accountHolderName?: string;
|
||||
/**
|
||||
* How many drafts this slip raises. Absent or 1 is the ordinary single draft and the dialog is
|
||||
* unchanged; 2 or more turns it into a slip, asking for a face value, charge and beneficiary per
|
||||
* draft and reporting the combined total the account will be debited for.
|
||||
*/
|
||||
ddCount?: number;
|
||||
/**
|
||||
* Whether the funding account is a GL account rather than a member's. Only affects wording: a GL
|
||||
* account has no passbook, so it is not promised one.
|
||||
*/
|
||||
glFunded?: boolean;
|
||||
/**
|
||||
* The leaflets provisionally shown against each line of a slip.
|
||||
*
|
||||
* Only ever indicative. The server allocates the real numbers inside the locked transaction that
|
||||
* submits the slip, so what is shown here is the next-available run at the moment the dialog was
|
||||
* opened — accurate unless another clerk submits first, which is precisely why the numbers cannot
|
||||
* be promised until submit returns them.
|
||||
*/
|
||||
previewDdNumbers?: string[];
|
||||
}
|
||||
|
||||
/** One captured draft on a multi-DD slip. */
|
||||
export interface DdSlipLineResult {
|
||||
beneficiaryName: string;
|
||||
payableAtBranch: string;
|
||||
payableAtCity: string;
|
||||
faceValue: number;
|
||||
chargesAmount: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -56,10 +85,20 @@ export interface CashierIssueDdDialogData {
|
||||
<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>
|
||||
<h2>
|
||||
@if (isSlip) {
|
||||
Multi-DD Slip · {{ lineCount }} Demand Drafts
|
||||
} @else {
|
||||
{{ 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 & beneficiary details' }}
|
||||
@if (isSlip) {
|
||||
One debit funds all {{ lineCount }} drafts · Enter each draft's beneficiary and amount
|
||||
} @else {
|
||||
Next available DD leaflet auto-selected ·
|
||||
{{ isAccountDebit ? 'Enter beneficiary details' : 'Enter purchaser & beneficiary details' }}
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,41 +108,91 @@ export interface CashierIssueDdDialogData {
|
||||
</div>
|
||||
|
||||
<mat-dialog-content class="dd-dialog-body">
|
||||
<!-- Draft & Financials Summary Card -->
|
||||
<div class="dd-summary-card">
|
||||
<!-- The number the customer will actually see printed on the instrument. "Auto-Selected
|
||||
<!--
|
||||
Slip running total.
|
||||
|
||||
A slip's figures are not known until every line is typed — different drafts carry different
|
||||
face values and different charges — so the total the account will be debited for is built
|
||||
up here as the clerk works, rather than shown as a fixed figure up front. Answering "what
|
||||
is this going to cost my member" without it would mean adding the lines by hand.
|
||||
-->
|
||||
@if (isSlip) {
|
||||
<div class="dd-summary-card">
|
||||
<div class="summary-col">
|
||||
<span class="label">Drafts On This Slip</span>
|
||||
<strong class="dd-number-highlight">{{ lineCount }}</strong>
|
||||
<span class="sublabel">{{ data.sponsorBankName || 'Sponsor Bank' }} · {{ data.branchName }}</span>
|
||||
</div>
|
||||
<div class="summary-divider"></div>
|
||||
<div class="summary-col">
|
||||
<span class="label">Total Face Value</span>
|
||||
<strong class="amount">₹{{ slipFaceValue | number: '1.2-2' }}</strong>
|
||||
<span class="sublabel">+ Charges ₹{{ slipCharges | 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">₹{{ slipGst | 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">Total To Debit</span>
|
||||
<strong class="total-amount">₹{{ slipTotal | number: '1.2-2' }}</strong>
|
||||
<span class="sublabel">{{ data.accountNo || 'Account' }} · one debit</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Said plainly because the numbers on screen are the one thing in this dialog the clerk
|
||||
cannot rely on. They are the next-available run as it stood when the dialog opened; the
|
||||
binding allocation happens inside the submit transaction, so a slip filed after another
|
||||
clerk's gets the following numbers instead.
|
||||
-->
|
||||
<div class="dd-preview-note">
|
||||
<mat-icon>info</mat-icon>
|
||||
<div>
|
||||
DD numbers below are <strong>provisional</strong>. The final numbers are allocated when the slip is
|
||||
submitted and will be confirmed on the receipt.
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Draft & Financials Summary Card -->
|
||||
<div class="dd-summary-card">
|
||||
<!-- The number the customer will actually see printed on the instrument. "Auto-Selected
|
||||
Leaflet" named the stock item rather than the draft, so it was not obvious that this
|
||||
is the DD number being issued. -->
|
||||
<div class="summary-col">
|
||||
<span class="label">DD Number Being Issued</span>
|
||||
<strong class="dd-number-highlight">{{ data.leaflet.ddNumber }}</strong>
|
||||
<span class="sublabel">{{ data.sponsorBankName || 'Sponsor Bank' }} · {{ data.branchName }}</span>
|
||||
<div class="summary-col">
|
||||
<span class="label">DD Number Being Issued</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>
|
||||
<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.
|
||||
@@ -121,123 +210,237 @@ export interface CashierIssueDdDialogData {
|
||||
· {{ 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.
|
||||
@if (isSlip) {
|
||||
₹{{ slipTotal | number: '1.2-2' }} will be debited from this account as a
|
||||
<strong>single transaction</strong> covering all {{ lineCount }} drafts, when a checker approves the
|
||||
slip —
|
||||
<!-- A GL account has no passbook, so the reassurance a member gets is phrased for
|
||||
the ledger instead of promising a statement line that does not exist. -->
|
||||
@if (isGlFunded) {
|
||||
one journal entry against this GL account, not {{ lineCount }}.
|
||||
} @else {
|
||||
the member's passbook shows one entry, not {{ lineCount }}.
|
||||
}
|
||||
} @else {
|
||||
₹{{ 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">
|
||||
<!--
|
||||
<!--
|
||||
A slip's drafts, one card each.
|
||||
|
||||
Face value and charge sit on the line rather than on the slip because that is how the bank
|
||||
actually uses this: three drafts to three institutions are three different amounts, and the
|
||||
charge is levied per draft. The server stores and posts them per line for the same reason.
|
||||
-->
|
||||
@if (isSlip) {
|
||||
<form [formGroup]="slipForm" class="dd-form">
|
||||
<div formArrayName="lines">
|
||||
@for (lineCtrl of slipLines.controls; track $index) {
|
||||
<div class="dd-line-card" [formGroupName]="$index">
|
||||
<div class="dd-line-head">
|
||||
<span class="dd-line-badge">DD {{ $index + 1 }} of {{ lineCount }}</span>
|
||||
@if (previewNumberAt($index)) {
|
||||
<span class="dd-line-number">No. {{ previewNumberAt($index) }} <em>(provisional)</em></span>
|
||||
}
|
||||
<span class="dd-line-total"> Debit ₹{{ lineTotalAt($index) | number: '1.2-2' }} </span>
|
||||
</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="lineCtrl.get('beneficiaryName')?.hasError('required')"
|
||||
>Beneficiary name is required</mat-error
|
||||
>
|
||||
<mat-error *ngIf="lineCtrl.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="lineCtrl.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="lineCtrl.get('payableAtCity')?.hasError('required')"
|
||||
>Payable City is required</mat-error
|
||||
>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="form-field-flex">
|
||||
<mat-label>Face Value *</mat-label>
|
||||
<input matInput type="number" formControlName="faceValue" placeholder="0.00" />
|
||||
<span matPrefix>₹ </span>
|
||||
<mat-error *ngIf="lineCtrl.get('faceValue')?.hasError('required')"
|
||||
>Face value is required</mat-error
|
||||
>
|
||||
<mat-error *ngIf="lineCtrl.get('faceValue')?.hasError('min')"
|
||||
>Face value must be greater than zero</mat-error
|
||||
>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="form-field-flex">
|
||||
<mat-label>Charges</mat-label>
|
||||
<input matInput type="number" formControlName="chargesAmount" placeholder="0.00" />
|
||||
<span matPrefix>₹ </span>
|
||||
<mat-hint>GST ₹{{ lineGstAt($index) | number: '1.2-2' }} at {{ data.gstPercentage }}%</mat-hint>
|
||||
<mat-error *ngIf="lineCtrl.get('chargesAmount')?.hasError('min')"
|
||||
>Charges cannot be negative</mat-error
|
||||
>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
} @else {
|
||||
<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>
|
||||
@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>Walk-in Purchaser Name *</mat-label>
|
||||
<mat-label>Payable At - Branch *</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="walkinPurchaserName"
|
||||
placeholder="e.g. RAMESH CHANDRA"
|
||||
formControlName="payableAtBranch"
|
||||
placeholder="e.g. FORT BRANCH"
|
||||
maxlength="150"
|
||||
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-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>12-Digit Aadhaar Number *</mat-label>
|
||||
<mat-label>Payable At - City *</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="walkinPurchaserAadhaar"
|
||||
placeholder="12-digit Aadhaar"
|
||||
maxlength="12"
|
||||
formControlName="payableAtCity"
|
||||
placeholder="e.g. MUMBAI"
|
||||
maxlength="100"
|
||||
style="text-transform: uppercase;"
|
||||
/>
|
||||
<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-error *ngIf="form.get('payableAtCity')?.hasError('required')">Payable City is required</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>
|
||||
</form>
|
||||
}
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="dd-dialog-actions" align="end">
|
||||
@if (isSlip) {
|
||||
<span class="dd-actions-total">
|
||||
{{ lineCount }} drafts · <strong>₹{{ slipTotal | number: '1.2-2' }}</strong> to debit
|
||||
</span>
|
||||
}
|
||||
<button mat-button type="button" (click)="onClose()">Cancel</button>
|
||||
<button mat-flat-button color="primary" [disabled]="form.invalid" (click)="onSubmit()">
|
||||
Save DD Details
|
||||
<button mat-flat-button color="primary" [disabled]="activeForm.invalid" (click)="onSubmit()">
|
||||
{{ isSlip ? 'Save Slip Details' : 'Save DD Details' }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
@@ -395,6 +598,81 @@ export interface CashierIssueDdDialogData {
|
||||
line-height: 1.5;
|
||||
color: #64748b;
|
||||
}
|
||||
/* One card per draft on a slip — the boundary is what stops a clerk typing line 2's
|
||||
beneficiary against line 1's amount. */
|
||||
.dd-line-card {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-left: 3px solid #0369a1;
|
||||
border-radius: 8px;
|
||||
padding: 14px 16px 4px;
|
||||
margin-bottom: 14px;
|
||||
background: #fcfdff;
|
||||
}
|
||||
.dd-line-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.dd-line-badge {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
color: #ffffff;
|
||||
background: #0369a1;
|
||||
padding: 3px 9px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
.dd-line-number {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
.dd-line-number em {
|
||||
font-style: normal;
|
||||
color: #94a3b8;
|
||||
font-size: 11px;
|
||||
}
|
||||
.dd-line-total {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #065f46;
|
||||
background: #ecfdf5;
|
||||
border: 1px solid #a7f3d0;
|
||||
padding: 3px 10px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.dd-preview-note {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
margin: 0 0 16px;
|
||||
padding: 9px 12px;
|
||||
background: #fffbeb;
|
||||
border: 1px solid #fde68a;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #78350f;
|
||||
}
|
||||
.dd-preview-note mat-icon {
|
||||
color: #b45309;
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dd-actions-total {
|
||||
margin-right: auto;
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
}
|
||||
.dd-actions-total strong {
|
||||
color: #065f46;
|
||||
}
|
||||
`
|
||||
]
|
||||
})
|
||||
@@ -404,13 +682,127 @@ export class CashierIssueDdDialogComponent implements OnInit {
|
||||
public data: CashierIssueDdDialogData = inject(MAT_DIALOG_DATA);
|
||||
|
||||
form!: FormGroup;
|
||||
/** Built only in slip mode; `form` stays untouched so the single-draft path is unchanged. */
|
||||
slipForm!: 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';
|
||||
}
|
||||
|
||||
/** How many drafts this dialog is capturing — 1 unless the clerk asked for a slip. */
|
||||
get lineCount(): number {
|
||||
const n = Number(this.data.ddCount) || 1;
|
||||
return n > 1 ? n : 1;
|
||||
}
|
||||
|
||||
/** Whether this dialog is capturing a multi-DD slip rather than a single draft. */
|
||||
get isSlip(): boolean {
|
||||
return this.lineCount > 1;
|
||||
}
|
||||
|
||||
/** Whether the debit lands on a GL account rather than a member's — wording only. */
|
||||
get isGlFunded(): boolean {
|
||||
return !!this.data.glFunded;
|
||||
}
|
||||
|
||||
/** The form the Save button validates against — whichever of the two was built. */
|
||||
get activeForm(): FormGroup {
|
||||
return this.isSlip ? this.slipForm : this.form;
|
||||
}
|
||||
|
||||
get slipLines(): FormArray {
|
||||
return this.slipForm.get('lines') as FormArray;
|
||||
}
|
||||
|
||||
/** The provisional leaflet number shown against a line, if the caller supplied a run of them. */
|
||||
previewNumberAt(index: number): string | null {
|
||||
return this.data.previewDdNumbers?.[index] ?? null;
|
||||
}
|
||||
|
||||
private lineValueAt(index: number, field: string): number {
|
||||
return Number(this.slipLines.at(index)?.get(field)?.value) || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* GST on one line's charge.
|
||||
*
|
||||
* Rounded per line, matching the server, which computes and stores GST against each draft rather
|
||||
* than once for the slip. Summing rounded lines and rounding a summed charge can differ by a
|
||||
* paisa, and it is the per-line figure that ends up on the draft.
|
||||
*/
|
||||
lineGstAt(index: number): number {
|
||||
const charges = this.lineValueAt(index, 'chargesAmount');
|
||||
if (charges <= 0) return 0;
|
||||
return Math.round(charges * (Number(this.data.gstPercentage) || 0)) / 100;
|
||||
}
|
||||
|
||||
/** What one line adds to the debit — its face value, its charge and the GST on that charge. */
|
||||
lineTotalAt(index: number): number {
|
||||
return this.lineValueAt(index, 'faceValue') + this.lineValueAt(index, 'chargesAmount') + this.lineGstAt(index);
|
||||
}
|
||||
|
||||
get slipFaceValue(): number {
|
||||
return this.slipLines.controls.reduce((sum, _c, i) => sum + this.lineValueAt(i, 'faceValue'), 0);
|
||||
}
|
||||
|
||||
get slipCharges(): number {
|
||||
return this.slipLines.controls.reduce((sum, _c, i) => sum + this.lineValueAt(i, 'chargesAmount'), 0);
|
||||
}
|
||||
|
||||
get slipGst(): number {
|
||||
return this.slipLines.controls.reduce((sum, _c, i) => sum + this.lineGstAt(i), 0);
|
||||
}
|
||||
|
||||
/** The figure the account is debited for — rounded up to the whole rupee, as the single flow does. */
|
||||
get slipTotal(): number {
|
||||
return Math.ceil(this.slipFaceValue + this.slipCharges + this.slipGst - 1e-9);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.isSlip) {
|
||||
this.slipForm = this.fb.group({
|
||||
lines: this.fb.array(
|
||||
Array.from({ length: this.lineCount }, () =>
|
||||
this.fb.group({
|
||||
beneficiaryName: [
|
||||
'',
|
||||
[
|
||||
Validators.required,
|
||||
Validators.maxLength(120)
|
||||
]
|
||||
],
|
||||
payableAtBranch: [
|
||||
'',
|
||||
[Validators.required]
|
||||
],
|
||||
payableAtCity: [
|
||||
'',
|
||||
[Validators.required]
|
||||
],
|
||||
// Zero is rejected rather than merely warned about: the server refuses a line with a
|
||||
// face value at or below zero, and a slip that fails there has already cost the clerk
|
||||
// every other line they typed.
|
||||
faceValue: [
|
||||
null as
|
||||
| number
|
||||
| null,
|
||||
[
|
||||
Validators.required,
|
||||
Validators.min(0.01)
|
||||
]
|
||||
],
|
||||
chargesAmount: [
|
||||
0,
|
||||
[Validators.min(0)]
|
||||
]
|
||||
})
|
||||
)
|
||||
)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -477,7 +869,25 @@ export class CashierIssueDdDialogComponent implements OnInit {
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.form.invalid) return;
|
||||
if (this.activeForm.invalid) return;
|
||||
|
||||
// A slip closes with its lines. There is no walk-in branch here: a slip is only ever offered on
|
||||
// a member-account debit, where the purchaser is the account holder already on record.
|
||||
if (this.isSlip) {
|
||||
const lines: DdSlipLineResult[] = this.slipLines.controls.map((c) => {
|
||||
const v = c.value;
|
||||
return {
|
||||
beneficiaryName: (v.beneficiaryName || '').trim().toUpperCase(),
|
||||
payableAtBranch: (v.payableAtBranch || '').trim().toUpperCase(),
|
||||
payableAtCity: (v.payableAtCity || '').trim().toUpperCase(),
|
||||
faceValue: Number(v.faceValue) || 0,
|
||||
chargesAmount: Number(v.chargesAmount) || 0
|
||||
};
|
||||
});
|
||||
this.dialogRef.close({ lines });
|
||||
return;
|
||||
}
|
||||
|
||||
const val = this.form.value;
|
||||
const beneficiary = {
|
||||
beneficiaryName: (val.beneficiaryName || '').trim().toUpperCase(),
|
||||
|
||||
Reference in New Issue
Block a user