Files
cbsnexora/src/app/sponsor-dd/dd-issue-approvals/dd-issue-approvals.component.ts
T
keshav 3cf01e2733 Add customer search dialog and sponsor DD/reconciliation modules
Adds sponsor-dd and sponsor-dd-reconciliation feature modules, a
shared customer search dialog, and wires them into routing/shell/
organization screens alongside cashier and deposit-request updates.
2026-07-22 11:59:31 +05:30

400 lines
11 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, 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<any>([]);
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: `
<h2 mat-dialog-title>
Reject DD Issuance {{ data.request.batchId ? 'Slip' : 'Request' }} (DD No: {{ data.request.ddNumber
}}{{ data.siblingCount > 1 ? ' + ' + (data.siblingCount - 1) + ' more' : '' }})
</h2>
<mat-dialog-content>
<p class="hint-text" *ngIf="data.request.batchId">
This slip has {{ data.siblingCount }} drafts — rejecting applies to all of them together.
</p>
<form [formGroup]="rejectForm" class="dialog-form">
<mat-form-field appearance="outline" class="full-width">
<mat-label>Rejection Reason</mat-label>
<textarea matInput formControlName="rejectionReason" rows="3"></textarea>
<mat-error *ngIf="rejectForm.get('rejectionReason')?.hasError('required')">Reason is required</mat-error>
</mat-form-field>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="onCancel()">Cancel</button>
<button mat-flat-button color="warn" [disabled]="rejectForm.invalid || loading" (click)="onSubmit()">
Confirm Rejection
</button>
</mat-dialog-actions>
`,
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<RejectIssueRequestDialogComponent>);
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 });
}
});
}
}