feat(cfcfrms): I4C Live screens for the I4C-MHA integration
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>
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user