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:
2026-10-23 11:43:30 +05:30
co-authored by Claude Opus 5.5
parent 3e5cc26e49
commit fca550f4c4
47 changed files with 12426 additions and 0 deletions
@@ -0,0 +1,343 @@
<!--
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="i4c-page">
<!-- Modern Toolbar & Filter Chips -->
<div class="i4c-toolbar">
<div class="i4c-filter-chips">
@for (s of statuses; track s) {
<button class="filter-chip" [class.filter-chip--active]="status === s" (click)="setStatus(s)">
<span>{{ s || 'All Requests' }}</span>
</button>
}
</div>
<span class="i4c-toolbar__spacer"></span>
<div class="maker-checker-pill">
<fa-icon icon="shield-alt"></fa-icon>
<span>Maker-Checker Enforced: you cannot approve a request you raised.</span>
</div>
<button mat-flat-button color="primary" class="refresh-btn" (click)="load()" [disabled]="loading">
<fa-icon icon="sync" [animation]="loading ? 'spin' : undefined"></fa-icon> Refresh
</button>
</div>
<div class="i4c-filter-bar">
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="search-field">
<mat-label>Account / ack / order no. / officer</mat-label>
<input
matInput
[(ngModel)]="search"
(keyup.enter)="load(true)"
(blur)="load(true)"
placeholder="Type & hit enter..."
/>
</mat-form-field>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label>Action</mat-label>
<mat-select [(ngModel)]="action" (selectionChange)="load(true)">
<mat-option value="">All actions</mat-option>
<mat-option value="RELEASE">Release hold</mat-option>
<mat-option value="FREEZE">Freeze debits</mat-option>
<mat-option value="UNFREEZE">Unfreeze debits</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label>Reason</mat-label>
<mat-select [(ngModel)]="reasonType" (selectionChange)="load(true)">
<mat-option value="">All reasons</mat-option>
@for (r of reasonTypes; track r.value) {
<mat-option [value]="r.value">{{ r.label }}</mat-option>
}
</mat-select>
</mat-form-field>
<mifosx-i4c-date-range label="Raised on" [range]="dates" (rangeChange)="onDates($event)"></mifosx-i4c-date-range>
<button mat-stroked-button class="i4c-filter-bar__clear" (click)="clearFilters()" [disabled]="!hasFilters">
<fa-icon icon="times"></fa-icon> Clear filters
</button>
</div>
@if (loading) {
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
}
@if (error) {
<div class="i4c-error">
<fa-icon icon="exclamation-circle"></fa-icon>
<span>{{ error }}</span>
</div>
}
<!-- Approvals Table Card -->
<div class="i4c-card">
<div class="i4c-card__head">
<fa-icon icon="user-check"></fa-icon>
<span>Checker Approval Queue (I4C Account Holds)</span>
<span class="head-hint">{{ total }} requests in current queue</span>
</div>
<div class="i4c-table-wrap">
<table class="i4c-table">
<thead>
<tr>
<th>Raised On</th>
<th>Requested Action</th>
<th>Target Account / Complaint</th>
<th class="num">Held Amount (₹)</th>
<th>Reason &amp; Legal Order</th>
<th>Maker Remarks</th>
<th>Status</th>
<th>Decision Actions</th>
</tr>
</thead>
<tbody>
@for (r of rows; track r.id) {
<tr>
<td>
<span class="date-main">{{ r.maker_at | date: 'dd MMM yyyy, HH:mm' }}</span>
<div class="sub">
Raised by: <b>{{ r.maker_name }}</b>
</div>
</td>
<td>
<span
class="i4c-badge"
[class.i4c-badge--red]="r.action !== 'RELEASE'"
[class.i4c-badge--blue]="r.action === 'RELEASE'"
>
{{ r.action }}
</span>
</td>
<td>
<a [routerLink]="['../holds']" [queryParams]="{ view: '' }" class="acc-link font-semibold">
{{ r.account_no }}
</a>
<div class="sub">{{ r.account_type }} • Ack: {{ r.acknowledgement_no }}</div>
</td>
<td class="num">
<span class="font-bold text-accent">₹{{ r.amount_held | number: '1.2-2' }}</span>
<div class="sub">Disputed: ₹{{ r.disputed_amount | number: '1.2-2' }}</div>
</td>
<td>
<div class="reason-title">{{ reasonLabel(r.reason_type) }}</div>
<div class="sub">
Ref: {{ r.order_reference }} {{ r.order_date ? '(' + r.order_date + ')' : '' }}
{{ r.authority ? '• ' + r.authority : '' }}
</div>
</td>
<td>
<div class="remarks-text">{{ r.remarks || '-' }}</div>
</td>
<td>
<span
class="i4c-badge"
[class.i4c-badge--green]="r.status === 'APPROVED'"
[class.i4c-badge--red]="r.status === 'REJECTED' || r.status === 'FAILED'"
[class.i4c-badge--amber]="r.status === 'PENDING'"
>
{{ r.status }}
</span>
@if (r.checker_name) {
<div class="sub">Checked by {{ r.checker_name }}, {{ r.checker_at | date: 'dd MMM, HH:mm' }}</div>
}
@if (r.rejection_reason || r.failure_message) {
<div class="sub err-text">{{ r.rejection_reason || r.failure_message }}</div>
}
</td>
<td class="actions-cell">
@if (r.status === 'PENDING') {
<div class="i4c-actions">
<button class="approve-btn" (click)="startApprove(r)" [disabled]="busyId === r.id">
<fa-icon icon="check"></fa-icon> Approve
</button>
<button class="reject-btn" (click)="startReject(r)" [disabled]="busyId === r.id">
<fa-icon icon="times"></fa-icon> Reject
</button>
</div>
} @else {
<span class="text-muted">Decision Recorded</span>
}
</td>
</tr>
} @empty {
<tr>
<td colspan="8" class="i4c-empty">
<div class="empty-icon"><fa-icon icon="user-check"></fa-icon></div>
<div>No approval requests in this category.</div>
</td>
</tr>
}
</tbody>
</table>
</div>
<mat-paginator
class="i4c-paginator"
[length]="total"
[pageIndex]="pageIndex"
[pageSize]="pageSize"
[pageSizeOptions]="pageSizes"
(page)="onPage($event)"
showFirstLastButtons
aria-label="Approval requests pages"
></mat-paginator>
</div>
<!-- ========================================================
MODERN POPUP WINDOW: APPROVE CONFIRMATION MODAL
======================================================== -->
@if (approvingRow) {
<div class="i4c-modal-backdrop" (click)="closeApprove()">
<div class="i4c-modal-window i4c-modal-window--compact" (click)="$event.stopPropagation()">
<div class="i4c-modal-header approve-header">
<div class="i4c-modal-header__title">
<fa-icon icon="check-circle" class="text-green"></fa-icon>
<div>
<span>Confirm Checker Approval</span>
<div class="i4c-modal-header__sub">Executing live CBS account action</div>
</div>
</div>
<button class="modal-close-btn" (click)="closeApprove()" aria-label="Close">
<fa-icon icon="times"></fa-icon>
</button>
</div>
<div class="i4c-modal-body">
<div class="approve-card">
<div class="approve-action-badge">
<span
>ACTION: <b>{{ approvingRow.action }}</b></span
>
</div>
<dl class="i4c-kv">
<dt>Account Number</dt>
<dd>
<b>{{ approvingRow.account_no }}</b>
</dd>
<dt>Held Amount</dt>
<dd>
<b class="text-accent">₹{{ approvingRow.amount_held | number: '1.2-2' }}</b>
</dd>
<dt>Reason</dt>
<dd>{{ reasonLabel(approvingRow.reason_type) }}</dd>
<dt>Order Reference</dt>
<dd>
{{ approvingRow.order_reference }}
{{ approvingRow.authority ? '(' + approvingRow.authority + ')' : '' }}
</dd>
<dt>Maker Details</dt>
<dd>{{ approvingRow.maker_name }} on {{ approvingRow.maker_at | date: 'dd MMM yyyy, HH:mm' }}</dd>
</dl>
</div>
<div class="i4c-note warn-note">
<fa-icon icon="exclamation-triangle"></fa-icon>
<span
>Warning: Once confirmed, the hold change or freeze will be executed immediately on the core banking
system.</span
>
</div>
</div>
<div class="i4c-modal-footer">
<button class="footer-btn-close" (click)="closeApprove()">Cancel</button>
<button
mat-flat-button
color="primary"
class="confirm-approve-btn"
(click)="confirmApprove()"
[disabled]="busyId === approvingRow.id"
>
<fa-icon
[icon]="busyId === approvingRow.id ? 'sync' : 'check'"
[animation]="busyId === approvingRow.id ? 'spin' : undefined"
></fa-icon>
Confirm &amp; Approve
</button>
</div>
</div>
</div>
}
<!-- ========================================================
MODERN POPUP WINDOW: REJECT REASON MODAL
======================================================== -->
@if (rejectingRow) {
<div class="i4c-modal-backdrop" (click)="closeReject()">
<div class="i4c-modal-window" (click)="$event.stopPropagation()">
<div class="i4c-modal-header reject-header">
<div class="i4c-modal-header__title">
<fa-icon icon="times-circle" class="text-red"></fa-icon>
<div>
<span>Reject Request #{{ rejectingRow.id }}</span>
<div class="i4c-modal-header__sub">Provide the reason for rejecting this hold action</div>
</div>
</div>
<button class="modal-close-btn" (click)="closeReject()" aria-label="Close">
<fa-icon icon="times"></fa-icon>
</button>
</div>
<div class="i4c-modal-body">
<div class="reject-summary">
<span
>Rejecting: <b>{{ rejectingRow.action }}</b> on Account <b>{{ rejectingRow.account_no }}</b></span
>
</div>
<!-- Preset Reason Chips -->
<div class="preset-box">
<div class="preset-title">Quick Preset Reasons:</div>
<div class="preset-chips">
@for (preset of presetReasons; track preset) {
<button type="button" class="preset-chip" (click)="applyPreset(preset)">
{{ preset }}
</button>
}
</div>
</div>
<!-- Rejection Reason Input -->
<mat-form-field appearance="outline" class="w-full">
<mat-label>Rejection Reason (Required)</mat-label>
<textarea
matInput
rows="3"
[(ngModel)]="rejectReason"
placeholder="Explain why this request is rejected for the maker's audit log..."
maxlength="1000"
></textarea>
<mat-hint align="end">{{ rejectReason.length }}/1000</mat-hint>
</mat-form-field>
</div>
<div class="i4c-modal-footer">
<button class="footer-btn-close" (click)="closeReject()">Cancel</button>
<button
mat-flat-button
color="warn"
class="confirm-reject-btn"
(click)="confirmReject()"
[disabled]="busyId === rejectingRow.id || !rejectReason.trim()"
>
<fa-icon
[icon]="busyId === rejectingRow.id ? 'sync' : 'times'"
[animation]="busyId === rejectingRow.id ? 'spin' : undefined"
></fa-icon>
Confirm Rejection
</button>
</div>
</div>
</div>
}
</div>