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>
344 lines
13 KiB
HTML
344 lines
13 KiB
HTML
<!--
|
|
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 & 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 & 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>
|