501 lines
17 KiB
TypeScript
501 lines
17 KiB
TypeScript
/**
|
|
* 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 & 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">
|
|
<!-- 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>
|
|
<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()">
|
|
Save DD Details
|
|
</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
|
|
}
|
|
);
|
|
}
|
|
}
|