feat(credit-bureau): CIC enquiry-alert dashboard and generic API Management hub (static UI)

Replace CIBIL fetch screens with a dashboard of our customers' loan enquiries
at other lenders (no lender name), CIC whitelist/API mapping, and a Vendor ->
Work -> API set management UI (IN/OUT, SMS, WhatsApp, CIC) with full HTTP detail
and JSON/cURL/Postman/HAR import.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 15:13:56 +05:30
co-authored by Claude Sonnet 5.5
parent 1b70fb4534
commit 7eac2dcf3b
43 changed files with 6557 additions and 2265 deletions
@@ -8,30 +8,44 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit, inject } from '@angular/core';
import { Router, RouterModule } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { CicService, cicError, cicScoreBand } from '../cic.service';
import { HasPermissionDirective } from 'app/directives/has-permission/has-permission.directive';
import { CicAlertDetailComponent } from '../cic-alert-detail/cic-alert-detail.component';
import { CicService, cicError, cicStatusTone } from '../cic.service';
/** Credit Bureau overview: volumes, outcomes, score spread and the state of each bureau. */
/**
* Credit Bureau dashboard: our customers who applied for a loan at another bank / NBFC, as reported by the CICs -
* who, what loan and how much (the CIC does not name the lender) - with the customers applying elsewhere repeatedly,
* the loan types, the trend, and the fetch status of each bureau. The other pages (fetch log, CIC configuration, API
* mapping) are reached from the buttons at the top.
*/
@Component({
selector: 'mifosx-cic-dashboard',
standalone: true,
imports: [
CommonModule,
RouterModule
FormsModule,
RouterModule,
HasPermissionDirective,
CicAlertDetailComponent
],
templateUrl: './cic-dashboard.component.html',
styleUrls: ['../cic-shared.scss']
})
export class CicDashboardComponent implements OnInit {
private cic = inject(CicService);
private router = inject(Router);
days = 30;
loading = false;
error: string | null = null;
info: string | null = null;
data: any = null;
fetching: number | 'ALL' | null = null;
openAlertId: number | null = null;
readonly scoreBand = cicScoreBand;
readonly statusTone = cicStatusTone;
ngOnInit(): void {
this.load();
@@ -40,7 +54,7 @@ export class CicDashboardComponent implements OnInit {
load(): void {
this.loading = true;
this.error = null;
this.cic.dashboard().subscribe({
this.cic.dashboard(this.days).subscribe({
next: (d) => {
this.data = d;
this.loading = false;
@@ -52,32 +66,54 @@ export class CicDashboardComponent implements OnInit {
});
}
statusCount(status: string): number {
return Number(this.data?.byStatusLast30Days?.[status] ?? 0);
get whitelisted(): any[] {
return (this.data?.bureaus ?? []).filter((b: any) => b.whitelisted);
}
get successRate(): number {
const total = Number(this.data?.enquiriesLast30Days ?? 0);
if (!total) return 0;
return Math.round(((this.statusCount('SUCCESS') + this.statusCount('REUSED')) / total) * 100);
/** Fetch now from one bureau, or from every whitelisted bureau in turn. */
fetchNow(bureauId: number | 'ALL'): void {
const ids = bureauId === 'ALL' ? this.whitelisted.map((b) => b.id as number) : [bureauId];
if (!ids.length) return;
this.fetching = bureauId;
this.error = null;
this.info = null;
const messages: string[] = [];
const next = (i: number) => {
if (i >= ids.length) {
this.fetching = null;
this.info = messages.join(' · ');
this.load();
return;
}
this.cic.fetch(ids[i]).subscribe({
next: (run) => {
messages.push(
`${run.bureauCode}: ${run.status === 'SUCCESS' ? run.newAlerts + ' new alert(s)' : 'failed - ' + run.message}`
);
next(i + 1);
},
error: (e) => {
messages.push(cicError(e, 'Fetch failed.'));
next(i + 1);
}
});
};
next(0);
}
get maxBand(): number {
return Math.max(1, ...(this.data?.scoreBandsLast30Days ?? []).map((b: any) => Number(b.count)));
max(list: any[] | undefined, key: string): number {
return Math.max(1, ...(list ?? []).map((x) => Number(x[key] ?? 0)));
}
get whitelistedCount(): number {
return (this.data?.bureaus ?? []).filter((b: any) => b.whitelisted).length;
pct(value: any, list: any[] | undefined, key: string): number {
return Math.round((Number(value ?? 0) / this.max(list, key)) * 100);
}
statusTone(status: string): string {
return status === 'SUCCESS' ? 'good' : status === 'REUSED' ? 'ok' : status === 'NO_HIT' ? 'warn' : 'bad';
onChanged(): void {
this.load();
}
open(id: number): void {
this.router.navigate([
'/credit-bureau/enquiries',
id
]);
fetchTone(status?: string): string {
return status === 'SUCCESS' ? 'good' : status === 'FAILED' ? 'bad' : '';
}
}