/** * 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 } from '@angular/forms'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { MatIconModule } from '@angular/material/icon'; import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; import { catchError } from 'rxjs/operators'; import { of } from 'rxjs'; import { ClerkClosureRequestService } from '../../../../cashier/clerk-closure-request.service'; /** Manager's Approve/Reject review of one Cash closure request — opened as a MatDialog popup from * the Closure Requests tab, mirroring DisbursementReviewComponent's shape. Approving here is what * actually runs Fineract's close command (pro-rata interest + TDS + the real GL withdrawal all * post at this moment); a cashier then hands over the rounded cash figure against the token. */ @Component({ selector: 'mifosx-closure-review-dialog', standalone: true, imports: [ CommonModule, FormsModule, MatSnackBarModule, MatIconModule, MatDialogModule ], templateUrl: './closure-review-dialog.component.html', styleUrl: './closure-review-dialog.component.scss' }) export class ClosureReviewDialogComponent implements OnInit { private service = inject(ClerkClosureRequestService); private snackBar = inject(MatSnackBar); private dialogRef = inject(MatDialogRef); private dialogData = inject<{ requestId: number }>(MAT_DIALOG_DATA); requestId: number | null = null; request: any = null; loading = false; acting = false; showRejectForm = false; rejectionReason = ''; ngOnInit(): void { this.requestId = this.dialogData?.requestId ?? null; if (this.requestId) this.loadRequest(); } loadRequest(): void { if (!this.requestId) return; this.loading = true; this.service .getById(this.requestId) .pipe(catchError(() => of(null))) .subscribe((res: any) => { this.request = res; this.loading = false; }); } approve(): void { if (!this.requestId || this.acting) return; this.acting = true; this.service .approve(this.requestId) .pipe(catchError((err) => of({ error: err?.error?.errors?.[0]?.defaultUserMessage ?? 'Approval failed.' }))) .subscribe((res: any) => { this.acting = false; if (res?.error) { this.snackBar.open(res.error, 'Close', { duration: 6000 }); } else { // A nil-balance closure has no cash to hand over, so the backend closes it outright // rather than parking it in the cashier's token queue. const msg = res?.status === 'CLOSED' ? 'Account closed. Nil balance — no cash hand-over was required.' : 'Closure approved — ₹' + this.formatAmount(res?.cashierDisbursedAmount) + ' is ready for the cashier to hand over against the token.'; this.snackBar.open(msg, 'Close', { duration: 6000 }); this.close(true); } }); } reject(): void { if (!this.requestId || this.acting || !this.rejectionReason.trim()) return; this.acting = true; this.service .reject(this.requestId, this.rejectionReason.trim()) .pipe(catchError((err) => of({ error: err?.error?.errors?.[0]?.defaultUserMessage ?? 'Rejection failed.' }))) .subscribe((res: any) => { this.acting = false; if (res?.error) { this.snackBar.open(res.error, 'Close', { duration: 6000 }); } else { this.snackBar.open('Closure request rejected. The account remains Active and untouched.', 'Close', { duration: 4000 }); this.close(true); } }); } close(refreshed = false): void { this.dialogRef.close({ refreshed }); } statusClass(status: string): string { const map: Record = { PENDING: 'crv-status--pending', APPROVED: 'crv-status--approved', REJECTED: 'crv-status--rejected', CLOSED: 'crv-status--closed' }; return map[status] ?? ''; } formatAmount(val: any): string { const n = Number(val ?? 0); return n.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } formatDateTime(val: any): string { if (!val) return '—'; try { const d = new Date(val); return d.toLocaleString('en-IN', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }); } catch { return String(val); } } }