/** * 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 { ActivatedRoute, Router } from '@angular/router'; 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 { ClerkDisbursementService } from '../../cashier/clerk-disbursement.service'; interface CreditEntry { accountId?: number; accountNo: string; accountName: string; accountType: string; amount: number; dbCr?: string; } @Component({ selector: 'mifosx-disbursement-review', standalone: true, imports: [ CommonModule, FormsModule, MatSnackBarModule, MatIconModule, MatDialogModule ], templateUrl: './disbursement-review.component.html', styleUrl: './disbursement-review.component.scss' }) export class DisbursementReviewComponent implements OnInit { private route = inject(ActivatedRoute); private router = inject(Router); private service = inject(ClerkDisbursementService); private snackBar = inject(MatSnackBar); /** Present only when opened as a popup from the requests list; absent when reached by direct * route (e.g. a bookmarked/shared link). Both entry points stay supported — this only changes * how "done" is signalled: closing the dialog vs. navigating back to the inbox route. */ private dialogRef = inject(MatDialogRef, { optional: true }); private dialogData = inject<{ requestId: number } | null>(MAT_DIALOG_DATA, { optional: true }); requestId: number | null = null; request: any = null; creditEntries: CreditEntry[] = []; loading = false; acting = false; showRejectForm = false; rejectionReason = ''; get isDialog(): boolean { return !!this.dialogRef; } ngOnInit(): void { if (this.dialogData?.requestId) { this.requestId = this.dialogData.requestId; } else { const id = this.route.snapshot.params['id']; this.requestId = id != null ? Number(id) : 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; if (res?.credit_entries) { try { const raw: any[] = typeof res.credit_entries === 'string' ? JSON.parse(res.credit_entries) : res.credit_entries; this.creditEntries = raw.map((e) => ({ ...e, amount: parseFloat(e.amount) || 0 })); } catch { this.creditEntries = []; } } }); } 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: 5000 }); } else { const msg = this.isBankTransferMode ? 'Bank transfer approved successfully.' : this.isFundTransferMode ? 'Fund transfer approved successfully.' : 'Disbursement approved successfully.'; this.snackBar.open(msg, 'Close', { duration: 3000 }); this.finish(); } }); } 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: 5000 }); } else { this.snackBar.open('Request rejected.', 'Close', { duration: 3000 }); this.finish(); } }); } /** "Done" for this screen — closes the popup (telling the list to refresh) when opened as a * dialog, or returns to the inbox route otherwise. */ private finish(): void { if (this.dialogRef) { this.dialogRef.close({ refreshed: true }); } else { this.router.navigate(['/checker-inbox-and-tasks/disbursement-requests']); } } get isBankTransferMode(): boolean { return this.request?.request_type === 'BANK_TRANSFER'; } /** Mechanically identical to a Disbursement (same 1 GL debit -> N account credits flow) — only * the label differs, so a checker doesn't read this as a loan payout. */ get isFundTransferMode(): boolean { return this.request?.request_type === 'FUND_TRANSFER'; } get isCollectionMode(): boolean { if (this.isBankTransferMode) return false; return this.creditEntries.some((e) => e.dbCr === 'Dr'); } get drEntries(): CreditEntry[] { return this.creditEntries.filter((e) => e.dbCr === 'Dr'); } get crEntries(): CreditEntry[] { return this.creditEntries.filter((e) => e.dbCr !== 'Dr'); } totalCreditAmount(): number { const entries = this.isCollectionMode ? this.drEntries : this.crEntries; return entries.reduce((sum, e) => sum + (e.amount ?? 0), 0); } totalDebitAmount(): number { return this.drEntries.reduce((sum, e) => sum + (e.amount ?? 0), 0); } /** * Human-readable account type for the badge. The stored value is the backend's own code * (SAVINGS / CURRENT / FIXED_DEPOSIT / RECURRING_DEPOSIT / SHARE / LOAN / GL), so this only * changes presentation -- a checker should read "Fixed Deposit", not "FIXED_DEPOSIT". */ accountTypeLabel(accountType: string): string { const labels: Record = { SAVINGS: 'Savings', CURRENT: 'Current', FIXED_DEPOSIT: 'Fixed Deposit', RECURRING_DEPOSIT: 'Recurring Deposit', SHARE: 'Share', LOAN: 'Loan', GL: 'GL' }; return labels[(accountType ?? '').toUpperCase()] ?? accountType; } statusClass(status: string): string { const map: Record = { PENDING: 'dri-status--pending', PROCESSING: 'dri-status--processing', APPROVED: 'dri-status--approved', REJECTED: 'dri-status--rejected', FAILED: 'dri-status--failed' }; return map[status] ?? ''; } 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); } } backToInbox(): void { if (this.dialogRef) { this.dialogRef.close(); } else { this.router.navigate(['/checker-inbox-and-tasks/disbursement-requests']); } } }