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
@@ -0,0 +1,237 @@
<!--
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/.
-->
<div class="cic-modal-backdrop" (click)="closed.emit()">
<div class="cic-modal" style="width: min(980px, 100%)" (click)="$event.stopPropagation()">
@if (loading && !alert) {
<div class="cic-empty">Loading…</div>
}
@if (alert) {
<div style="display: flex; justify-content: space-between; gap: 12px; align-items: flex-start">
<div>
<h3 style="margin-bottom: 4px">
{{ alert.clientName || alert.customerName || 'Unknown customer' }} applied for
{{ alert.loanType || 'a loan' }} elsewhere
</h3>
<div class="muted" style="font-size: 12px">
{{ alert.bureauCode }} alert · {{ alert.alertKey }} · received {{ alert.receivedOn | slice: 0 : 16 }} via
{{ alert.receivedVia }}
</div>
</div>
<div class="cic-actions">
<span class="cic-badge" [class]="'cic-badge ' + statusTone(alert.status)">{{ alert.status }}</span>
<button class="cic-btn sm" (click)="closed.emit()">Close</button>
</div>
</div>
@if (error) {
<div class="cic-alert error" style="margin-top: 12px">{{ error }}</div>
}
@if (info) {
<div class="cic-alert success" style="margin-top: 12px">{{ info }}</div>
}
<div class="cic-grid cols-3" style="margin-top: 14px">
<div class="cic-stat">
<div class="k">Loan applied for</div>
<div class="v" style="font-size: 17px">{{ alert.loanType || '—' }}</div>
</div>
<div class="cic-stat">
<div class="k">Amount</div>
<div class="v" style="font-size: 20px">
{{ alert.amount !== null && alert.amount !== undefined ? '₹' + (alert.amount | number: '1.0-0') : '—' }}
</div>
</div>
<div class="cic-stat">
<div class="k">Enquiry date</div>
<div class="v" style="font-size: 17px">{{ alert.enquiryDate | date: 'dd MMM yyyy' }}</div>
</div>
</div>
<div class="cic-split" style="margin-top: 14px">
<div class="cic-card" style="margin-bottom: 0">
<h2>
Enquiry elsewhere
<span class="cic-card-sub">the CIC does not name the lender</span>
</h2>
<dl class="cic-kv" style="grid-template-columns: 140px minmax(0, 1fr)">
<dt>Loan type</dt>
<dd>{{ alert.loanType || '—' }}</dd>
<dt>Amount</dt>
<dd>
{{ alert.amount !== null && alert.amount !== undefined ? '₹' + (alert.amount | number: '1.0-2') : '—' }}
</dd>
<dt>Purpose</dt>
<dd>{{ alert.purpose || '—' }}</dd>
<dt>Place</dt>
<dd>{{ alert.location || '—' }}</dd>
<dt>Enquiry date</dt>
<dd>{{ alert.enquiryDate | date: 'dd MMM yyyy' }}</dd>
</dl>
</div>
<div class="cic-card" style="margin-bottom: 0">
<h2>
Our customer
@if (!alert.clientId) {
<button
class="cic-btn sm primary"
(click)="linkCustomer()"
[disabled]="saving"
*mifosxHasPermission="'UPDATE_CICALERT'"
>
Link to customer
</button>
}
</h2>
<dl class="cic-kv" style="grid-template-columns: 140px minmax(0, 1fr)">
<dt>Customer</dt>
<dd>
@if (alert.clientId) {
<a [routerLink]="['/clients', alert.clientId, 'general']" (click)="closed.emit()">{{
alert.clientName
}}</a>
<span class="muted"> · {{ alert.clientAccountNo }}</span>
} @else {
<span class="tone-bad">Not matched to any customer</span>
}
</dd>
<dt>Branch</dt>
<dd>{{ alert.officeName || '—' }}</dd>
<dt>Matched by</dt>
<dd>{{ matchLabels[alert.matchType] || alert.matchType }}</dd>
<dt>Name at bureau</dt>
<dd>{{ alert.customerName || '—' }}</dd>
<dt>PAN / Aadhaar</dt>
<dd class="cic-mono">{{ alert.panMasked || '—' }} / {{ alert.aadhaarMasked || '—' }}</dd>
@if (alert.customerRef) {
<dt>Reference</dt>
<dd class="cic-mono">{{ alert.customerRef }}</dd>
}
</dl>
</div>
</div>
@if (alert.clientId) {
<div class="cic-card" style="margin-top: 14px; margin-bottom: 0">
<h2>
Customer's loans with us
<span class="cic-card-sub">outstanding ₹{{ exposure | number: '1.0-0' }}</span>
</h2>
@if (alert.clientLoans?.length) {
<div class="cic-table-wrap">
<table class="cic-table">
<thead>
<tr>
<th>Account</th>
<th>Product</th>
<th>Status</th>
<th class="num">Principal</th>
<th class="num">Outstanding</th>
<th>Disbursed</th>
</tr>
</thead>
<tbody>
@for (l of alert.clientLoans; track l.id) {
<tr>
<td class="cic-mono">{{ l.accountNo }}</td>
<td>{{ l.productName }}</td>
<td>{{ l.status }}</td>
<td class="num">{{ l.principal | number: '1.0-0' }}</td>
<td class="num">{{ l.outstanding | number: '1.0-0' }}</td>
<td>{{ l.disbursedOn | date: 'dd MMM yyyy' }}</td>
</tr>
}
</tbody>
</table>
</div>
} @else {
<div class="muted" style="font-size: 13px">
No loan with us. A customer with deposits only who is borrowing elsewhere may be a cross-sell opportunity.
</div>
}
</div>
}
@if (alert.otherAlertsOfCustomer?.length) {
<div class="cic-card" style="margin-top: 14px; margin-bottom: 0">
<h2>
Other enquiries on this customer
<span class="cic-card-sub">{{ alert.otherAlertsOfCustomer?.length }} more</span>
</h2>
<div class="cic-table-wrap">
<table class="cic-table">
<thead>
<tr>
<th>Date</th>
<th>Loan type</th>
<th class="num">Amount</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@for (o of alert.otherAlertsOfCustomer; track o.id) {
<tr class="clickable" (click)="alertId = o.id; load()">
<td>{{ o.enquiryDate | date: 'dd MMM yyyy' }}</td>
<td>{{ o.loanType }}</td>
<td class="num">{{ o.amount | number: '1.0-0' }}</td>
<td>
<span class="cic-badge" [class]="'cic-badge ' + statusTone(o.status)">{{ o.status }}</span>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
@if (alert.remarks || alert.actionedByName) {
<div class="cic-alert info" style="margin-top: 14px">
<b>{{ alert.status }}</b> by {{ alert.actionedByName }} on {{ alert.actionedOn | slice: 0 : 16 }}
@if (alert.remarks) {
— {{ alert.remarks }}
}
</div>
}
<ng-container *mifosxHasPermission="'UPDATE_CICALERT'">
@if (action) {
<div class="cic-field" style="margin-top: 14px">
<label>{{ action === 'ACTIONED' ? 'What was done' : 'Reason for dismissing' }}</label>
<textarea rows="3" [(ngModel)]="remarks" maxlength="1000"></textarea>
</div>
}
<div class="foot">
@if (action) {
<button class="cic-btn" (click)="action = null">Cancel</button>
<button class="cic-btn primary" (click)="confirmAction()" [disabled]="saving">
{{ action === 'ACTIONED' ? 'Save as actioned' : 'Dismiss alert' }}
</button>
} @else {
@if (alert.status === 'ACTIONED' || alert.status === 'DISMISSED') {
<button class="cic-btn" (click)="setStatus('SEEN', null)" [disabled]="saving">Reopen</button>
}
@if (alert.status !== 'DISMISSED') {
<button class="cic-btn danger" (click)="action = 'DISMISSED'">Dismiss</button>
}
@if (alert.status !== 'ACTIONED') {
<button class="cic-btn primary" (click)="action = 'ACTIONED'">Mark actioned</button>
}
}
</div>
</ng-container>
<div style="margin-top: 12px">
<button class="cic-btn sm" (click)="showRaw = !showRaw">{{ showRaw ? 'Hide' : 'Show' }} bureau data</button>
@if (showRaw) {
<pre class="cic-code" style="margin-top: 8px">{{ rawText() }}</pre>
}
</div>
}
</div>
</div>