/** * 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 { DatePipe, DecimalPipe } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatPaginatorModule, PageEvent } from '@angular/material/paginator'; import { MatSnackBar } from '@angular/material/snack-bar'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { I4cService, i4cErrorMessage } from '../i4c.service'; import { ALL_DATES, I4cDateRange, I4cDateRangeComponent } from '../i4c-date-range/i4c-date-range.component'; /** Every complaint I4C has pushed to the bank, newest first. */ @Component({ selector: 'mifosx-i4c-requests', templateUrl: './i4c-requests.component.html', styleUrls: ['./i4c-requests.component.scss'], imports: [ DatePipe, DecimalPipe, FormsModule, MatButtonModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatProgressBarModule, MatPaginatorModule, I4cDateRangeComponent, FaIconComponent ] }) export class I4cRequestsComponent implements OnInit { private i4c = inject(I4cService); private route = inject(ActivatedRoute); private router = inject(Router); private snackBar = inject(MatSnackBar); readonly statuses = [ 'RECEIVED', 'PROCESSING', 'RETRY', 'PROCESSED' ]; rows: any[] = []; status = ''; search = ''; subCategory = ''; layer = ''; response = ''; dates: I4cDateRange = { ...ALL_DATES }; total = 0; pageIndex = 0; pageSize = 20; readonly pageSizes = [ 10, 20, 50, 100 ]; loading = false; error = ''; // Popup Window Modal state selectedComplaint: any = null; detailLoading = false; detailError = ''; modalTab: 'overview' | 'incidents' | 'findings' | 'outbox' | 'payloads' = 'overview'; openFindingJsonId: number | null = null; reprocessing = false; retryingOutboxId: number | null = null; copiedId = ''; ngOnInit(): void { this.status = this.route.snapshot.queryParamMap.get('status') || ''; this.load(); } /** Reload the current page; a changed filter passes `fromFirstPage` so the officer lands on page one. */ load(fromFirstPage = false): void { if (fromFirstPage) { this.pageIndex = 0; } this.loading = true; this.error = ''; this.i4c .requests({ status: this.status, search: this.search.trim(), subCategory: this.subCategory.trim(), layer: this.layer, response: this.response, fromDate: this.dates.fromDate, toDate: this.dates.toDate, offset: this.pageIndex * this.pageSize, limit: this.pageSize }) .subscribe({ next: (page) => { this.rows = page.pageItems; this.total = page.totalFilteredRecords; this.loading = false; }, error: (e) => { this.error = i4cErrorMessage(e); this.loading = false; } }); } setStatus(s: string): void { this.status = s; this.load(true); } onPage(e: PageEvent): void { this.pageIndex = e.pageIndex; this.pageSize = e.pageSize; this.load(); } onDates(range: I4cDateRange): void { this.dates = range; this.load(true); } get hasFilters(): boolean { return !!( this.status || this.search || this.subCategory || this.layer || this.response || this.dates.preset !== 'ALL' ); } clearFilters(): void { this.status = ''; this.search = ''; this.subCategory = ''; this.layer = ''; this.response = ''; this.dates = { ...ALL_DATES }; this.load(true); } // Open in Modern Popup Window Modal openModal(row: any): void { this.detailLoading = true; this.detailError = ''; this.modalTab = 'overview'; this.openFindingJsonId = null; this.selectedComplaint = { ...row }; this.i4c.request(row.id).subscribe({ next: (full) => { // Keep the list figures (held amount, response status) if the detail lacks them. this.selectedComplaint = { ...row, ...full }; this.detailLoading = false; }, error: (e) => { this.detailError = i4cErrorMessage(e); this.detailLoading = false; } }); } closeModal(): void { this.selectedComplaint = null; } toggleFindingJson(id: number): void { this.openFindingJsonId = this.openFindingJsonId === id ? null : id; } // Open in Full Page view openFullPage(id: number): void { this.closeModal(); this.router.navigate([id], { relativeTo: this.route }); } reprocessModal(id: number): void { this.reprocessing = true; this.i4c.reprocess(id).subscribe({ next: () => { this.reprocessing = false; this.snackBar.open('Complaint queued for reprocessing.', 'OK', { duration: 4000 }); this.openModal({ id }); this.load(); }, error: (e) => { this.reprocessing = false; this.snackBar.open(i4cErrorMessage(e), 'Close', { duration: 6000 }); } }); } retryOutboxItem(id: number): void { this.retryingOutboxId = id; this.i4c.retryOutbox(id).subscribe({ next: (r) => { this.retryingOutboxId = null; this.snackBar.open(r?.sent ? 'Delivered to I4C.' : 'Queued for retry.', 'OK', { duration: 4000 }); if (this.selectedComplaint?.id) { this.openModal({ id: this.selectedComplaint.id }); } }, error: (e) => { this.retryingOutboxId = null; this.snackBar.open(i4cErrorMessage(e), 'Close', { duration: 6000 }); } }); } copyAck(text: string, id: string, event?: Event): void { if (event) { event.stopPropagation(); } if (!text) { return; } navigator.clipboard.writeText(text); this.copiedId = id; this.snackBar.open(`Copied: ${text}`, 'OK', { duration: 2500 }); setTimeout(() => { if (this.copiedId === id) { this.copiedId = ''; } }, 2000); } /** What a section 6 status_code means, as I4C reads it. */ statusCodeMeaning(code: string): string { switch (code) { case '00': return 'Success'; case '01': return 'Record not found'; case '02': return 'Invalid RRN'; case '70010': return 'Not processed'; default: return code ? 'Code ' + code : 'No code'; } } statusCodeClass(code: string): string { return code === '00' ? 'i4c-badge--green' : code === '01' || code === '02' ? 'i4c-badge--amber' : 'i4c-badge--red'; } prettyJson(val: any): string { if (!val) { return ''; } try { const parsed = typeof val === 'string' ? JSON.parse(val) : val; return JSON.stringify(parsed, null, 2); } catch { return String(val); } } statusClass(status: string): string { switch (status) { case 'PROCESSED': return 'i4c-badge--green'; case 'RETRY': return 'i4c-badge--red'; case 'PROCESSING': case 'RECEIVED': return 'i4c-badge--amber'; default: return ''; } } responseClass(status: string): string { switch (status) { case 'SENT': return 'i4c-badge--green'; case 'FAILED': return 'i4c-badge--red'; case 'PENDING': case 'SENDING': return 'i4c-badge--amber'; default: return ''; } } outcomeClass(outcome: string): string { switch (outcome) { case 'HELD': return 'i4c-badge--green'; case 'SHORT': return 'i4c-badge--amber'; case 'NOT_FOUND': case 'FAILED': return 'i4c-badge--red'; default: return ''; } } holdFor(incidentId: number): any { return (this.selectedComplaint?.holds || []).find((h: any) => h.incident_id === incidentId); } }