/** * 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: `
receipt_long

{{ isAccountDebit ? 'Account-Debit Demand Draft (DD)' : 'Walk-in Cash Demand Draft (DD)' }}

Next available DD leaflet auto-selected · {{ isAccountDebit ? 'Enter beneficiary details' : 'Enter purchaser & beneficiary details' }}

Auto-Selected Leaflet {{ data.leaflet.ddNumber }} {{ data.sponsorBankName || 'Sponsor Bank' }} · {{ data.branchName }}
Face Value ₹{{ data.faceValue | number: '1.2-2' }} + Charges ₹{{ data.chargesAmount | number: '1.2-2' }}
GST ({{ data.gstPercentage }}%) ₹{{ data.gstAmount | number: '1.2-2' }} on charges only
{{ isAccountDebit ? 'Total Debited' : 'Total Cash Collected' }} ₹{{ data.totalCashPayable | number: '1.2-2' }} @if (isAccountDebit) { {{ data.accountNo || 'Account' }} } @else { Denominations verified }
@if (isAccountDebit) {
account_balance_wallet
Debiting {{ data.accountNo }} @if (data.accountHolderName) { · {{ data.accountHolderName }} }
₹{{ data.totalCashPayable | number: '1.2-2' }} will be debited from this account when a checker approves. No cash is taken at the counter.
}
@if (!isAccountDebit) {
Walk-in Purchaser Details
Walk-in Purchaser Name * Purchaser name is required Must be at least 3 characters 12-Digit Aadhaar Number * Masked to last 4 digits before storage Aadhaar number is required Must be exactly 12 numeric digits
Purchaser Address / Location * Address is required }
Beneficiary & Payment Details
Beneficiary Name (In Favour Of) * Beneficiary name is required Max 120 characters
Payable At - Branch * Payable Branch is required Payable At - City * Payable City is required
`, 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); 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 } ); } }