/** * 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, AfterViewInit, ViewChild, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; import { MatTableModule, MatTableDataSource } from '@angular/material/table'; import { MatPaginatorModule, MatPaginator } from '@angular/material/paginator'; import { MatButtonModule } from '@angular/material/button'; import { MatDialog, MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { SponsorDDService } from '../sponsor-dd.service'; import { SignatureSettingsDialogComponent } from '../signature-settings-dialog/signature-settings-dialog.component'; @Component({ selector: 'mifosx-dd-issue-approvals', standalone: true, imports: [ CommonModule, RouterModule, MatCardModule, MatTableModule, MatPaginatorModule, MatButtonModule, MatDialogModule, MatSnackBarModule, MatFormFieldModule, MatInputModule ], templateUrl: './dd-issue-approvals.component.html', styles: [ ` .approvals-container { padding: 24px; max-width: 1300px; margin: 0 auto; } .header-row { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 24px; } .title-area h2 { margin: 0 0 4px 0; font-size: 24px; font-weight: 600; color: #1e293b; } .title-area p { margin: 0; color: #64748b; font-size: 14px; } .search-field { width: 320px; display: block; margin-bottom: 8px; } .mat-mdc-card { border-radius: 12px; border: 1px solid #e2e8f0; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); overflow: hidden; } table { width: 100%; } th { font-weight: 600; color: #475569; background-color: #f8fafc; } td { color: #334155; } .status-badge { display: inline-flex; align-items: center; padding: 4px 8px; border-radius: 9999px; font-size: 12px; font-weight: 500; } .status-badge.pending { background-color: #fef3c7; color: #b45309; } .status-badge.approved { background-color: #dcfce7; color: #15803d; } .status-badge.rejected { background-color: #fee2e2; color: #b91c1c; } .action-btn-group { display: flex; gap: 8px; } .status-filter-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; } .status-filter-row button.filter-active { background-color: #ede9fe; } .filter-count { color: #64748b; margin-left: 4px; } ` ] }) export class DdIssueApprovalsComponent implements OnInit, AfterViewInit { private sponsorDDService = inject(SponsorDDService); private dialog = inject(MatDialog); private snackBar = inject(MatSnackBar); @ViewChild(MatPaginator) paginator!: MatPaginator; displayedColumns: string[] = [ 'id', 'ddNumber', 'clientName', 'beneficiaryName', 'faceValue', 'paymentMethod', 'paymentType', 'status', 'makerUsername', 'createdDate', 'actions' ]; dataSource = new MatTableDataSource([]); readonly statusOptions: string[] = [ 'PENDING', 'APPROVED', 'REJECTED', 'ALL' ]; selectedStatus = 'PENDING'; searchText = ''; ngOnInit(): void { this.dataSource.filterPredicate = (data) => { const statusMatches = this.selectedStatus === 'ALL' || data.status === this.selectedStatus; const searchMatches = !this.searchText || (data.ddNumber || '').toLowerCase().includes(this.searchText.trim().toLowerCase()); return statusMatches && searchMatches; }; this.loadRequests(); } ngAfterViewInit(): void { this.dataSource.paginator = this.paginator; } loadRequests(): void { this.sponsorDDService.getAllIssueRequests().subscribe({ next: (data) => { this.dataSource.data = data; this.applyFilter(); }, error: (err) => { this.snackBar.open('Error loading requests: ' + (err.error?.message || err.message), 'Close', { duration: 5000 }); } }); } paymentTypeLabel(element: any): string { if (element.paymentMethod === 'CASH') { return 'Cash'; } if (element.paymentMethod === 'CHEQUE') { return 'Cheque'; } if (element.savingsAccountId) { return 'Savings'; } if (element.currentAccountId) { return 'Current'; } return '-'; } statusCount(status: string): number { const all = this.dataSource.data; return status === 'ALL' ? all.length : all.filter((r) => r.status === status).length; } filterByStatus(status: string): void { this.selectedStatus = status; this.applyFilter(); } onSearchChange(value: string): void { this.searchText = value; this.applyFilter(); } private applyFilter(): void { this.dataSource.filter = `${this.selectedStatus}|${this.searchText}`; if (this.dataSource.paginator) { this.dataSource.paginator.firstPage(); } } /** Every row sharing one batchId was submitted together as one multi-DD slip. */ batchSiblings(request: any): any[] { return request.batchId ? this.dataSource.data.filter((r) => r.batchId === request.batchId) : [request]; } batchSiblingCount(request: any): number { return this.batchSiblings(request).length; } approve(request: any): void { if (request.batchId) { this.approveBatch(request); return; } if ( !confirm( `Approve DD issuance for DD No. ${request.ddNumber} (₹${request.faceValue})? This will debit the account (or clear the cheque) and issue the draft.` ) ) { return; } this.sponsorDDService.approveIssueRequest(request.id).subscribe({ next: () => { this.snackBar.open('DD issuance approved and posted successfully', 'Close', { duration: 3000 }); this.loadRequests(); }, error: (err) => { this.snackBar.open('Error: ' + (err.error?.message || err.message), 'Close', { duration: 6000 }); } }); } private approveBatch(request: any): void { const siblings = this.batchSiblings(request); const lines = siblings.map((r) => ` • ${r.beneficiaryName} — ₹${r.faceValue}`).join('\n'); const total = siblings.reduce((sum, r) => sum + (Number(r.faceValue) || 0), 0); if ( !confirm( `Approve this DD slip - ${siblings.length} drafts, combined face value ₹${total}?\n${lines}\n\n` + 'This debits the account (or clears the cheque) ONCE for the combined total and issues every draft in the slip.' ) ) { return; } this.sponsorDDService.approveIssueRequestBatch(request.batchId).subscribe({ next: () => { this.snackBar.open('DD slip approved and posted successfully', 'Close', { duration: 3000 }); this.loadRequests(); }, error: (err) => { this.snackBar.open('Error: ' + (err.error?.message || err.message), 'Close', { duration: 6000 }); } }); } openRejectDialog(request: any): void { const dialogRef = this.dialog.open(RejectIssueRequestDialogComponent, { width: '450px', data: { request, siblingCount: this.batchSiblingCount(request) }, disableClose: true }); dialogRef.afterClosed().subscribe((result) => { if (result) { this.loadRequests(); } }); } openSignatureSettings(): void { this.dialog.open(SignatureSettingsDialogComponent, { width: '450px' }); } } // ── REJECT DIALOG COMPONENT ── @Component({ selector: 'mifosx-reject-issue-request-dialog', standalone: true, imports: [ CommonModule, FormsModule, ReactiveFormsModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatButtonModule ], template: `

Reject DD Issuance {{ data.request.batchId ? 'Slip' : 'Request' }} (DD No: {{ data.request.ddNumber }}{{ data.siblingCount > 1 ? ' + ' + (data.siblingCount - 1) + ' more' : '' }})

This slip has {{ data.siblingCount }} drafts — rejecting applies to all of them together.

Rejection Reason Reason is required
`, styles: [ ` .dialog-form { display: flex; flex-direction: column; gap: 12px; padding-top: 8px; } .full-width { width: 100%; } .hint-text { color: #64748b; font-size: 13px; margin: 0 0 12px 0; } ` ] }) export class RejectIssueRequestDialogComponent implements OnInit { private fb = inject(FormBuilder); private sponsorDDService = inject(SponsorDDService); private dialogRef = inject(MatDialogRef); private snackBar = inject(MatSnackBar); protected data = inject(MAT_DIALOG_DATA); rejectForm!: FormGroup; loading = false; ngOnInit(): void { this.rejectForm = this.fb.group({ rejectionReason: [ '', Validators.required ] }); } onCancel(): void { this.dialogRef.close(false); } onSubmit(): void { if (this.rejectForm.invalid) return; this.loading = true; const reason = this.rejectForm.value.rejectionReason; const request$ = this.data.request.batchId ? this.sponsorDDService.rejectIssueRequestBatch(this.data.request.batchId, reason) : this.sponsorDDService.rejectIssueRequest(this.data.request.id, reason); request$.subscribe({ next: () => { this.snackBar.open(this.data.request.batchId ? 'DD slip rejected' : 'DD issuance request rejected', 'Close', { duration: 3000 }); this.dialogRef.close(true); }, error: (err) => { this.loading = false; this.snackBar.open('Error: ' + (err.error?.message || err.message), 'Close', { duration: 5000 }); } }); } }