New CFCFRMS > I4C Live section backed by /v1/i4c/admin and /v1/i4c/cases (the demo mock interceptor does not match these paths): - Dashboard, complaints from I4C, holds register, approvals, responses and API log, bank code master, intimation settings. - Hold popup: overview, hold transactions, customer intimation (automatic and manual), maker request (full / partial release, freeze, unfreeze), GRM / MRM and documents, request history. - GRM grievances, MRM restorations and MRM balance report pages; shared date range filter (today, yesterday, week, month, custom), server-side paging and filters on every list; document upload / download. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
321 lines
8.3 KiB
TypeScript
321 lines
8.3 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, 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);
|
|
}
|
|
}
|