feat(cfcfrms): I4C Live screens for SOP layer chain, risk, bank holds and restorations
- Risk screen and shared I4C date field - Bank Master: IFSC to I4C bank code map and unmapped IFSC list - Holds, restorations, grievances, documents, outbox and dashboard updates for the backend I4C SOP / outward ledger changes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@ import { I4cIntimationSettingsComponent } from './i4c/i4c-intimation-settings/i4
|
|||||||
import { I4cGrievancesComponent } from './i4c/i4c-grievances/i4c-grievances.component';
|
import { I4cGrievancesComponent } from './i4c/i4c-grievances/i4c-grievances.component';
|
||||||
import { I4cRestorationsComponent } from './i4c/i4c-restorations/i4c-restorations.component';
|
import { I4cRestorationsComponent } from './i4c/i4c-restorations/i4c-restorations.component';
|
||||||
import { I4cMrmReportComponent } from './i4c/i4c-mrm-report/i4c-mrm-report.component';
|
import { I4cMrmReportComponent } from './i4c/i4c-mrm-report/i4c-mrm-report.component';
|
||||||
|
import { I4cRiskComponent } from './i4c/i4c-risk/i4c-risk.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
Route.withShell([
|
Route.withShell([
|
||||||
@@ -143,6 +144,11 @@ const routes: Routes = [
|
|||||||
component: I4cMrmReportComponent,
|
component: I4cMrmReportComponent,
|
||||||
data: { title: 'I4C MRM Balance Report', breadcrumb: 'MRM Balance Report' }
|
data: { title: 'I4C MRM Balance Report', breadcrumb: 'MRM Balance Report' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'risk',
|
||||||
|
component: I4cRiskComponent,
|
||||||
|
data: { title: 'I4C Risk Registers', breadcrumb: 'Risk Registers' }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'intimation-settings',
|
path: 'intimation-settings',
|
||||||
component: I4cIntimationSettingsComponent,
|
component: I4cIntimationSettingsComponent,
|
||||||
|
|||||||
@@ -76,7 +76,8 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
mat-form-field {
|
mat-form-field,
|
||||||
|
mifosx-i4c-date-field {
|
||||||
width: 220px;
|
width: 220px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+170
-15
@@ -609,7 +609,7 @@
|
|||||||
animation: i4c-modal-pop 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
animation: i4c-modal-pop 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
|
||||||
&--wide {
|
&--wide {
|
||||||
max-width: 860px;
|
max-width: min(980px, 95vw);
|
||||||
}
|
}
|
||||||
|
|
||||||
&--full {
|
&--full {
|
||||||
@@ -679,42 +679,197 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modern Modal Tabs Bar */
|
||||||
.i4c-modal-tabs {
|
.i4c-modal-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 6px 16px;
|
padding: 8px 14px;
|
||||||
background: #f8fafc;
|
background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
|
||||||
border-bottom: 1px solid #e2e8f0;
|
border-bottom: 1px solid #e2e8f0;
|
||||||
overflow-x: auto;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
.modal-tab {
|
.modal-tab {
|
||||||
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 6px;
|
||||||
padding: 4px 10px;
|
padding: 6px 12px;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #64748b;
|
color: #475569;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
user-select: none;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
transition:
|
||||||
|
color 0.2s ease,
|
||||||
|
transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
background: #e2e8f0;
|
background: rgb(226 232 240 / 70%);
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--active {
|
&--active,
|
||||||
color: #2563eb;
|
&--active:hover {
|
||||||
|
color: #1d4ed8;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-color: #e2e8f0;
|
border-color: #cbd5e1;
|
||||||
box-shadow: 0 1px 3px rgb(15 23 42 / 5%);
|
box-shadow:
|
||||||
|
0 2px 6px -1px rgb(15 23 42 / 8%),
|
||||||
|
0 1px 3px -1px rgb(15 23 42 / 4%);
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--has-risk {
|
||||||
|
&:not(.modal-tab--active) {
|
||||||
|
color: #991b1b;
|
||||||
|
background: rgb(254 242 242 / 65%);
|
||||||
|
border-color: rgb(254 202 202 / 70%);
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #7f1d1d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.modal-tab--active {
|
||||||
|
border-color: #fca5a5;
|
||||||
|
box-shadow:
|
||||||
|
0 2px 6px -1px rgb(220 38 38 / 15%),
|
||||||
|
0 1px 3px -1px rgb(15 23 42 / 4%);
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When an existing i4c-badge is nested in a tab */
|
||||||
|
.i4c-badge {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 1.5px 6.5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sleek Tab Badges for Status & Risk Alerts */
|
||||||
|
.tab-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 1.5px 7px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&__dot {
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentcolor;
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--pulse &__dot::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -2px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentcolor;
|
||||||
|
animation: tab-alert-pulse 1.8s infinite ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--danger {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #dc2626;
|
||||||
|
border-color: #fecaca;
|
||||||
|
box-shadow: 0 1px 2px rgb(220 38 38 / 6%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--amber {
|
||||||
|
background: #fffbeb;
|
||||||
|
color: #b45309;
|
||||||
|
border-color: #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--green {
|
||||||
|
background: #ecfdf5;
|
||||||
|
color: #059669;
|
||||||
|
border-color: #a7f3d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--blue {
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
border-color: #bfdbfe;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compact numeric pill inside tab buttons */
|
||||||
|
.tab-count {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0 5px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #475569;
|
||||||
|
line-height: 1;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-tab--active .tab-count {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tab-alert-pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.9);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
transform: scale(2.4);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(2.4);
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,103 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!-- The bank's own IFSC -> I4C Bank Code map -->
|
||||||
|
<div class="i4c-card">
|
||||||
|
<div class="i4c-card-head">
|
||||||
|
<fa-icon icon="exchange-alt"></fa-icon>
|
||||||
|
<span>IFSC to I4C Bank Code map</span>
|
||||||
|
<span class="head-hint"
|
||||||
|
>When money moved to another bank, the response names it by this code so CFCFRMS can send that bank the next
|
||||||
|
notice (MHA SOP 6(g), 6(j)). A full IFSC wins over a prefix, which wins over the master.</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="i4c-form ifsc-map-form">
|
||||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||||
|
<mat-label>IFSC prefix or full IFSC</mat-label>
|
||||||
|
<input matInput [(ngModel)]="mapKey" maxlength="11" placeholder="HDFC or HDFC0CXYZ01" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||||
|
<mat-label>I4C Bank Code</mat-label>
|
||||||
|
<input matInput type="number" [(ngModel)]="mapCode" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="ifsc-map-note">
|
||||||
|
<mat-label>Note</mat-label>
|
||||||
|
<input matInput [(ngModel)]="mapNote" maxlength="300" placeholder="e.g. co-operative bank on a sponsor IFSC" />
|
||||||
|
</mat-form-field>
|
||||||
|
<button mat-flat-button color="primary" (click)="addMapping()" [disabled]="mapBusy || !mapKey || !mapCode">
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@if (mapError) {
|
||||||
|
<div class="i4c-error">
|
||||||
|
<fa-icon icon="exclamation-circle"></fa-icon>
|
||||||
|
<span>{{ mapError }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="i4c-table-wrap">
|
||||||
|
<table class="i4c-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>IFSC / prefix</th>
|
||||||
|
<th class="num">Bank Code</th>
|
||||||
|
<th>Bank</th>
|
||||||
|
<th>Note</th>
|
||||||
|
<th>Added</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (m of mappings; track m.id) {
|
||||||
|
<tr>
|
||||||
|
<td class="font-mono">{{ m.ifsc_key }}</td>
|
||||||
|
<td class="num">{{ m.bank_code }}</td>
|
||||||
|
<td>{{ m.bank_name || '-' }}</td>
|
||||||
|
<td>{{ m.note || '-' }}</td>
|
||||||
|
<td>{{ m.added_at | date: 'dd MMM yyyy' }} · {{ m.added_by }}</td>
|
||||||
|
<td><button mat-button (click)="removeMapping(m.id)" [disabled]="mapBusy">Remove</button></td>
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="i4c-empty">No mappings. The master's IFSC prefixes are used.</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@if (unknown.length) {
|
||||||
|
<div class="i4c-card-head">
|
||||||
|
<span>Reported with no bank code (payee_bank_code 0)</span>
|
||||||
|
<span class="head-hint">Map these so later responses name the next bank.</span>
|
||||||
|
</div>
|
||||||
|
<div class="i4c-table-wrap">
|
||||||
|
<table class="i4c-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>IFSC</th>
|
||||||
|
<th class="num">Times</th>
|
||||||
|
<th>Last seen</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (u of unknown; track u.ifsc) {
|
||||||
|
<tr>
|
||||||
|
<td class="font-mono">{{ u.ifsc || '(none given)' }}</td>
|
||||||
|
<td class="num">{{ u.times }}</td>
|
||||||
|
<td>{{ u.last_seen | date: 'dd MMM yyyy' }}</td>
|
||||||
|
<td>
|
||||||
|
@if (u.ifsc) {
|
||||||
|
<button mat-button (click)="mapKey = u.ifsc">Map</button>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Main Directory Card -->
|
<!-- Main Directory Card -->
|
||||||
<div class="i4c-card">
|
<div class="i4c-card">
|
||||||
<div class="i4c-card-head">
|
<div class="i4c-card-head">
|
||||||
|
|||||||
@@ -358,3 +358,15 @@
|
|||||||
.ifsc-filter {
|
.ifsc-filter {
|
||||||
width: 170px;
|
width: 170px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ifsc-map-form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ifsc-map-note {
|
||||||
|
flex: 1 1 240px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,8 +66,61 @@ export class I4cBankMasterComponent implements OnInit {
|
|||||||
copiedKey = '';
|
copiedKey = '';
|
||||||
selectedBankModal: any = null;
|
selectedBankModal: any = null;
|
||||||
|
|
||||||
|
mappings: any[] = [];
|
||||||
|
unknown: any[] = [];
|
||||||
|
mapKey = '';
|
||||||
|
mapCode: number | null = null;
|
||||||
|
mapNote = '';
|
||||||
|
mapBusy = false;
|
||||||
|
mapError = '';
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.load();
|
this.load();
|
||||||
|
this.loadMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMap(): void {
|
||||||
|
this.i4c.ifscMap().subscribe({
|
||||||
|
next: (m) => {
|
||||||
|
this.mappings = m.mappings;
|
||||||
|
this.unknown = m.unknown;
|
||||||
|
},
|
||||||
|
error: (e) => (this.mapError = i4cErrorMessage(e))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addMapping(): void {
|
||||||
|
this.mapBusy = true;
|
||||||
|
this.mapError = '';
|
||||||
|
this.i4c
|
||||||
|
.mapIfsc({ ifscKey: this.mapKey.trim().toUpperCase(), bankCode: Number(this.mapCode), note: this.mapNote.trim() })
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.mapKey = '';
|
||||||
|
this.mapCode = null;
|
||||||
|
this.mapNote = '';
|
||||||
|
this.mapBusy = false;
|
||||||
|
this.loadMap();
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
this.mapError = i4cErrorMessage(e);
|
||||||
|
this.mapBusy = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMapping(id: number): void {
|
||||||
|
this.mapBusy = true;
|
||||||
|
this.i4c.unmapIfsc(id).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.mapBusy = false;
|
||||||
|
this.loadMap();
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
this.mapError = i4cErrorMessage(e);
|
||||||
|
this.mapBusy = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setType(t: string): void {
|
setType(t: string): void {
|
||||||
|
|||||||
@@ -195,6 +195,102 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- GRM / MRM -->
|
||||||
|
<section class="kpi-group">
|
||||||
|
<div class="kpi-group__head">
|
||||||
|
<fa-icon icon="balance-scale"></fa-icon>
|
||||||
|
<span>GRM & MRM</span>
|
||||||
|
</div>
|
||||||
|
<div class="kpi-group__row">
|
||||||
|
<a class="modern-kpi" routerLink="../grievances" [class.modern-kpi--warn]="data.grievancesOverdue > 0">
|
||||||
|
<div class="kpi-top">
|
||||||
|
<span class="kpi-icon-circle kpi-icon-circle--amber"><fa-icon icon="balance-scale"></fa-icon></span>
|
||||||
|
</div>
|
||||||
|
<span class="modern-kpi__value">{{ data.grievancesOpen }}</span>
|
||||||
|
<span class="modern-kpi__label">Open Grievances</span>
|
||||||
|
<span class="modern-kpi__sub"
|
||||||
|
>{{ data.grievancesOverdue }} past deadline · {{ data.grievancesToCarryOut }} orders to carry out</span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
<a class="modern-kpi" routerLink="../restorations" [class.modern-kpi--warn]="data.restorationsOverdue > 0">
|
||||||
|
<div class="kpi-top">
|
||||||
|
<span class="kpi-icon-circle kpi-icon-circle--green"><fa-icon icon="hand-holding-usd"></fa-icon></span>
|
||||||
|
</div>
|
||||||
|
<span class="modern-kpi__value">{{ data.restorationsOpen }}</span>
|
||||||
|
<span class="modern-kpi__label">Open Restorations</span>
|
||||||
|
<span class="modern-kpi__sub"
|
||||||
|
>{{ data.restorationsOverdue }} past the 15-day credit · {{ data.restorationsMrmToUpdate }} to enter on
|
||||||
|
MRM</span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
<a class="modern-kpi" routerLink="../restorations" [class.modern-kpi--warn]="data.restorationsInPool > 0">
|
||||||
|
<div class="kpi-top">
|
||||||
|
<span class="kpi-icon-circle kpi-icon-circle--blue"><fa-icon icon="exchange-alt"></fa-icon></span>
|
||||||
|
</div>
|
||||||
|
<span class="modern-kpi__value">{{ data.restorationsInPool }}</span>
|
||||||
|
<span class="modern-kpi__label">Restorations In Transit</span>
|
||||||
|
<span class="modern-kpi__sub"
|
||||||
|
>₹{{ data.amountInPool | number: '1.0-0' }} in payable / outward · ₹{{
|
||||||
|
data.amountRestored | number: '1.0-0'
|
||||||
|
}}
|
||||||
|
restored</span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- SOP & risk duties -->
|
||||||
|
<section class="kpi-group">
|
||||||
|
<div class="kpi-group__head">
|
||||||
|
<fa-icon icon="user-shield"></fa-icon>
|
||||||
|
<span>SOP & Risk</span>
|
||||||
|
</div>
|
||||||
|
<div class="kpi-group__row">
|
||||||
|
<a
|
||||||
|
class="modern-kpi"
|
||||||
|
routerLink="../holds"
|
||||||
|
[class.modern-kpi--warn]="data.leaNoticesDue > 0 || data.holdsLapsed > 0"
|
||||||
|
>
|
||||||
|
<div class="kpi-top">
|
||||||
|
<span class="kpi-icon-circle kpi-icon-circle--amber"><fa-icon icon="calendar-alt"></fa-icon></span>
|
||||||
|
</div>
|
||||||
|
<span class="modern-kpi__value">{{ data.leaNoticesDue }}</span>
|
||||||
|
<span class="modern-kpi__label">LEA Notices Due</span>
|
||||||
|
<span class="modern-kpi__sub">{{ data.holdsLapsed }} hold(s) lapsed without extension</span>
|
||||||
|
</a>
|
||||||
|
<a class="modern-kpi" routerLink="../risk" [class.modern-kpi--warn]="data.mulesSuspected > 0">
|
||||||
|
<div class="kpi-top">
|
||||||
|
<span class="kpi-icon-circle kpi-icon-circle--red"><fa-icon icon="user-shield"></fa-icon></span>
|
||||||
|
</div>
|
||||||
|
<span class="modern-kpi__value">{{ data.mulesSuspected }}</span>
|
||||||
|
<span class="modern-kpi__label">Suspected Mules</span>
|
||||||
|
<span class="modern-kpi__sub"
|
||||||
|
>{{ data.mulesConfirmed }} confirmed · {{ data.digitalSuspended }} digital suspensions</span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="modern-kpi"
|
||||||
|
routerLink="../risk"
|
||||||
|
[class.modern-kpi--warn]="
|
||||||
|
data.fraudReportsOverdue > 0 || data.linkedAlertsOpen > 0 || data.onwardUnknownBank > 0
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="kpi-top">
|
||||||
|
<span class="kpi-icon-circle kpi-icon-circle--red"><fa-icon icon="file-alt"></fa-icon></span>
|
||||||
|
</div>
|
||||||
|
<span class="modern-kpi__value">{{ data.fraudReportsOverdue }}</span>
|
||||||
|
<span class="modern-kpi__label">FMR / STR Overdue</span>
|
||||||
|
<span class="modern-kpi__sub"
|
||||||
|
>{{ data.linkedAlertsOpen }} linked alerts · {{ data.onwardUnknownBank || 0 }} onward transfers with no
|
||||||
|
bank code · {{ data.shadowCreditsDue }} shadow credits due · {{ data.bankHoldsOpen }} bank holds ({{
|
||||||
|
data.bankHoldDecisionsDue
|
||||||
|
}}
|
||||||
|
decisions due)</span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Middle Row: API Performance + Complaints Status -->
|
<!-- Middle Row: API Performance + Complaints Status -->
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<!--
|
||||||
|
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/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||||
|
<mat-label>{{ label }}</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
[matDatepicker]="picker"
|
||||||
|
[value]="date"
|
||||||
|
[max]="maxDate"
|
||||||
|
(dateChange)="onChange($event)"
|
||||||
|
placeholder="dd/mm/yyyy"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #picker></mat-datepicker>
|
||||||
|
</mat-form-field>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Fills the width the parent form gives it; size tokens (field height, text size) are inherited from the parent.
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* 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, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
|
||||||
|
import { MatDatepickerInputEvent, MatDatepickerModule } from '@angular/material/datepicker';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { DdsDateAdapter, DDS_DATE_FORMATS } from '../../../organization/dds-batch-processing/dds-date-adapter';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A date field with a calendar picker for the I4C forms. Shown and typed day-first (dd/mm/yyyy); the value in and out is
|
||||||
|
* the yyyy-MM-dd string the I4C APIs take, or '' when empty, so it drops in where a plain date input bound a string.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-i4c-date-field',
|
||||||
|
templateUrl: './i4c-date-field.component.html',
|
||||||
|
styleUrls: ['./i4c-date-field.component.scss'],
|
||||||
|
imports: [
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatDatepickerModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: DateAdapter, useClass: DdsDateAdapter },
|
||||||
|
{ provide: MAT_DATE_FORMATS, useValue: DDS_DATE_FORMATS }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class I4cDateFieldComponent {
|
||||||
|
@Input() label = '';
|
||||||
|
/** Latest date that can be picked, yyyy-MM-dd; defaults to none. */
|
||||||
|
@Input() max = '';
|
||||||
|
@Output() valueChange = new EventEmitter<string>();
|
||||||
|
|
||||||
|
date: Date | null = null;
|
||||||
|
|
||||||
|
/** yyyy-MM-dd, or '' for no date. */
|
||||||
|
@Input()
|
||||||
|
set value(value: string | null | undefined) {
|
||||||
|
this.date = parse(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
get maxDate(): Date | null {
|
||||||
|
return parse(this.max);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(event: MatDatepickerInputEvent<Date>): void {
|
||||||
|
this.date = event.value;
|
||||||
|
this.valueChange.emit(event.value ? iso(event.value) : '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse(value: string | null | undefined): Date | null {
|
||||||
|
const match = value ? /^(\d{4})-(\d{2})-(\d{2})/.exec(value) : null;
|
||||||
|
return match ? new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** yyyy-MM-dd in the browser's own time zone (toISOString would shift to UTC). */
|
||||||
|
function iso(d: Date): string {
|
||||||
|
const pad = (n: number) => (n < 10 ? '0' + n : '' + n);
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||||
|
}
|
||||||
@@ -9,75 +9,144 @@
|
|||||||
<div class="docs">
|
<div class="docs">
|
||||||
@if (missing.length) {
|
@if (missing.length) {
|
||||||
<div class="docs__missing">
|
<div class="docs__missing">
|
||||||
<fa-icon icon="exclamation-triangle"></fa-icon>
|
<div class="docs__missing-head">
|
||||||
Still needed:
|
<fa-icon icon="exclamation-triangle"></fa-icon>
|
||||||
@for (m of missing; track m; let last = $last) {
|
<span>Mandatory Documents Required (MHA SOP):</span>
|
||||||
<b>{{ label(m) }}</b
|
</div>
|
||||||
>{{ last ? '' : ', ' }}
|
<div class="docs__missing-chips">
|
||||||
}
|
@for (m of missing; track m) {
|
||||||
|
<span class="missing-chip">{{ label(m) }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<table class="i4c-table">
|
<div class="docs__table-wrap">
|
||||||
<thead>
|
<table class="i4c-table docs-table">
|
||||||
<tr>
|
<thead>
|
||||||
<th>Type</th>
|
|
||||||
<th>File</th>
|
|
||||||
<th>Remarks</th>
|
|
||||||
<th>Uploaded</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@for (d of rows; track d.id) {
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ label(d.doc_type) }}</td>
|
<th>Document Type</th>
|
||||||
<td>
|
<th>File Details</th>
|
||||||
<span class="docs__name">{{ d.file_name }}</span>
|
<th>Remarks</th>
|
||||||
<div class="docs__sub" [title]="'SHA-256 ' + d.sha256">
|
<th>Uploaded Info</th>
|
||||||
{{ size(d.size_bytes) }} · {{ d.sha256.slice(0, 12) }}…
|
<th class="action-col"></th>
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>{{ d.remarks || '-' }}</td>
|
|
||||||
<td>
|
|
||||||
{{ d.uploaded_at | date: 'dd MMM yyyy, HH:mm' }}
|
|
||||||
<div class="docs__sub">{{ d.uploaded_by }}</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button mat-stroked-button type="button" (click)="download(d)">
|
|
||||||
<fa-icon icon="download"></fa-icon> Download
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
</thead>
|
||||||
<tr>
|
<tbody>
|
||||||
<td colspan="5" class="i4c-empty">No documents attached yet.</td>
|
@for (d of rows; track d.id) {
|
||||||
</tr>
|
<tr>
|
||||||
}
|
<td>
|
||||||
</tbody>
|
<span class="doc-type-badge">{{ label(d.doc_type) }}</span>
|
||||||
</table>
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="doc-file-info">
|
||||||
|
<span class="docs__name">{{ d.file_name }}</span>
|
||||||
|
<div class="docs__sub" [title]="'SHA-256: ' + d.sha256">
|
||||||
|
<span class="doc-size">{{ size(d.size_bytes) }}</span>
|
||||||
|
<span class="doc-hash-pill"><fa-icon icon="shield-alt"></fa-icon> {{ d.sha256.slice(0, 10) }}…</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="doc-remarks">{{ d.remarks || '-' }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="doc-upload-meta">
|
||||||
|
<span class="doc-upload-date">{{ d.uploaded_at | date: 'dd MMM yyyy, HH:mm' }}</span>
|
||||||
|
<span class="doc-upload-by">by {{ d.uploaded_by }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="action-col">
|
||||||
|
<button class="doc-download-btn" type="button" (click)="download(d)">
|
||||||
|
<fa-icon icon="download"></fa-icon> Download
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="docs-empty-row">
|
||||||
|
<div class="docs-empty-state">
|
||||||
|
<div class="docs-empty-state__icon">
|
||||||
|
<fa-icon icon="folder-open"></fa-icon>
|
||||||
|
</div>
|
||||||
|
<div class="docs-empty-state__title">No documents attached yet</div>
|
||||||
|
<div class="docs-empty-state__desc">
|
||||||
|
Upload police intimations, IO notices, court orders (freeze/unfreeze), or grievance evidence below.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
@if (canUpload) {
|
@if (canUpload) {
|
||||||
<div class="docs__upload">
|
<div class="docs__upload-card">
|
||||||
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
<div class="docs__upload-head">
|
||||||
<mat-label>Document type</mat-label>
|
<div class="docs__upload-title">
|
||||||
<mat-select [(ngModel)]="docType">
|
<fa-icon icon="cloud-upload-alt"></fa-icon>
|
||||||
@for (t of types; track t.value) {
|
<span>Attach Case Document</span>
|
||||||
<mat-option [value]="t.value">{{ t.label }}</mat-option>
|
</div>
|
||||||
|
<span class="docs__upload-badge">Secure Vault</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="docs__upload-body">
|
||||||
|
<div class="docs__upload-row">
|
||||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="docs__type-field">
|
||||||
|
<mat-label>Document Type</mat-label>
|
||||||
|
<mat-select [(ngModel)]="docType">
|
||||||
|
@for (t of types; track t.value) {
|
||||||
|
<mat-option [value]="t.value">{{ t.label }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="docs__remarks-field">
|
||||||
|
<mat-label>Remarks / Reference (Order no., Court, Date)</mat-label>
|
||||||
|
<input matInput [(ngModel)]="remarks" maxlength="500" placeholder="e.g. IO Order Ref #1234, FIR #56/26" />
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="docs__upload-action-row">
|
||||||
|
<input
|
||||||
|
#fileInput
|
||||||
|
type="file"
|
||||||
|
accept="application/pdf,image/jpeg,image/png"
|
||||||
|
(change)="pick($event)"
|
||||||
|
class="hidden-file-input"
|
||||||
|
/>
|
||||||
|
|
||||||
|
@if (!file) {
|
||||||
|
<button type="button" class="file-picker-trigger" (click)="fileInput.click()">
|
||||||
|
<fa-icon icon="paperclip"></fa-icon>
|
||||||
|
<span>Choose File (PDF, JPEG, PNG)</span>
|
||||||
|
</button>
|
||||||
|
} @else {
|
||||||
|
<div class="file-selected-chip">
|
||||||
|
<fa-icon icon="file-alt"></fa-icon>
|
||||||
|
<span class="file-selected-name" [title]="file.name">{{ file.name }}</span>
|
||||||
|
<span class="file-selected-size">({{ size(file.size) }})</span>
|
||||||
|
<button type="button" class="file-remove-btn" (click)="clearFile(fileInput)" title="Remove file">
|
||||||
|
<fa-icon icon="times"></fa-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
<button class="doc-submit-btn" type="button" (click)="upload(fileInput)" [disabled]="uploading || !file">
|
||||||
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="docs__remarks">
|
<fa-icon icon="upload"></fa-icon>
|
||||||
<mat-label>Remarks (order no., date…)</mat-label>
|
<span>{{ uploading ? 'Uploading…' : 'Attach Document' }}</span>
|
||||||
<input matInput [(ngModel)]="remarks" maxlength="500" />
|
</button>
|
||||||
</mat-form-field>
|
</div>
|
||||||
<label class="docs__file">
|
|
||||||
<input #fileInput type="file" accept="application/pdf,image/jpeg,image/png" (change)="pick($event)" />
|
<div class="docs__security-footer">
|
||||||
</label>
|
<fa-icon icon="shield-alt"></fa-icon>
|
||||||
<button mat-flat-button color="primary" type="button" (click)="upload(fileInput)" [disabled]="uploading || !file">
|
<span
|
||||||
<fa-icon icon="upload"></fa-icon> {{ uploading ? 'Uploading…' : 'Attach' }}
|
>Maximum size 5 MB. Files are stored immutably with SHA-256 cryptographic hashing for regulatory
|
||||||
</button>
|
audit.</span
|
||||||
<span class="docs__sub">PDF, JPEG or PNG, up to 5 MB. Stored with a SHA-256; never changed once attached.</span>
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,50 +17,420 @@
|
|||||||
.docs {
|
.docs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
|
|
||||||
|
/* Alert banner for required documents */
|
||||||
&__missing {
|
&__missing {
|
||||||
padding: 8px 12px;
|
padding: 9px 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid #fde68a;
|
border: 1px solid #fde68a;
|
||||||
background: #fffbeb;
|
background: #fffbeb;
|
||||||
color: #92400e;
|
color: #92400e;
|
||||||
font-size: 12.5px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 6px;
|
||||||
|
|
||||||
|
&-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 11.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.missing-chip {
|
||||||
|
padding: 2px 7px;
|
||||||
|
background: #fef3c7;
|
||||||
|
border: 1px solid #fcd34d;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #78350f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modern Documents Table */
|
||||||
|
&__table-wrap {
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-table {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
thead th {
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 10.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody td {
|
||||||
|
padding: 9px 12px;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
font-size: 11.5px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:hover:not(.docs-empty-row) {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-col {
|
||||||
|
text-align: right;
|
||||||
|
width: 110px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-type-badge {
|
||||||
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
border: 1px solid #bfdbfe;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-file-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__name {
|
&__name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 11.5px;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__sub {
|
&__sub {
|
||||||
font-size: 11px;
|
font-size: 10.5px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__upload {
|
.doc-size {
|
||||||
display: flex;
|
font-weight: 500;
|
||||||
flex-wrap: wrap;
|
}
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding-top: 10px;
|
|
||||||
border-top: 1px solid #f1f5f9;
|
|
||||||
|
|
||||||
mat-form-field {
|
.doc-hash-pill {
|
||||||
width: 200px;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
padding: 1px 4px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #475569;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
font-size: 9px;
|
||||||
|
color: #64748b;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__remarks {
|
.doc-remarks {
|
||||||
flex: 1 1 220px;
|
color: #334155;
|
||||||
|
font-size: 11px;
|
||||||
|
max-width: 220px;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__file input {
|
.doc-upload-meta {
|
||||||
font-size: 12px;
|
display: flex;
|
||||||
max-width: 240px;
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-upload-date {
|
||||||
|
color: #0f172a;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-upload-by {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 10.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-download-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
|
height: 26px;
|
||||||
|
padding: 0 9px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #eff6ff;
|
||||||
|
border-color: #93c5fd;
|
||||||
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clean Empty State */
|
||||||
|
.docs-empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 22px 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #94a3b8;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__desc {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #64748b;
|
||||||
|
max-width: 420px;
|
||||||
|
margin-top: 3px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modern Upload Card */
|
||||||
|
&__upload-card {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__upload-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__upload-title {
|
||||||
|
font-size: 11.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__upload-badge {
|
||||||
|
font-size: 9.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__upload-body {
|
||||||
|
padding: 10px 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__upload-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 200px 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
@media (width <= 640px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__upload-action-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Styled File Picker Button */
|
||||||
|
.file-picker-trigger {
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px dashed #cbd5e1;
|
||||||
|
background: #fff;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 11.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #2563eb;
|
||||||
|
color: #2563eb;
|
||||||
|
background: #eff6ff;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-selected-chip {
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 8px 0 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #bfdbfe;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #1e40af;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 11.5px;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-selected-name {
|
||||||
|
font-weight: 600;
|
||||||
|
max-width: 220px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-selected-size {
|
||||||
|
font-size: 10.5px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-remove-btn {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #dc2626;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgb(220 38 38 / 12%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-submit-btn {
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 11.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
box-shadow: 0 1px 2px rgb(37 99 235 / 20%);
|
||||||
|
transition: all 0.18s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: #1d4ed8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__security-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 10.5px;
|
||||||
|
color: #64748b;
|
||||||
|
margin-top: 2px;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
color: #059669;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,6 +116,11 @@ export class I4cDocumentsComponent implements OnChanges {
|
|||||||
this.file = f;
|
this.file = f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearFile(input: HTMLInputElement): void {
|
||||||
|
this.file = null;
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
upload(input: HTMLInputElement): void {
|
upload(input: HTMLInputElement): void {
|
||||||
if (!this.file || !this.docType) {
|
if (!this.file || !this.docType) {
|
||||||
this.snackBar.open('Choose the document type and the file.', 'Close', { duration: 4000 });
|
this.snackBar.open('Choose the document type and the file.', 'Close', { duration: 4000 });
|
||||||
|
|||||||
@@ -327,10 +327,10 @@
|
|||||||
<input matInput [(ngModel)]="decision.orderRef" maxlength="200" />
|
<input matInput [(ngModel)]="decision.orderRef" maxlength="200" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
}
|
}
|
||||||
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
<mifosx-i4c-date-field
|
||||||
<mat-label>Order / decision date</mat-label>
|
label="Order / decision date"
|
||||||
<input matInput type="date" [(ngModel)]="decision.orderDate" />
|
[(value)]="decision.orderDate"
|
||||||
</mat-form-field>
|
></mifosx-i4c-date-field>
|
||||||
</div>
|
</div>
|
||||||
<mat-form-field appearance="outline" class="step__wide">
|
<mat-form-field appearance="outline" class="step__wide">
|
||||||
<mat-label
|
<mat-label
|
||||||
@@ -379,6 +379,18 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (ioSilent(g)) {
|
||||||
|
<div class="step">
|
||||||
|
<div class="step__title err-text">The IO has not decided within 15 days</div>
|
||||||
|
<div class="sub">SOP 9.1: the grievance goes to the District Grievance Officer.</div>
|
||||||
|
<div class="i4c-actions">
|
||||||
|
<button mat-flat-button color="warn" (click)="escalate()" [disabled]="busy">
|
||||||
|
<fa-icon icon="arrow-up"></fa-icon> Escalate to District GRO
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@if (g.status === 'REJECTED' && g.stage !== 'CLOSED') {
|
@if (g.status === 'REJECTED' && g.stage !== 'CLOSED') {
|
||||||
<div class="step">
|
<div class="step">
|
||||||
<div class="step__title">Rejected by the {{ g.decision_level }}: {{ g.decision_remarks }}</div>
|
<div class="step__title">Rejected by the {{ g.decision_level }}: {{ g.decision_remarks }}</div>
|
||||||
|
|||||||
@@ -11,3 +11,7 @@
|
|||||||
|
|
||||||
@include i4c-skin;
|
@include i4c-skin;
|
||||||
@include i4c-case-page;
|
@include i4c-case-page;
|
||||||
|
|
||||||
|
.err-text {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
|
|||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||||
import { I4cService, i4cErrorMessage } from '../i4c.service';
|
import { I4cService, i4cErrorMessage } from '../i4c.service';
|
||||||
|
import { I4cDateFieldComponent } from '../i4c-date-field/i4c-date-field.component';
|
||||||
import { ALL_DATES, I4cDateRange, I4cDateRangeComponent } from '../i4c-date-range/i4c-date-range.component';
|
import { ALL_DATES, I4cDateRange, I4cDateRangeComponent } from '../i4c-date-range/i4c-date-range.component';
|
||||||
import { I4cDocumentsComponent } from '../i4c-documents/i4c-documents.component';
|
import { I4cDocumentsComponent } from '../i4c-documents/i4c-documents.component';
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ export const GRM_DECISIONS: { value: string; label: string }[] = [
|
|||||||
{ value: 'UNFREEZE_HOLD_CONTINUES', label: 'Unfreeze the account, hold continues' },
|
{ value: 'UNFREEZE_HOLD_CONTINUES', label: 'Unfreeze the account, hold continues' },
|
||||||
{ value: 'UNFREEZE_AND_REMOVE_HOLD', label: 'Unfreeze and remove put on hold' },
|
{ value: 'UNFREEZE_AND_REMOVE_HOLD', label: 'Unfreeze and remove put on hold' },
|
||||||
{ value: 'PARTIAL_HOLD', label: 'Keep a smaller amount on hold' },
|
{ value: 'PARTIAL_HOLD', label: 'Keep a smaller amount on hold' },
|
||||||
|
{ value: 'RESTORE_DIGITAL', label: 'Restore digital services' },
|
||||||
{ value: 'REJECTED_CONTINUE', label: 'Request rejected, hold / freeze continues' },
|
{ value: 'REJECTED_CONTINUE', label: 'Request rejected, hold / freeze continues' },
|
||||||
{ value: 'SEEK_INFO', label: 'Seek additional information' }
|
{ value: 'SEEK_INFO', label: 'Seek additional information' }
|
||||||
];
|
];
|
||||||
@@ -69,7 +71,8 @@ const REQUESTS: Record<string, string> = {
|
|||||||
MatPaginatorModule,
|
MatPaginatorModule,
|
||||||
FaIconComponent,
|
FaIconComponent,
|
||||||
I4cDateRangeComponent,
|
I4cDateRangeComponent,
|
||||||
I4cDocumentsComponent
|
I4cDocumentsComponent,
|
||||||
|
I4cDateFieldComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class I4cGrievancesComponent implements OnInit {
|
export class I4cGrievancesComponent implements OnInit {
|
||||||
@@ -344,6 +347,11 @@ export class I4cGrievancesComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** SOP 9.1: the IO did not decide within 15 days. */
|
||||||
|
ioSilent(g: any): boolean {
|
||||||
|
return g.status === 'OPEN' && (g.stage === 'WITH_IO' || g.stage === 'IO_INVESTIGATION') && this.isLate(g.due_at);
|
||||||
|
}
|
||||||
|
|
||||||
missingDocs(g: any): string[] {
|
missingDocs(g: any): string[] {
|
||||||
return (g.requiredDocuments || []).filter((t: string) => !(g.documents || []).some((d: any) => d.doc_type === t));
|
return (g.requiredDocuments || []).filter((t: string) => !(g.documents || []).some((d: any) => d.doc_type === t));
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -200,37 +200,809 @@
|
|||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cases-grid {
|
/* ========================================================
|
||||||
display: grid;
|
SHARED PRIMITIVES FOR I4C CASES & RISK TABS
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
======================================================== */
|
||||||
gap: 14px;
|
.card-icon-badge {
|
||||||
}
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 11px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
@media (width <= 900px) {
|
&--blue {
|
||||||
.cases-grid {
|
background: #eff6ff;
|
||||||
grid-template-columns: minmax(0, 1fr);
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--amber {
|
||||||
|
background: #fffbeb;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--red {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--purple {
|
||||||
|
background: #f5f3ff;
|
||||||
|
color: #7c3aed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--green {
|
||||||
|
background: #ecfdf5;
|
||||||
|
color: #059669;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.case-row {
|
.status-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
|
||||||
|
&--green {
|
||||||
|
background: #ecfdf5;
|
||||||
|
color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--blue {
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--neutral {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: all 0.18s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
align-self: flex-start;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--primary {
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 1px 2px rgb(37 99 235 / 20%);
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: #1d4ed8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--danger {
|
||||||
|
background: #dc2626;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 1px 2px rgb(220 38 38 / 20%);
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: #b91c1c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--warn {
|
||||||
|
background: #d97706;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 1px 2px rgb(217 119 6 / 20%);
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: #b45309;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--success {
|
||||||
|
background: #059669;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 1px 2px rgb(5 150 105 / 20%);
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: #047857;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--ghost {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-btn-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 8px 10px;
|
flex-wrap: wrap;
|
||||||
margin-bottom: 8px;
|
}
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover,
|
/* ========================================================
|
||||||
&:focus-visible {
|
GRM / MRM & Documents Tab: Modern Executive Case Hub
|
||||||
border-color: #2563eb;
|
======================================================== */
|
||||||
outline: none;
|
.cases-tab {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.cases-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
@media (width <= 900px) {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgb(15 23 42 / 4%),
|
||||||
|
0 1px 2px rgb(15 23 42 / 2%);
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
box-shadow 0.2s ease,
|
||||||
|
border-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 12px rgb(15 23 42 / 6%);
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
background: linear-gradient(180deg, #fff 0%, #f8fafc 100%);
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__subtitle {
|
||||||
|
font-size: 10.5px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #64748b;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__body {
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 11.5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Interactive Case List Items */
|
||||||
|
.case-item-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
border-color: #2563eb;
|
||||||
|
background: #eff6ff;
|
||||||
|
outline: none;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 6px rgb(37 99 235 / 8%);
|
||||||
|
|
||||||
|
.case-view-link {
|
||||||
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
b {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 10.5px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-badge-pill {
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restorable-amt-pill {
|
||||||
|
padding: 1px 7px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: #ecfdf5;
|
||||||
|
color: #059669;
|
||||||
|
border: 1px solid #a7f3d0;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-view-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
color: #2563eb;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 10.5px;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action Panel inside Case Cards */
|
||||||
|
.case-action-panel {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&__head {
|
||||||
|
font-size: 10.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #475569;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.full-span {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 480px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-empty-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px dashed #cbd5e1;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 13px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Statement Tool Card */
|
||||||
|
.statement-card {
|
||||||
|
.statement-form-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
mat-form-field,
|
||||||
|
mifosx-i4c-date-field {
|
||||||
|
width: 190px;
|
||||||
|
flex: 1 1 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statement-btn {
|
||||||
|
height: 34px;
|
||||||
|
align-self: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-form-field,
|
||||||
|
mifosx-i4c-date-field {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
--mat-form-field-container-height: 34px;
|
||||||
|
--mat-form-field-container-vertical-padding: 6px;
|
||||||
|
--mat-form-field-container-text-size: 11.5px;
|
||||||
|
--mat-form-field-outlined-label-text-size: 11.5px;
|
||||||
|
--mat-select-trigger-text-size: 11.5px;
|
||||||
|
--mat-form-field-outlined-container-shape: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SOP & Risk tab: balanced 2-column executive dashboard layout */
|
||||||
|
.risk-tab {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
/* Sleek Executive Risk Notification Banner */
|
||||||
|
.risk-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: linear-gradient(135deg, #fff7f7 0%, #fef2f2 100%);
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
border-left: 4px solid #ef4444;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 4px rgb(239 68 68 / 6%);
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #dc2626;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1px 7px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
font-size: 9.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__desc {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #7f1d1d;
|
||||||
|
line-height: 1.45;
|
||||||
|
|
||||||
|
b {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #450a0a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2-Column Responsive Dashboard Grid (Columns don't stretch each other) */
|
||||||
|
.risk-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: start;
|
||||||
|
|
||||||
|
@media (width <= 960px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Executive Risk Cards */
|
||||||
|
.risk-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgb(15 23 42 / 4%),
|
||||||
|
0 1px 2px rgb(15 23 42 / 2%);
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
box-shadow 0.2s ease,
|
||||||
|
border-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 12px rgb(15 23 42 / 6%);
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
background: linear-gradient(180deg, #fff 0%, #f8fafc 100%);
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__body {
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 11.5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Key-Value Timeline Rows */
|
||||||
|
.risk-kv-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-kv-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f8fafc;
|
||||||
|
font-size: 11.5px;
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
color: #0f172a;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: right;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compact Action Boxes */
|
||||||
|
.risk-action-box {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&__head {
|
||||||
|
font-size: 10.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #475569;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
@media (width <= 480px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modern Material Form Field Overrides */
|
||||||
|
mat-form-field,
|
||||||
|
mifosx-i4c-date-field {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
--mat-form-field-container-height: 34px;
|
||||||
|
--mat-form-field-container-vertical-padding: 6px;
|
||||||
|
--mat-form-field-container-text-size: 11.5px;
|
||||||
|
--mat-form-field-outlined-label-text-size: 11.5px;
|
||||||
|
--mat-select-trigger-text-size: 11.5px;
|
||||||
|
--mat-form-field-outlined-container-shape: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Complaint info chip */
|
||||||
|
.complaint-chip-row {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.complaint-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: #fffbeb;
|
||||||
|
color: #92400e;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* List Items inside Cards */
|
||||||
|
.case-card-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
|
||||||
|
&__top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__source {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__date {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__reason {
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__remarks {
|
||||||
|
color: #64748b;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__val {
|
||||||
|
margin-left: auto;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transfer Alert Card */
|
||||||
|
.transfer-alert-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&__top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-type-tag {
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-acc {
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-amount {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #b91c1c;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
font-size: 10.5px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Channel status box */
|
||||||
|
.channel-status-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: #f0fdf4;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #166534;
|
||||||
|
font-size: 11px;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
color: #16a34a;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty Prompt */
|
||||||
|
.empty-prompt {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px dashed #cbd5e1;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.45;
|
||||||
|
|
||||||
|
b {
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.compliance-note {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: #eff6ff;
|
||||||
|
border: 1px solid #bfdbfe;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #1e40af;
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
color: #3b82f6;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-timeline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #475569;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-clean {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: #059669;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub {
|
.sub {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, OnInit, inject } from '@angular/core';
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
import { DatePipe, DecimalPipe } from '@angular/common';
|
import { DatePipe, DecimalPipe, LowerCasePipe, TitleCasePipe } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
@@ -20,6 +20,7 @@ import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
|
|||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||||
import { HOLD_REASON_TYPES, HoldActionRequest, I4cHold, I4cService, i4cErrorMessage, saveBlob } from '../i4c.service';
|
import { HOLD_REASON_TYPES, HoldActionRequest, I4cHold, I4cService, i4cErrorMessage, saveBlob } from '../i4c.service';
|
||||||
|
import { I4cDateFieldComponent } from '../i4c-date-field/i4c-date-field.component';
|
||||||
import { I4cDocumentsComponent } from '../i4c-documents/i4c-documents.component';
|
import { I4cDocumentsComponent } from '../i4c-documents/i4c-documents.component';
|
||||||
import { MRM_BASES } from '../i4c-restorations/i4c-restorations.component';
|
import { MRM_BASES } from '../i4c-restorations/i4c-restorations.component';
|
||||||
import { ALL_DATES, I4cDateRange, I4cDateRangeComponent } from '../i4c-date-range/i4c-date-range.component';
|
import { ALL_DATES, I4cDateRange, I4cDateRangeComponent } from '../i4c-date-range/i4c-date-range.component';
|
||||||
@@ -36,6 +37,8 @@ import { ALL_DATES, I4cDateRange, I4cDateRangeComponent } from '../i4c-date-rang
|
|||||||
imports: [
|
imports: [
|
||||||
DatePipe,
|
DatePipe,
|
||||||
DecimalPipe,
|
DecimalPipe,
|
||||||
|
LowerCasePipe,
|
||||||
|
TitleCasePipe,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
@@ -47,7 +50,8 @@ import { ALL_DATES, I4cDateRange, I4cDateRangeComponent } from '../i4c-date-rang
|
|||||||
MatPaginatorModule,
|
MatPaginatorModule,
|
||||||
I4cDateRangeComponent,
|
I4cDateRangeComponent,
|
||||||
I4cDocumentsComponent,
|
I4cDocumentsComponent,
|
||||||
FaIconComponent
|
FaIconComponent,
|
||||||
|
I4cDateFieldComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class I4cHoldsComponent implements OnInit {
|
export class I4cHoldsComponent implements OnInit {
|
||||||
@@ -96,7 +100,14 @@ export class I4cHoldsComponent implements OnInit {
|
|||||||
loading = false;
|
loading = false;
|
||||||
busy = false;
|
busy = false;
|
||||||
error = '';
|
error = '';
|
||||||
modalTab: 'overview' | 'txns' | 'intimation' | 'action' | 'cases' | 'history' = 'overview';
|
modalTab: 'overview' | 'txns' | 'intimation' | 'action' | 'cases' | 'risk' | 'history' = 'overview';
|
||||||
|
leaNotice = { sentOn: '', reference: '' };
|
||||||
|
extension = { until: '', reference: '' };
|
||||||
|
muleForm = { source: 'OFFICER', reason: '' };
|
||||||
|
muleReview = { outcome: 'CONFIRMED', remarks: '' };
|
||||||
|
fraudForm: Record<string, any> = {};
|
||||||
|
alertRemarks: Record<number, string> = {};
|
||||||
|
showCause = { sentOn: '', respondedOn: '', response: '', orderRef: '', orderOn: '' };
|
||||||
readonly mrmBases = MRM_BASES;
|
readonly mrmBases = MRM_BASES;
|
||||||
grievanceForm = { customerRequest: 'REMOVE_HOLD', customerReason: '' };
|
grievanceForm = { customerRequest: 'REMOVE_HOLD', customerReason: '' };
|
||||||
assessForm = { basis: 'ATTRIBUTION', restorableAmount: null as number | null, remarks: '' };
|
assessForm = { basis: 'ATTRIBUTION', restorableAmount: null as number | null, remarks: '' };
|
||||||
@@ -228,16 +239,19 @@ export class I4cHoldsComponent implements OnInit {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const list = [{ value: 'RELEASE', label: 'Release hold' }];
|
const list = [{ value: 'RELEASE', label: 'Release hold' }];
|
||||||
if (h.account_type === 'SAVINGS') {
|
list.push(
|
||||||
list.push(
|
h.debit_frozen
|
||||||
h.debit_frozen
|
? { value: 'UNFREEZE', label: 'Unfreeze debits, hold continues' }
|
||||||
? { value: 'UNFREEZE', label: 'Unfreeze debits, hold continues' }
|
: { value: 'FREEZE', label: 'Freeze all debits (on explicit LEA / court direction only)' }
|
||||||
: { value: 'FREEZE', label: 'Freeze all debits (on explicit LEA / court direction only)' }
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
if (h.debit_frozen) {
|
if (h.debit_frozen) {
|
||||||
list.push({ value: 'UNFREEZE_AND_RELEASE', label: 'Unfreeze and remove the hold' });
|
list.push({ value: 'UNFREEZE_AND_RELEASE', label: 'Unfreeze and remove the hold' });
|
||||||
}
|
}
|
||||||
|
list.push(
|
||||||
|
h.digital_suspended
|
||||||
|
? { value: 'RESTORE_DIGITAL', label: 'Restore digital banking (UPI, IMPS, NEFT, RTGS, ATM, cards)' }
|
||||||
|
: { value: 'SUSPEND_DIGITAL', label: 'Suspend digital banking (repeatedly reported account)' }
|
||||||
|
);
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,6 +321,90 @@ export class I4cHoldsComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** POST on /i4c/cases and reload the hold. */
|
||||||
|
private caseAction(path: string, body: Record<string, any>, done: string, put = false): void {
|
||||||
|
if (!this.selected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = this.selected.id;
|
||||||
|
this.busy = true;
|
||||||
|
(put ? this.i4c.casePut(path, body) : this.i4c.casePost(path, body)).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.busy = false;
|
||||||
|
this.snackBar.open(done, 'OK', { duration: 4000 });
|
||||||
|
this.select(id);
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
this.busy = false;
|
||||||
|
this.snackBar.open(i4cErrorMessage(e), 'Close', { duration: 7000 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
recordLeaNotice(): void {
|
||||||
|
this.caseAction('holds/' + this.selected!.id + '/lea-notice', this.leaNotice, 'Notice to the LEA recorded.');
|
||||||
|
}
|
||||||
|
|
||||||
|
recordExtension(): void {
|
||||||
|
this.caseAction('holds/' + this.selected!.id + '/extension', this.extension, 'Extension recorded.');
|
||||||
|
}
|
||||||
|
|
||||||
|
flagMule(): void {
|
||||||
|
this.caseAction('holds/' + this.selected!.id + '/mule-flag', this.muleForm, 'Account added to the mule register.');
|
||||||
|
}
|
||||||
|
|
||||||
|
reviewMule(flagId: number): void {
|
||||||
|
this.caseAction('mule-flags/' + flagId + '/review', this.muleReview, 'Review recorded.');
|
||||||
|
}
|
||||||
|
|
||||||
|
fraudStep(reportId: number, step: string, body: Record<string, any>): void {
|
||||||
|
this.caseAction('fraud-reports/' + reportId + '/' + step, body, 'Recorded.');
|
||||||
|
}
|
||||||
|
|
||||||
|
openFraudReport(): void {
|
||||||
|
this.caseAction(
|
||||||
|
'holds/' + this.selected!.id + '/fraud-report',
|
||||||
|
{},
|
||||||
|
'Fraud case opened: STR due in 7 days; send the show-cause notice next.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
editFraudReport(r: any): void {
|
||||||
|
this.fraudForm = {
|
||||||
|
id: r.id,
|
||||||
|
policeRef: r.police_ref || '',
|
||||||
|
policeReportedOn: r.police_reported_on || '',
|
||||||
|
fmrRef: r.fmr_ref || '',
|
||||||
|
fmrFiledOn: r.fmr_filed_on || '',
|
||||||
|
strRef: r.str_ref || '',
|
||||||
|
strFiledOn: r.str_filed_on || '',
|
||||||
|
remarks: r.remarks || ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
saveFraudReport(): void {
|
||||||
|
const { id, ...body } = this.fraudForm;
|
||||||
|
this.caseAction('fraud-reports/' + id, body, 'Fraud reporting updated.', true);
|
||||||
|
this.fraudForm = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
actionAlert(alertId: number, outcome: 'ACTIONED' | 'DISMISSED'): void {
|
||||||
|
this.caseAction(
|
||||||
|
'linked-alerts/' + alertId,
|
||||||
|
{ outcome, remarks: this.alertRemarks[alertId] || '' },
|
||||||
|
'Alert ' + outcome.toLowerCase() + '.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether fraud money from this account repaid a loan here (MHA SOP 9.1(v)). */
|
||||||
|
repaidLoan(hold: I4cHold): boolean {
|
||||||
|
return (hold.linkedAlerts || []).some((a) => a.kind === 'LOAN_REPAYMENT');
|
||||||
|
}
|
||||||
|
|
||||||
|
isPast(date: string | null | undefined): boolean {
|
||||||
|
return !!date && new Date(date).getTime() < Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
goTo(page: 'grievances' | 'restorations', id: number): void {
|
goTo(page: 'grievances' | 'restorations', id: number): void {
|
||||||
this.router.navigate(['/cfcfrms/i4c/' + page], { queryParams: { open: id } });
|
this.router.navigate(['/cfcfrms/i4c/' + page], { queryParams: { open: id } });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,16 @@
|
|||||||
<mat-option value="RESPONSE">RESPONSE only</mat-option>
|
<mat-option value="RESPONSE">RESPONSE only</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||||
|
<mat-label>Layer</mat-label>
|
||||||
|
<mat-select [(ngModel)]="outboxLayer" (selectionChange)="loadOutbox(true)">
|
||||||
|
<mat-option value="">All layers</mat-option>
|
||||||
|
<mat-option value="0">L0 · Victim</mat-option>
|
||||||
|
<mat-option value="1">L1 · First layer</mat-option>
|
||||||
|
<mat-option value="2">L2</mat-option>
|
||||||
|
<mat-option value="3">L3</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
<mifosx-i4c-date-range
|
<mifosx-i4c-date-range
|
||||||
label="Queued"
|
label="Queued"
|
||||||
[range]="outboxDates"
|
[range]="outboxDates"
|
||||||
@@ -96,6 +106,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Created At</th>
|
<th>Created At</th>
|
||||||
<th>Complaint Ack No.</th>
|
<th>Complaint Ack No.</th>
|
||||||
|
<th>Layer</th>
|
||||||
<th title="ACK: separate acknowledgement callback. RESPONSE: the Section 6 findings.">Type</th>
|
<th title="ACK: separate acknowledgement callback. RESPONSE: the Section 6 findings.">Type</th>
|
||||||
<th>Delivery Status</th>
|
<th>Delivery Status</th>
|
||||||
<th class="num">Attempts</th>
|
<th class="num">Attempts</th>
|
||||||
@@ -121,6 +132,12 @@
|
|||||||
{{ o.acknowledgement_no }}
|
{{ o.acknowledgement_no }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
class="layer-pill layer-pill--{{ o.layer === 0 ? 'victim' : o.layer === 1 ? 'first' : 'onward' }}"
|
||||||
|
>{{ layerLabel(o.layer) }}</span
|
||||||
|
>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="i4c-badge" [class.i4c-badge--blue]="o.message_type === 'ACK'">{{
|
<span class="i4c-badge" [class.i4c-badge--blue]="o.message_type === 'ACK'">{{
|
||||||
o.message_type === 'ACK' ? 'ACK' : 'RESPONSE'
|
o.message_type === 'ACK' ? 'ACK' : 'RESPONSE'
|
||||||
@@ -158,7 +175,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="10" class="i4c-empty">
|
<td colspan="11" class="i4c-empty">
|
||||||
<div class="empty-icon"><fa-icon icon="paper-plane"></fa-icon></div>
|
<div class="empty-icon"><fa-icon icon="paper-plane"></fa-icon></div>
|
||||||
<div>No responses in the outbox queue matching current filter.</div>
|
<div>No responses in the outbox queue matching current filter.</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -235,6 +252,16 @@
|
|||||||
placeholder="e.g. 00, 11"
|
placeholder="e.g. 00, 11"
|
||||||
/>
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||||
|
<mat-label>Layer</mat-label>
|
||||||
|
<mat-select [(ngModel)]="logLayer" (selectionChange)="loadLog(true)">
|
||||||
|
<mat-option value="">All layers</mat-option>
|
||||||
|
<mat-option value="0">L0 · Victim</mat-option>
|
||||||
|
<mat-option value="1">L1 · First layer</mat-option>
|
||||||
|
<mat-option value="2">L2</mat-option>
|
||||||
|
<mat-option value="3">L3</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
<mifosx-i4c-date-range
|
<mifosx-i4c-date-range
|
||||||
label="Call time"
|
label="Call time"
|
||||||
[range]="logDates"
|
[range]="logDates"
|
||||||
@@ -260,6 +287,7 @@
|
|||||||
<th>HTTP Status</th>
|
<th>HTTP Status</th>
|
||||||
<th>Response Code</th>
|
<th>Response Code</th>
|
||||||
<th>Ack No.</th>
|
<th>Ack No.</th>
|
||||||
|
<th>Layer</th>
|
||||||
<th class="num">Latency (ms)</th>
|
<th class="num">Latency (ms)</th>
|
||||||
<th>Caller IP</th>
|
<th>Caller IP</th>
|
||||||
<th>Error Details</th>
|
<th>Error Details</th>
|
||||||
@@ -290,6 +318,16 @@
|
|||||||
<td>
|
<td>
|
||||||
<b>{{ l.acknowledgement_no || '-' }}</b>
|
<b>{{ l.acknowledgement_no || '-' }}</b>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (l.layer !== null && l.layer !== undefined) {
|
||||||
|
<span
|
||||||
|
class="layer-pill layer-pill--{{ l.layer === 0 ? 'victim' : l.layer === 1 ? 'first' : 'onward' }}"
|
||||||
|
>{{ layerLabel(l.layer) }}</span
|
||||||
|
>
|
||||||
|
} @else {
|
||||||
|
-
|
||||||
|
}
|
||||||
|
</td>
|
||||||
<td class="num">{{ l.duration_ms }} ms</td>
|
<td class="num">{{ l.duration_ms }} ms</td>
|
||||||
<td>{{ l.client_ip || '-' }}</td>
|
<td>{{ l.client_ip || '-' }}</td>
|
||||||
<td [class.err-text]="isFailedCall(l)" [class.note-text]="!isFailedCall(l)">
|
<td [class.err-text]="isFailedCall(l)" [class.note-text]="!isFailedCall(l)">
|
||||||
@@ -298,7 +336,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="9" class="i4c-empty">
|
<td colspan="10" class="i4c-empty">
|
||||||
<div class="empty-icon"><fa-icon icon="history"></fa-icon></div>
|
<div class="empty-icon"><fa-icon icon="history"></fa-icon></div>
|
||||||
<div>No API calls recorded.</div>
|
<div>No API calls recorded.</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -189,3 +189,30 @@
|
|||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Layer of the complaint a row belongs to: victim (L0), first layer (L1), onward (L2+). */
|
||||||
|
.layer-pill {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 10.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
|
||||||
|
&--victim {
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--first {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #b45309;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--onward {
|
||||||
|
background: #f3e8ff;
|
||||||
|
color: #7e22ce;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export class I4cOutboxComponent implements OnInit {
|
|||||||
|
|
||||||
outboxStatus = '';
|
outboxStatus = '';
|
||||||
outboxType = '';
|
outboxType = '';
|
||||||
|
outboxLayer = '';
|
||||||
outboxSearch = '';
|
outboxSearch = '';
|
||||||
outboxDates: I4cDateRange = { ...ALL_DATES };
|
outboxDates: I4cDateRange = { ...ALL_DATES };
|
||||||
outboxTotal = 0;
|
outboxTotal = 0;
|
||||||
@@ -78,6 +79,7 @@ export class I4cOutboxComponent implements OnInit {
|
|||||||
logSearch = '';
|
logSearch = '';
|
||||||
logHttpStatus = '';
|
logHttpStatus = '';
|
||||||
logResponseCode = '';
|
logResponseCode = '';
|
||||||
|
logLayer = '';
|
||||||
logDates: I4cDateRange = { ...ALL_DATES };
|
logDates: I4cDateRange = { ...ALL_DATES };
|
||||||
logTotal = 0;
|
logTotal = 0;
|
||||||
logPageIndex = 0;
|
logPageIndex = 0;
|
||||||
@@ -123,12 +125,19 @@ export class I4cOutboxComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get outboxHasFilters(): boolean {
|
get outboxHasFilters(): boolean {
|
||||||
return !!(this.outboxStatus || this.outboxType || this.outboxSearch || this.outboxDates.preset !== 'ALL');
|
return !!(
|
||||||
|
this.outboxStatus ||
|
||||||
|
this.outboxType ||
|
||||||
|
this.outboxLayer ||
|
||||||
|
this.outboxSearch ||
|
||||||
|
this.outboxDates.preset !== 'ALL'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearOutboxFilters(): void {
|
clearOutboxFilters(): void {
|
||||||
this.outboxStatus = '';
|
this.outboxStatus = '';
|
||||||
this.outboxType = '';
|
this.outboxType = '';
|
||||||
|
this.outboxLayer = '';
|
||||||
this.outboxSearch = '';
|
this.outboxSearch = '';
|
||||||
this.outboxDates = { ...ALL_DATES };
|
this.outboxDates = { ...ALL_DATES };
|
||||||
this.loadOutbox(true);
|
this.loadOutbox(true);
|
||||||
@@ -152,6 +161,7 @@ export class I4cOutboxComponent implements OnInit {
|
|||||||
this.logSearch ||
|
this.logSearch ||
|
||||||
this.logHttpStatus ||
|
this.logHttpStatus ||
|
||||||
this.logResponseCode ||
|
this.logResponseCode ||
|
||||||
|
this.logLayer ||
|
||||||
this.logDates.preset !== 'ALL'
|
this.logDates.preset !== 'ALL'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -162,6 +172,7 @@ export class I4cOutboxComponent implements OnInit {
|
|||||||
this.logSearch = '';
|
this.logSearch = '';
|
||||||
this.logHttpStatus = '';
|
this.logHttpStatus = '';
|
||||||
this.logResponseCode = '';
|
this.logResponseCode = '';
|
||||||
|
this.logLayer = '';
|
||||||
this.logDates = { ...ALL_DATES };
|
this.logDates = { ...ALL_DATES };
|
||||||
this.loadLog(true);
|
this.loadLog(true);
|
||||||
}
|
}
|
||||||
@@ -177,6 +188,7 @@ export class I4cOutboxComponent implements OnInit {
|
|||||||
.outbox({
|
.outbox({
|
||||||
status: this.outboxStatus,
|
status: this.outboxStatus,
|
||||||
type: this.outboxType,
|
type: this.outboxType,
|
||||||
|
layer: this.outboxLayer,
|
||||||
search: this.outboxSearch.trim(),
|
search: this.outboxSearch.trim(),
|
||||||
fromDate: this.outboxDates.fromDate,
|
fromDate: this.outboxDates.fromDate,
|
||||||
toDate: this.outboxDates.toDate,
|
toDate: this.outboxDates.toDate,
|
||||||
@@ -207,6 +219,7 @@ export class I4cOutboxComponent implements OnInit {
|
|||||||
search: this.logSearch.trim(),
|
search: this.logSearch.trim(),
|
||||||
httpStatus: this.logHttpStatus,
|
httpStatus: this.logHttpStatus,
|
||||||
responseCode: this.logResponseCode.trim(),
|
responseCode: this.logResponseCode.trim(),
|
||||||
|
layer: this.logLayer,
|
||||||
fromDate: this.logDates.fromDate,
|
fromDate: this.logDates.fromDate,
|
||||||
toDate: this.logDates.toDate,
|
toDate: this.logDates.toDate,
|
||||||
offset: this.logPageIndex * this.logPageSize,
|
offset: this.logPageIndex * this.logPageSize,
|
||||||
@@ -300,6 +313,14 @@ export class I4cOutboxComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Layer 0 is the victim's own account, 1 the first-layer account, 2+ onward accounts in the trail. */
|
||||||
|
layerLabel(layer: number | null | undefined): string {
|
||||||
|
if (layer === null || layer === undefined) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
return layer === 0 ? 'L0 · Victim' : layer === 1 ? 'L1 · First layer' : 'L' + layer + ' · Onward';
|
||||||
|
}
|
||||||
|
|
||||||
ok(status: number | null): boolean {
|
ok(status: number | null): boolean {
|
||||||
return status !== null && status !== undefined && status >= 200 && status < 300;
|
return status !== null && status !== undefined && status >= 200 && status < 300;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ export class I4cRequestDetailComponent implements OnInit {
|
|||||||
return 'i4c-badge--green';
|
return 'i4c-badge--green';
|
||||||
case 'PARTIALLY_HELD':
|
case 'PARTIALLY_HELD':
|
||||||
case 'PROSPECTIVE_HOLD':
|
case 'PROSPECTIVE_HOLD':
|
||||||
|
case 'NOT_HELD_MOVED_ON':
|
||||||
return 'i4c-badge--amber';
|
return 'i4c-badge--amber';
|
||||||
case 'NOT_FOUND':
|
case 'NOT_FOUND':
|
||||||
case 'NO_MATCH':
|
case 'NO_MATCH':
|
||||||
|
|||||||
@@ -205,15 +205,16 @@
|
|||||||
[class.modal-tab--active]="modalTab === 'incidents'"
|
[class.modal-tab--active]="modalTab === 'incidents'"
|
||||||
(click)="modalTab = 'incidents'"
|
(click)="modalTab = 'incidents'"
|
||||||
>
|
>
|
||||||
<fa-icon icon="tasks"></fa-icon> Incidents ({{
|
<fa-icon icon="tasks"></fa-icon> Incidents
|
||||||
selectedComplaint.incidents?.length || selectedComplaint.incidents || 0
|
<span class="tab-count">{{ selectedComplaint.incidents?.length || selectedComplaint.incidents || 0 }}</span>
|
||||||
}})
|
|
||||||
</button>
|
</button>
|
||||||
<button class="modal-tab" [class.modal-tab--active]="modalTab === 'findings'" (click)="modalTab = 'findings'">
|
<button class="modal-tab" [class.modal-tab--active]="modalTab === 'findings'" (click)="modalTab = 'findings'">
|
||||||
<fa-icon icon="list-alt"></fa-icon> Section 6 Findings ({{ selectedComplaint.findings?.length || 0 }})
|
<fa-icon icon="list-alt"></fa-icon> Section 6 Findings
|
||||||
|
<span class="tab-count">{{ selectedComplaint.findings?.length || 0 }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="modal-tab" [class.modal-tab--active]="modalTab === 'outbox'" (click)="modalTab = 'outbox'">
|
<button class="modal-tab" [class.modal-tab--active]="modalTab === 'outbox'" (click)="modalTab = 'outbox'">
|
||||||
<fa-icon icon="paper-plane"></fa-icon> Outbox Responses ({{ selectedComplaint.outbox?.length || 0 }})
|
<fa-icon icon="paper-plane"></fa-icon> Outbox Responses
|
||||||
|
<span class="tab-count">{{ selectedComplaint.outbox?.length || 0 }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="modal-tab" [class.modal-tab--active]="modalTab === 'payloads'" (click)="modalTab = 'payloads'">
|
<button class="modal-tab" [class.modal-tab--active]="modalTab === 'payloads'" (click)="modalTab = 'payloads'">
|
||||||
<fa-icon icon="code"></fa-icon> Raw JSON
|
<fa-icon icon="code"></fa-icon> Raw JSON
|
||||||
|
|||||||
@@ -58,6 +58,10 @@
|
|||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
}
|
}
|
||||||
|
@if (role === 'BENEFICIARY_BANK') {
|
||||||
|
<mat-checkbox [(ngModel)]="overdueOnly" (change)="load(true)">Past 15-day deadline</mat-checkbox>
|
||||||
|
<mat-checkbox [(ngModel)]="contestedOnly" (change)="load(true)">Contested</mat-checkbox>
|
||||||
|
}
|
||||||
<mifosx-i4c-date-range label="Opened" [range]="dates" (rangeChange)="onDates($event)"></mifosx-i4c-date-range>
|
<mifosx-i4c-date-range label="Opened" [range]="dates" (rangeChange)="onDates($event)"></mifosx-i4c-date-range>
|
||||||
<button mat-stroked-button class="i4c-filter-bar__clear" (click)="clearFilters()" [disabled]="!hasFilters">
|
<button mat-stroked-button class="i4c-filter-bar__clear" (click)="clearFilters()" [disabled]="!hasFilters">
|
||||||
<fa-icon icon="times"></fa-icon> Clear filters
|
<fa-icon icon="times"></fa-icon> Clear filters
|
||||||
@@ -132,6 +136,19 @@
|
|||||||
@if (r.bank_action && r.bank_action !== 'PENDING') {
|
@if (r.bank_action && r.bank_action !== 'PENDING') {
|
||||||
<div class="sub">{{ r.bank_action }}</div>
|
<div class="sub">{{ r.bank_action }}</div>
|
||||||
}
|
}
|
||||||
|
@if (
|
||||||
|
r.compliance_due_on &&
|
||||||
|
r.role === 'BENEFICIARY_BANK' &&
|
||||||
|
r.status !== 'RESTORED' &&
|
||||||
|
r.status !== 'CLOSED'
|
||||||
|
) {
|
||||||
|
<div class="sub" [class.err-text]="r.compliance_overdue">
|
||||||
|
Credit by {{ r.compliance_due_on | date: 'dd MMM' }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (r.contested) {
|
||||||
|
<span class="i4c-badge i4c-badge--red">CONTESTED</span>
|
||||||
|
}
|
||||||
@if (r.transfer_error) {
|
@if (r.transfer_error) {
|
||||||
<div class="sub err-text">{{ r.transfer_error }}</div>
|
<div class="sub err-text">{{ r.transfer_error }}</div>
|
||||||
}
|
}
|
||||||
@@ -349,10 +366,14 @@
|
|||||||
<mat-label>BNSS 106 notice no.</mat-label>
|
<mat-label>BNSS 106 notice no.</mat-label>
|
||||||
<input matInput [(ngModel)]="request['bnssNoticeNo']" maxlength="100" />
|
<input matInput [(ngModel)]="request['bnssNoticeNo']" maxlength="100" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
<mifosx-i4c-date-field
|
||||||
<mat-label>Notice date</mat-label>
|
label="Notice date"
|
||||||
<input matInput type="date" [(ngModel)]="request['bnssNoticeDate']" />
|
[(value)]="request['bnssNoticeDate']"
|
||||||
</mat-form-field>
|
></mifosx-i4c-date-field>
|
||||||
|
<mifosx-i4c-date-field
|
||||||
|
label="Notice / order received (15-day clock)"
|
||||||
|
[(value)]="request['noticeReceivedOn']"
|
||||||
|
></mifosx-i4c-date-field>
|
||||||
<mat-checkbox [(ngModel)]="request['indemnityBondReceived']"
|
<mat-checkbox [(ngModel)]="request['indemnityBondReceived']"
|
||||||
>Indemnity bond received (Rs 10 stamp paper)</mat-checkbox
|
>Indemnity bond received (Rs 10 stamp paper)</mat-checkbox
|
||||||
>
|
>
|
||||||
@@ -362,18 +383,18 @@
|
|||||||
<mat-label>FIR no. (needed above ₹{{ r.firThreshold }})</mat-label>
|
<mat-label>FIR no. (needed above ₹{{ r.firThreshold }})</mat-label>
|
||||||
<input matInput [(ngModel)]="request['firNo']" maxlength="100" />
|
<input matInput [(ngModel)]="request['firNo']" maxlength="100" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
<mifosx-i4c-date-field
|
||||||
<mat-label>FIR date</mat-label>
|
label="FIR date"
|
||||||
<input matInput type="date" [(ngModel)]="request['firDate']" />
|
[(value)]="request['firDate']"
|
||||||
</mat-form-field>
|
></mifosx-i4c-date-field>
|
||||||
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||||
<mat-label>Court order no.</mat-label>
|
<mat-label>Court order no.</mat-label>
|
||||||
<input matInput [(ngModel)]="request['courtOrderNo']" maxlength="100" />
|
<input matInput [(ngModel)]="request['courtOrderNo']" maxlength="100" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
<mifosx-i4c-date-field
|
||||||
<mat-label>Court order date</mat-label>
|
label="Court order date"
|
||||||
<input matInput type="date" [(ngModel)]="request['courtOrderDate']" />
|
[(value)]="request['courtOrderDate']"
|
||||||
</mat-form-field>
|
></mifosx-i4c-date-field>
|
||||||
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||||
<mat-label>Court</mat-label>
|
<mat-label>Court</mat-label>
|
||||||
<input matInput [(ngModel)]="request['courtName']" maxlength="200" />
|
<input matInput [(ngModel)]="request['courtName']" maxlength="200" />
|
||||||
@@ -425,12 +446,18 @@
|
|||||||
₹{{ r.restorable_amount | number: '1.2-2' }} goes to {{ r.victim_name }} ({{
|
₹{{ r.restorable_amount | number: '1.2-2' }} goes to {{ r.victim_name }} ({{
|
||||||
r.victim_account_no
|
r.victim_account_no
|
||||||
}}{{ r.victim_ifsc ? ', ' + r.victim_ifsc : ', this bank' }}) once a second officer approves.
|
}}{{ r.victim_ifsc ? ', ' + r.victim_ifsc : ', this bank' }}) once a second officer approves.
|
||||||
@if (r.victim_ifsc && !r.poolConfigured) {
|
@if (r.victim_ifsc && !r.outwardReady) {
|
||||||
<b class="err-text"
|
<b class="err-text"
|
||||||
>No restoration pool account is configured, so an outward restoration cannot be
|
>Map the I4C_RESTORATION_PAYABLE and FUNDTRANSFER_OUTWARD_SUSPENSE GLs (Organisation >
|
||||||
approved.</b
|
Clearing GL Configuration) before an outward restoration can be approved.</b
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
@if (r.compliance_due_on) {
|
||||||
|
<div [class.err-text]="isPast(r.compliance_due_on)">
|
||||||
|
SOP: credit the victim by {{ r.compliance_due_on | date: 'dd MMM yyyy' }} (15 days from
|
||||||
|
the notice).
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
} @else if (action.action === 'NOT_COMPLIED' || action.action === 'REFER_BACK_LEA') {
|
} @else if (action.action === 'NOT_COMPLIED' || action.action === 'REFER_BACK_LEA') {
|
||||||
<div class="sub">
|
<div class="sub">
|
||||||
@@ -462,13 +489,12 @@
|
|||||||
@if (r.status === 'IN_POOL') {
|
@if (r.status === 'IN_POOL') {
|
||||||
<div class="step">
|
<div class="step">
|
||||||
<div class="step__title">
|
<div class="step__title">
|
||||||
₹{{ r.restored_amount | number: '1.2-2' }} is in the restoration pool
|
₹{{ r.restored_amount | number: '1.2-2' }} is held in the restoration payable
|
||||||
</div>
|
</div>
|
||||||
<div class="sub">
|
<div class="sub">
|
||||||
It left the suspect account on approval. The outward {{ r.transfer_channel }} to
|
It left the suspect account on approval (Dr suspect, Cr I4C Restoration Payable). The outward
|
||||||
{{ r.victim_account_no }} / {{ r.victim_ifsc }} has not gone through yet{{
|
{{ r.transfer_channel }} to {{ r.victim_account_no }} / {{ r.victim_ifsc }} has not gone through
|
||||||
r.transfer_error ? ': ' + r.transfer_error : '.'
|
yet{{ r.transfer_error ? ': ' + r.transfer_error : '.' }}
|
||||||
}}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="i4c-actions">
|
<div class="i4c-actions">
|
||||||
<button mat-flat-button color="primary" (click)="retryOutward()" [disabled]="busy">
|
<button mat-flat-button color="primary" (click)="retryOutward()" [disabled]="busy">
|
||||||
@@ -503,8 +529,214 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if ((r.status === 'RESTORED' || r.status === 'CLOSED') && r.restored_at) {
|
||||||
|
<div class="step">
|
||||||
|
<div class="step__title">
|
||||||
|
Suspect's challenge (SOP: within 15 days of the refund, until
|
||||||
|
{{ r.challenge_until | date: 'dd MMM yyyy' }})
|
||||||
|
</div>
|
||||||
|
@if (r.contested) {
|
||||||
|
<div class="i4c-note">
|
||||||
|
Contested on {{ r.contested_on | date: 'dd MMM yyyy' }}: {{ r.contest_remarks }}. The
|
||||||
|
investigation continues; the bank and the account holder cooperate with the IO.
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="step__row">
|
||||||
|
<mifosx-i4c-date-field
|
||||||
|
label="Contested on"
|
||||||
|
[(value)]="contest.contestedOn"
|
||||||
|
></mifosx-i4c-date-field>
|
||||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="step__wide">
|
||||||
|
<mat-label>Details</mat-label>
|
||||||
|
<input matInput [(ngModel)]="contest.remarks" maxlength="1000" />
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="i4c-actions">
|
||||||
|
<button mat-stroked-button (click)="saveContest()" [disabled]="busy">Record challenge</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
} @else {
|
} @else {
|
||||||
@if (r.status !== 'CLOSED') {
|
@if (r.status !== 'CLOSED') {
|
||||||
|
<div class="step">
|
||||||
|
<div class="step__title">
|
||||||
|
Customer's claim on the bank (RBI customer protection in fraudulent EBTs)
|
||||||
|
</div>
|
||||||
|
@if (r.liability_category) {
|
||||||
|
<div class="sub">
|
||||||
|
<b>{{
|
||||||
|
r.liability_regime === 'RBI_2026'
|
||||||
|
? 'RBI 2026 directions (RBI/2026-27/172)'
|
||||||
|
: 'RBI 2017 framework'
|
||||||
|
}}</b>
|
||||||
|
· {{ label(liabilityCategories, r.liability_category) }} · transaction
|
||||||
|
{{ r.txn_date | date: 'dd MMM yyyy' }} · reported
|
||||||
|
{{ r.customer_reported_on | date: 'dd MMM yyyy'
|
||||||
|
}}{{ r.cross_border ? ' · cross-border' : '' }}
|
||||||
|
</div>
|
||||||
|
<div class="sub">{{ r.liability_note }}</div>
|
||||||
|
<div class="sub">
|
||||||
|
@if (r.shadow_credit_due_on) {
|
||||||
|
Shadow reversal due
|
||||||
|
<b
|
||||||
|
[class.err-text]="isPast(r.shadow_credit_due_on) && r.shadow_credit_status !== 'CREDITED'"
|
||||||
|
>{{ r.shadow_credit_due_on | date: 'dd MMM yyyy' }}</b
|
||||||
|
>
|
||||||
|
·
|
||||||
|
}
|
||||||
|
Resolve by
|
||||||
|
<b [class.err-text]="isPast(r.resolution_due_on)">{{
|
||||||
|
r.resolution_due_on | date: 'dd MMM yyyy'
|
||||||
|
}}</b>
|
||||||
|
({{
|
||||||
|
r.liability_regime === 'RBI_2026'
|
||||||
|
? (r.cross_border ? '60' : '45') + ' calendar days'
|
||||||
|
: '90 days'
|
||||||
|
}})
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="step__row">
|
||||||
|
<mifosx-i4c-date-field
|
||||||
|
label="Transaction date"
|
||||||
|
[(value)]="liability.txnDate"
|
||||||
|
></mifosx-i4c-date-field>
|
||||||
|
<mifosx-i4c-date-field
|
||||||
|
label="Reported to the bank on"
|
||||||
|
[(value)]="liability.reportedOn"
|
||||||
|
></mifosx-i4c-date-field>
|
||||||
|
<mifosx-i4c-date-field
|
||||||
|
label="Reported to NCRP / 1930 on"
|
||||||
|
[(value)]="liability.ncrpReportedOn"
|
||||||
|
></mifosx-i4c-date-field>
|
||||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="step__wide">
|
||||||
|
<mat-label>Caused by</mat-label>
|
||||||
|
<mat-select [(ngModel)]="liability.category">
|
||||||
|
@for (c of liabilityCategories; track c.value) {
|
||||||
|
<mat-option [value]="c.value">{{ c.label }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||||
|
<mat-label>Gross loss (₹)</mat-label>
|
||||||
|
<input matInput type="number" min="0" [(ngModel)]="liability.grossLoss" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||||
|
<mat-label>Recovered so far (₹)</mat-label>
|
||||||
|
<input matInput type="number" min="0" [(ngModel)]="liability.recoveredAmount" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-checkbox [(ngModel)]="liability.crossBorder">Cross-border</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="i4c-actions">
|
||||||
|
<button mat-stroked-button (click)="saveLiability()" [disabled]="busy">Assess liability</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (r.liability_category === 'CUSTOMER_NEGLIGENCE') {
|
||||||
|
<div class="sub">
|
||||||
|
<b>Compensation (RBI 2026, para 65.1T):</b>
|
||||||
|
@if (r.comp_eligible) {
|
||||||
|
₹{{ r.comp_amount | number: '1.2-2' }} ({{ r.comp_reason }}) · RBI ₹{{
|
||||||
|
r.comp_share_rbi | number: '1.2-2'
|
||||||
|
}}
|
||||||
|
· this bank ₹{{ r.comp_share_own | number: '1.2-2' }} · beneficiary bank ₹{{
|
||||||
|
r.comp_share_beneficiary | number: '1.2-2'
|
||||||
|
}}
|
||||||
|
} @else {
|
||||||
|
not eligible: {{ r.comp_reason }}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (r.comp_eligible && !r.comp_status) {
|
||||||
|
<button mat-stroked-button color="primary" (click)="applyCompensation()" [disabled]="busy">
|
||||||
|
Customer applied (pay within 5 days)
|
||||||
|
</button>
|
||||||
|
} @else if (r.comp_status === 'APPLIED') {
|
||||||
|
<div class="sub" [class.err-text]="isPast(r.comp_due_on)">
|
||||||
|
Applied {{ r.comp_applied_on | date: 'dd MMM yyyy' }}; pay by
|
||||||
|
{{ r.comp_due_on | date: 'dd MMM yyyy' }}.
|
||||||
|
</div>
|
||||||
|
<div class="i4c-actions">
|
||||||
|
<button mat-flat-button color="primary" (click)="payCompensation(true)" [disabled]="busy">
|
||||||
|
Pay compensation (checker)
|
||||||
|
</button>
|
||||||
|
<button mat-button color="warn" (click)="payCompensation(false)" [disabled]="busy">
|
||||||
|
Reject
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
} @else if (r.comp_status) {
|
||||||
|
<div class="sub">
|
||||||
|
Compensation {{ r.comp_status }} {{ r.comp_paid_on | date: 'dd MMM yyyy' }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (r.liability_category && r.liability_category !== 'CUSTOMER_NEGLIGENCE') {
|
||||||
|
@if (!r.shadow_credit_status || r.shadow_credit_status === 'REJECTED') {
|
||||||
|
<div class="step__row">
|
||||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||||
|
<mat-label>Shadow reversal (₹)</mat-label>
|
||||||
|
<input matInput type="number" min="0" [(ngModel)]="shadowAmount" />
|
||||||
|
</mat-form-field>
|
||||||
|
<button
|
||||||
|
mat-flat-button
|
||||||
|
color="warn"
|
||||||
|
(click)="requestShadowCredit()"
|
||||||
|
[disabled]="busy || !shadowAmount"
|
||||||
|
>
|
||||||
|
Request shadow reversal (maker)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="sub">
|
||||||
|
A provisional credit the customer cannot use until the case is decided (a lien is placed on
|
||||||
|
it).
|
||||||
|
</div>
|
||||||
|
} @else if (r.shadow_credit_status === 'PENDING') {
|
||||||
|
<div class="sub">
|
||||||
|
Shadow reversal of ₹{{ r.shadow_credit_amount | number: '1.2-2' }} requested by
|
||||||
|
{{ r.shadow_credit_requested_by }}; a different officer approves it.
|
||||||
|
</div>
|
||||||
|
<div class="i4c-actions">
|
||||||
|
<button
|
||||||
|
mat-flat-button
|
||||||
|
color="primary"
|
||||||
|
(click)="decideShadowCredit(true)"
|
||||||
|
[disabled]="busy"
|
||||||
|
>
|
||||||
|
Approve and credit (checker)
|
||||||
|
</button>
|
||||||
|
<button mat-button color="warn" (click)="decideShadowCredit(false)" [disabled]="busy">
|
||||||
|
Reject
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="sub">
|
||||||
|
Shadow reversal ₹{{ r.shadow_credit_amount | number: '1.2-2' }} posted
|
||||||
|
{{ r.shadow_credited_at | date: 'dd MMM yyyy' }} by {{ r.shadow_credited_by }}
|
||||||
|
{{
|
||||||
|
r.shadow_finalised_at
|
||||||
|
? '· final ' + (r.shadow_finalised_at | date: 'dd MMM yyyy')
|
||||||
|
: '· provisional (lien on it)'
|
||||||
|
}}
|
||||||
|
@if (r.regularised_at) {
|
||||||
|
· regularised ₹{{ r.regularised_amount | number: '1.2-2' }} on
|
||||||
|
{{ r.regularised_at | date: 'dd MMM yyyy' }}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="i4c-actions">
|
||||||
|
@if (!r.shadow_finalised_at && !r.regularised_at) {
|
||||||
|
<button mat-stroked-button color="primary" (click)="finaliseShadow()" [disabled]="busy">
|
||||||
|
Zero liability established: make final
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
@if (!r.regularised_at && r.received_amount) {
|
||||||
|
<button mat-stroked-button (click)="regularise()" [disabled]="busy">
|
||||||
|
Recover it from the restored money
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<div class="step">
|
<div class="step">
|
||||||
<div class="step__title">Restored money received for our customer</div>
|
<div class="step__title">Restored money received for our customer</div>
|
||||||
<div class="step__row">
|
<div class="step__row">
|
||||||
@@ -516,10 +748,10 @@
|
|||||||
<mat-label>UTR / transaction id *</mat-label>
|
<mat-label>UTR / transaction id *</mat-label>
|
||||||
<input matInput [(ngModel)]="receipt.utr" maxlength="40" />
|
<input matInput [(ngModel)]="receipt.utr" maxlength="40" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
<mifosx-i4c-date-field
|
||||||
<mat-label>Credited on *</mat-label>
|
label="Credited on *"
|
||||||
<input matInput type="date" [(ngModel)]="receipt.receivedOn" />
|
[(value)]="receipt.receivedOn"
|
||||||
</mat-form-field>
|
></mifosx-i4c-date-field>
|
||||||
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||||
<mat-label>Credited to account</mat-label>
|
<mat-label>Credited to account</mat-label>
|
||||||
<input matInput [(ngModel)]="receipt.accountNo" maxlength="40" />
|
<input matInput [(ngModel)]="receipt.accountNo" maxlength="40" />
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
|
|||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||||
import { I4cService, i4cErrorMessage } from '../i4c.service';
|
import { I4cService, i4cErrorMessage } from '../i4c.service';
|
||||||
|
import { I4cDateFieldComponent } from '../i4c-date-field/i4c-date-field.component';
|
||||||
import { ALL_DATES, I4cDateRange, I4cDateRangeComponent } from '../i4c-date-range/i4c-date-range.component';
|
import { ALL_DATES, I4cDateRange, I4cDateRangeComponent } from '../i4c-date-range/i4c-date-range.component';
|
||||||
import { I4cDocumentsComponent } from '../i4c-documents/i4c-documents.component';
|
import { I4cDocumentsComponent } from '../i4c-documents/i4c-documents.component';
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ export const MRM_STATUSES: { value: string; label: string }[] = [
|
|||||||
{ value: 'ASSESSED', label: 'Assessed' },
|
{ value: 'ASSESSED', label: 'Assessed' },
|
||||||
{ value: 'REQUESTED', label: 'Victim request received' },
|
{ value: 'REQUESTED', label: 'Victim request received' },
|
||||||
{ value: 'AWAITING_APPROVAL', label: 'Awaiting checker' },
|
{ value: 'AWAITING_APPROVAL', label: 'Awaiting checker' },
|
||||||
{ value: 'IN_POOL', label: 'In restoration pool' },
|
{ value: 'IN_POOL', label: 'Held in restoration payable' },
|
||||||
{ value: 'TRANSFER_PENDING', label: 'Outward transfer pending' },
|
{ value: 'TRANSFER_PENDING', label: 'Outward transfer pending' },
|
||||||
{ value: 'RESTORED', label: 'Restored' },
|
{ value: 'RESTORED', label: 'Restored' },
|
||||||
{ value: 'AWAITED', label: 'Victim bank: awaited' },
|
{ value: 'AWAITED', label: 'Victim bank: awaited' },
|
||||||
@@ -89,7 +90,8 @@ const VICTIM_STATUSES = [
|
|||||||
MatPaginatorModule,
|
MatPaginatorModule,
|
||||||
FaIconComponent,
|
FaIconComponent,
|
||||||
I4cDateRangeComponent,
|
I4cDateRangeComponent,
|
||||||
I4cDocumentsComponent
|
I4cDocumentsComponent,
|
||||||
|
I4cDateFieldComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class I4cRestorationsComponent implements OnInit {
|
export class I4cRestorationsComponent implements OnInit {
|
||||||
@@ -112,6 +114,8 @@ export class I4cRestorationsComponent implements OnInit {
|
|||||||
role = 'BENEFICIARY_BANK';
|
role = 'BENEFICIARY_BANK';
|
||||||
status = 'OPEN';
|
status = 'OPEN';
|
||||||
bankAction = '';
|
bankAction = '';
|
||||||
|
overdueOnly = false;
|
||||||
|
contestedOnly = false;
|
||||||
search = '';
|
search = '';
|
||||||
dates: I4cDateRange = { ...ALL_DATES };
|
dates: I4cDateRange = { ...ALL_DATES };
|
||||||
rows: any[] = [];
|
rows: any[] = [];
|
||||||
@@ -151,6 +155,8 @@ export class I4cRestorationsComponent implements OnInit {
|
|||||||
role: this.role,
|
role: this.role,
|
||||||
status: this.status,
|
status: this.status,
|
||||||
bankAction: this.bankAction,
|
bankAction: this.bankAction,
|
||||||
|
overdue: this.overdueOnly || null,
|
||||||
|
contested: this.contestedOnly || null,
|
||||||
search: this.search.trim(),
|
search: this.search.trim(),
|
||||||
fromDate: this.dates.fromDate,
|
fromDate: this.dates.fromDate,
|
||||||
toDate: this.dates.toDate,
|
toDate: this.dates.toDate,
|
||||||
@@ -187,12 +193,21 @@ export class I4cRestorationsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get hasFilters(): boolean {
|
get hasFilters(): boolean {
|
||||||
return !!(this.status !== 'OPEN' || this.bankAction || this.search || this.dates.preset !== 'ALL');
|
return !!(
|
||||||
|
this.status !== 'OPEN' ||
|
||||||
|
this.bankAction ||
|
||||||
|
this.overdueOnly ||
|
||||||
|
this.contestedOnly ||
|
||||||
|
this.search ||
|
||||||
|
this.dates.preset !== 'ALL'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearFilters(): void {
|
clearFilters(): void {
|
||||||
this.status = 'OPEN';
|
this.status = 'OPEN';
|
||||||
this.bankAction = '';
|
this.bankAction = '';
|
||||||
|
this.overdueOnly = false;
|
||||||
|
this.contestedOnly = false;
|
||||||
this.search = '';
|
this.search = '';
|
||||||
this.dates = { ...ALL_DATES };
|
this.dates = { ...ALL_DATES };
|
||||||
this.load(true);
|
this.load(true);
|
||||||
@@ -242,6 +257,7 @@ export class I4cRestorationsComponent implements OnInit {
|
|||||||
courtName: r.court_name || '',
|
courtName: r.court_name || '',
|
||||||
bnssNoticeNo: r.bnss_notice_no || '',
|
bnssNoticeNo: r.bnss_notice_no || '',
|
||||||
bnssNoticeDate: r.bnss_notice_date || '',
|
bnssNoticeDate: r.bnss_notice_date || '',
|
||||||
|
noticeReceivedOn: r.notice_received_on || '',
|
||||||
indemnityBondReceived: !!r.indemnity_bond_received
|
indemnityBondReceived: !!r.indemnity_bond_received
|
||||||
};
|
};
|
||||||
this.action = { action: 'COMPLIED', reason: '' };
|
this.action = { action: 'COMPLIED', reason: '' };
|
||||||
@@ -362,6 +378,83 @@ export class I4cRestorationsComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly liabilityCategories = [
|
||||||
|
{ value: 'BANK_NEGLIGENCE', label: 'Bank negligence / deficiency' },
|
||||||
|
{ value: 'THIRD_PARTY_BREACH', label: 'Third-party breach (neither bank nor customer)' },
|
||||||
|
{ value: 'CUSTOMER_NEGLIGENCE', label: 'Customer negligence (shared OTP / PIN, coerced)' }
|
||||||
|
];
|
||||||
|
contest = { contestedOn: '', remarks: '' };
|
||||||
|
liability = {
|
||||||
|
txnDate: '',
|
||||||
|
reportedOn: '',
|
||||||
|
ncrpReportedOn: '',
|
||||||
|
category: 'THIRD_PARTY_BREACH',
|
||||||
|
crossBorder: false,
|
||||||
|
grossLoss: null as number | null,
|
||||||
|
recoveredAmount: null as number | null
|
||||||
|
};
|
||||||
|
shadowAmount: number | null = null;
|
||||||
|
|
||||||
|
isPast(date: string | null | undefined): boolean {
|
||||||
|
return !!date && new Date(date).getTime() < Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
saveContest(): void {
|
||||||
|
this.run(
|
||||||
|
this.i4c.restorationStep(this.selected.id, 'contest', this.contest),
|
||||||
|
'Challenge recorded; the investigation continues.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveLiability(): void {
|
||||||
|
this.run(
|
||||||
|
this.i4c.restorationStep(this.selected.id, 'liability', this.liability),
|
||||||
|
'Liability assessed under the RBI framework for that date.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestShadowCredit(): void {
|
||||||
|
this.run(
|
||||||
|
this.i4c.restorationStep(this.selected.id, 'shadow-credit', { amount: this.shadowAmount }),
|
||||||
|
'Shadow credit sent for approval.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
decideShadowCredit(approve: boolean): void {
|
||||||
|
this.run(
|
||||||
|
this.i4c.restorationStep(this.selected.id, 'shadow-credit/decision', { approve }),
|
||||||
|
approve ? 'Shadow credit posted to the customer.' : 'Shadow credit rejected.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
finaliseShadow(): void {
|
||||||
|
this.run(
|
||||||
|
this.i4c.restorationStep(this.selected.id, 'shadow-credit/finalise'),
|
||||||
|
'Shadow reversal made final; the customer can use it.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyCompensation(): void {
|
||||||
|
this.run(
|
||||||
|
this.i4c.restorationStep(this.selected.id, 'compensation/apply'),
|
||||||
|
'Application recorded; pay within 5 calendar days.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
payCompensation(approve: boolean): void {
|
||||||
|
this.run(
|
||||||
|
this.i4c.restorationStep(this.selected.id, 'compensation/decision', { approve }),
|
||||||
|
approve ? 'Compensation credited to the customer.' : 'Compensation rejected.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
regularise(): void {
|
||||||
|
this.run(
|
||||||
|
this.i4c.restorationStep(this.selected.id, 'regularise'),
|
||||||
|
'Shadow credit recovered from the restored money.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
loadProRata(): void {
|
loadProRata(): void {
|
||||||
this.i4c.proRata(this.selected.hold_account_no).subscribe({
|
this.i4c.proRata(this.selected.hold_account_no).subscribe({
|
||||||
next: (p) => {
|
next: (p) => {
|
||||||
|
|||||||
@@ -0,0 +1,616 @@
|
|||||||
|
<!--
|
||||||
|
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">
|
||||||
|
<div class="i4c-toolbar">
|
||||||
|
<div class="i4c-filter-chips">
|
||||||
|
@for (t of tabs; track t.value) {
|
||||||
|
<button class="filter-chip" [class.filter-chip--active]="tab === t.value" (click)="setTab(t.value)">
|
||||||
|
<fa-icon [icon]="t.icon"></fa-icon> <span>{{ t.label }}</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<span class="i4c-toolbar__spacer"></span>
|
||||||
|
@if (tab === 'fraud-reports') {
|
||||||
|
<button mat-stroked-button (click)="downloadFraudCsv()">
|
||||||
|
<fa-icon icon="file-csv"></fa-icon> Download (CSV)
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
<span class="count-tag">{{ total }} records</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="i4c-filter-bar">
|
||||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="search-input">
|
||||||
|
<mat-label>Account / customer / reference</mat-label>
|
||||||
|
<input matInput [(ngModel)]="search" (keyup.enter)="load(true)" (blur)="load(true)" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||||
|
<mat-label>Status</mat-label>
|
||||||
|
<mat-select [(ngModel)]="status" (selectionChange)="load(true)">
|
||||||
|
<mat-option value="">Any</mat-option>
|
||||||
|
@for (s of statuses; track s) {
|
||||||
|
<mat-option [value]="s">{{ s }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
@if (tab === 'fraud-reports') {
|
||||||
|
<mat-checkbox [(ngModel)]="overdueOnly" (change)="load(true)">FMR / STR overdue</mat-checkbox>
|
||||||
|
}
|
||||||
|
@if (tab === 'bank-holds') {
|
||||||
|
<mat-checkbox [(ngModel)]="overdueOnly" (change)="load(true)">Past a deadline</mat-checkbox>
|
||||||
|
<button mat-flat-button color="warn" (click)="showPlace = !showPlace">
|
||||||
|
<fa-icon icon="lock"></fa-icon> Place a hold
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
<mifosx-i4c-date-range label="Date" [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>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (tab === 'exclusions') {
|
||||||
|
<div class="i4c-card">
|
||||||
|
<div class="i4c-card__body">
|
||||||
|
<div class="i4c-form">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Account no.</mat-label>
|
||||||
|
<input matInput [(ngModel)]="exclusionForm.accountNo" maxlength="40" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Kind</mat-label>
|
||||||
|
<mat-select [(ngModel)]="exclusionForm.kind">
|
||||||
|
<mat-option value="NODAL">Nodal account</mat-option>
|
||||||
|
<mat-option value="POOL">Pool account</mat-option>
|
||||||
|
<mat-option value="ESCROW">Escrow account</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="wide">
|
||||||
|
<mat-label>What it is (merchant nodal, aggregator pool...)</mat-label>
|
||||||
|
<input matInput [(ngModel)]="exclusionForm.reason" maxlength="500" />
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<button mat-flat-button color="primary" (click)="addExclusion()" [disabled]="busy">
|
||||||
|
Exclude from I4C holds
|
||||||
|
</button>
|
||||||
|
<div class="sub">
|
||||||
|
MHA SOP: holds are for first-layer mule accounts, not nodal, pool or escrow accounts. A complaint against an
|
||||||
|
excluded account is answered "hold not placed" and traced through the merchant settlement.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (tab === 'bank-holds' && showPlace) {
|
||||||
|
<div class="i4c-card">
|
||||||
|
<div class="i4c-card__head"><fa-icon icon="lock"></fa-icon> Hold a suspected mule transaction</div>
|
||||||
|
<div class="i4c-card__body">
|
||||||
|
<div class="i4c-form">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Account no.</mat-label>
|
||||||
|
<input matInput [(ngModel)]="bankHoldForm.accountNo" maxlength="40" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Hold</mat-label>
|
||||||
|
<mat-select [(ngModel)]="bankHoldForm.scope">
|
||||||
|
<mat-option value="TRANSACTION">The flagged transaction's amount</mat-option>
|
||||||
|
<mat-option value="ACCOUNT">All debits on the account (last resort)</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Why flagged</mat-label>
|
||||||
|
<mat-select [(ngModel)]="bankHoldForm.trigger">
|
||||||
|
<mat-option value="PROFILE_MISMATCH">Does not match the customer's profile</mat-option>
|
||||||
|
<mat-option value="LINKED_TO_REPORTED_ACCOUNT">Linked to an account reported for fraud</mat-option>
|
||||||
|
<mat-option value="OTHER">Other</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Transaction ref</mat-label>
|
||||||
|
<input matInput [(ngModel)]="bankHoldForm.txnRef" maxlength="100" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Amount (₹, at least 1,000)</mat-label>
|
||||||
|
<input matInput type="number" min="1000" [(ngModel)]="bankHoldForm.amount" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="wide">
|
||||||
|
<mat-label>Reason (given to the customer)</mat-label>
|
||||||
|
<input matInput [(ngModel)]="bankHoldForm.reason" maxlength="1000" />
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<button mat-flat-button color="warn" (click)="placeBankHold()" [disabled]="busy">Place hold now</button>
|
||||||
|
<div class="sub">
|
||||||
|
RBI draft (11.09.2026, effective 01.04.2027 if finalised): customer 20 days to explain; bank decides within 10
|
||||||
|
days of it (30 days from the hold without one); police referral through NCRP, 30 days to instruct; never more
|
||||||
|
than 60 days without an LEA instruction. The Hold Review job lifts holds at the end of those periods.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="i4c-card">
|
||||||
|
<div class="i4c-card__head">
|
||||||
|
<fa-icon icon="user-shield"></fa-icon>
|
||||||
|
@switch (tab) {
|
||||||
|
@case ('mule-flags') {
|
||||||
|
<span>Mule-account register</span>
|
||||||
|
<span class="head-hint"
|
||||||
|
>Accounts in two or more complaints are flagged automatically; review each with EDD or the LEA.</span
|
||||||
|
>
|
||||||
|
}
|
||||||
|
@case ('fraud-reports') {
|
||||||
|
<span>Fraud reporting</span>
|
||||||
|
<span class="head-hint"
|
||||||
|
>Police / LEA, RBI fraud monitoring return (14 days), FIU-IND STR (7 days) from classification.</span
|
||||||
|
>
|
||||||
|
}
|
||||||
|
@case ('linked-alerts') {
|
||||||
|
<span>Fraud money that moved on inside this bank</span>
|
||||||
|
<span class="head-hint"
|
||||||
|
>Loan repayments (SOP 9.1(v): verify, hold, tell the linked account) and transfers to another account here
|
||||||
|
(the next layer is with us). Found by the I4C Hold Review job.</span
|
||||||
|
>
|
||||||
|
}
|
||||||
|
@case ('exclusions') {
|
||||||
|
<span>Nodal / pool / escrow accounts</span>
|
||||||
|
<span class="head-hint"
|
||||||
|
>Disputed amount is held; no prospective hold on future credits and normally no suspension (MHA SOP
|
||||||
|
9.1(iii)).</span
|
||||||
|
>
|
||||||
|
}
|
||||||
|
@case ('bank-holds') {
|
||||||
|
<span>Bank-initiated debit holds</span>
|
||||||
|
<span class="head-hint">Suspected mule transactions the bank holds itself (RBI draft mule SOP).</span>
|
||||||
|
}
|
||||||
|
@case ('channel-blocks') {
|
||||||
|
<span>Digital-channel suspensions</span>
|
||||||
|
<span class="head-hint"
|
||||||
|
>Placed and lifted through hold requests (maker-checker). CBS fund transfers refuse them.</span
|
||||||
|
>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="i4c-table-wrap">
|
||||||
|
<table class="i4c-table">
|
||||||
|
<thead>
|
||||||
|
@switch (tab) {
|
||||||
|
@case ('mule-flags') {
|
||||||
|
<tr>
|
||||||
|
<th>Flagged</th>
|
||||||
|
<th>Account / Customer</th>
|
||||||
|
<th>Source</th>
|
||||||
|
<th class="num">Complaints</th>
|
||||||
|
<th>Reason</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@case ('fraud-reports') {
|
||||||
|
<tr>
|
||||||
|
<th>Classified</th>
|
||||||
|
<th>Account / Customer</th>
|
||||||
|
<th class="num">Amount (₹)</th>
|
||||||
|
<th>Police / LEA</th>
|
||||||
|
<th>RBI FMR</th>
|
||||||
|
<th>FIU-IND STR</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@case ('linked-alerts') {
|
||||||
|
<tr>
|
||||||
|
<th>Moved on</th>
|
||||||
|
<th>From (held account)</th>
|
||||||
|
<th>Moved to</th>
|
||||||
|
<th class="num">Amount (₹)</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Remarks</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@case ('exclusions') {
|
||||||
|
<tr>
|
||||||
|
<th>Added</th>
|
||||||
|
<th>Account</th>
|
||||||
|
<th>Kind</th>
|
||||||
|
<th>What it is</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@case ('bank-holds') {
|
||||||
|
<tr>
|
||||||
|
<th>Placed</th>
|
||||||
|
<th>Account / Customer</th>
|
||||||
|
<th>Hold</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Next date</th>
|
||||||
|
<th>Customer told</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@case ('channel-blocks') {
|
||||||
|
<tr>
|
||||||
|
<th>Placed</th>
|
||||||
|
<th>Account</th>
|
||||||
|
<th>Channels</th>
|
||||||
|
<th>Reason</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Lifted</th>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (r of rows; track r.id) {
|
||||||
|
@switch (tab) {
|
||||||
|
@case ('mule-flags') {
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{ r.flagged_at | date: 'dd MMM yyyy' }}
|
||||||
|
<div class="sub">{{ r.flagged_by }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<b>{{ r.account_no }}</b>
|
||||||
|
<div class="sub">{{ r.client_name }} · {{ r.account_type }}</div>
|
||||||
|
</td>
|
||||||
|
<td>{{ r.source }}</td>
|
||||||
|
<td class="num">{{ r.reports_count }}</td>
|
||||||
|
<td class="wrap">
|
||||||
|
{{ r.reason }}
|
||||||
|
<div class="sub">{{ r.review_remarks }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
class="i4c-badge"
|
||||||
|
[class.i4c-badge--red]="r.status === 'CONFIRMED'"
|
||||||
|
[class.i4c-badge--amber]="r.status === 'SUSPECTED'"
|
||||||
|
>{{ r.status }}</span
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (r.status === 'SUSPECTED') {
|
||||||
|
<button mat-stroked-button (click)="edit(r)">Review</button>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@case ('fraud-reports') {
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{ r.classified_on ? (r.classified_on | date: 'dd MMM yyyy') : 'Show-cause' }}
|
||||||
|
@if (r.show_cause_due_on && !r.classified_on) {
|
||||||
|
<div class="sub">reply due {{ r.show_cause_due_on | date: 'dd MMM' }}</div>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<b>{{ r.account_no }}</b>
|
||||||
|
<div class="sub">{{ r.client_name }} · {{ r.acknowledgement_nos }}</div>
|
||||||
|
</td>
|
||||||
|
<td class="num">{{ r.amount | number: '1.2-2' }}</td>
|
||||||
|
<td>
|
||||||
|
{{ r.police_ref || '-' }}
|
||||||
|
<div class="sub">{{ r.police_reported_on | date: 'dd MMM yyyy' }}</div>
|
||||||
|
</td>
|
||||||
|
<td [class.err-text]="!r.fmr_filed_on && isPast(r.fmr_due_on)">
|
||||||
|
{{ r.fmr_ref || 'due ' + (r.fmr_due_on | date: 'dd MMM') }}
|
||||||
|
<div class="sub">{{ r.fmr_filed_on | date: 'dd MMM yyyy' }}</div>
|
||||||
|
</td>
|
||||||
|
<td [class.err-text]="!r.str_filed_on && isPast(r.str_due_on)">
|
||||||
|
{{ r.str_ref || 'due ' + (r.str_due_on | date: 'dd MMM') }}
|
||||||
|
<div class="sub">{{ r.str_filed_on | date: 'dd MMM yyyy' }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="i4c-badge" [class.i4c-badge--green]="r.status === 'CLOSED'">{{ r.status }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (r.status === 'OPEN') {
|
||||||
|
<button mat-stroked-button (click)="edit(r)">Update</button>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@case ('linked-alerts') {
|
||||||
|
<tr>
|
||||||
|
<td>{{ r.moved_on | date: 'dd MMM yyyy' }}</td>
|
||||||
|
<td>
|
||||||
|
<b>{{ r.hold_account_no }}</b>
|
||||||
|
<div class="sub">Ack {{ r.acknowledgement_no }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ r.target_no }}
|
||||||
|
<div class="sub">
|
||||||
|
{{ r.kind === 'INTERNAL_TRANSFER' ? r.target_type + ' account here' : 'Loan repayment' }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="num">{{ r.amount | number: '1.2-2' }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="i4c-badge" [class.i4c-badge--amber]="r.status === 'OPEN'">{{ r.status }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="wrap">{{ r.remarks || '-' }}</td>
|
||||||
|
<td>
|
||||||
|
@if (r.status === 'OPEN') {
|
||||||
|
<button mat-stroked-button (click)="edit(r)">Action</button>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@case ('exclusions') {
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{ r.added_at | date: 'dd MMM yyyy' }}
|
||||||
|
<div class="sub">{{ r.added_by }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<b>{{ r.account_no }}</b>
|
||||||
|
<div class="sub">{{ r.account_type }}</div>
|
||||||
|
</td>
|
||||||
|
<td>{{ r.kind }}</td>
|
||||||
|
<td class="wrap">{{ r.reason }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="i4c-badge" [class.i4c-badge--green]="r.status === 'ACTIVE'">{{ r.status }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (r.status === 'ACTIVE') {
|
||||||
|
<button mat-button color="warn" (click)="removeExclusion(r)" [disabled]="busy">Remove</button>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@case ('bank-holds') {
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{ r.placed_at | date: 'dd MMM yyyy' }}
|
||||||
|
<div class="sub">{{ r.placed_by }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<b>{{ r.account_no }}</b>
|
||||||
|
<div class="sub">{{ r.client_name }} · {{ r.trigger_type }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ r.scope === 'ACCOUNT' ? 'All debits' : '₹' + (r.hold_amount | number: '1.2-2') }}
|
||||||
|
<div class="sub">
|
||||||
|
txn {{ r.flagged_txn_ref || '-' }} · ₹{{ r.flagged_amount | number: '1.2-2' }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
class="i4c-badge"
|
||||||
|
[class.i4c-badge--red]="r.status === 'HELD' || r.status === 'REFERRED'"
|
||||||
|
[class.i4c-badge--green]="r.status === 'LIFTED'"
|
||||||
|
>{{ r.status }}</span
|
||||||
|
>
|
||||||
|
@if (r.lift_reason) {
|
||||||
|
<div class="sub">{{ r.lift_reason }}</div>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td [class.err-text]="r.due">
|
||||||
|
@switch (r.status) {
|
||||||
|
@case ('HELD') {
|
||||||
|
explain by {{ r.explanation_due_on | date: 'dd MMM' }} · decide by
|
||||||
|
{{ r.decision_due_on | date: 'dd MMM' }}
|
||||||
|
}
|
||||||
|
@case ('EXPLAINED') {
|
||||||
|
decide by {{ r.decision_due_on | date: 'dd MMM' }}
|
||||||
|
}
|
||||||
|
@case ('REFERRED') {
|
||||||
|
police by {{ r.police_due_on | date: 'dd MMM' }} ({{ r.ncrp_ref }})
|
||||||
|
}
|
||||||
|
@default {
|
||||||
|
{{
|
||||||
|
r.status === 'LIFTED'
|
||||||
|
? (r.lifted_at | date: 'dd MMM yyyy')
|
||||||
|
: 'limit ' + (r.max_until | date: 'dd MMM')
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@if (r.grievance_raised_on && !r.grievance_resolved_on) {
|
||||||
|
<div class="sub">grievance due {{ r.grievance_due_on | date: 'dd MMM' }}</div>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{
|
||||||
|
r.customer_notified_at
|
||||||
|
? (r.customer_notified_at | date: 'dd MMM') + ' · ' + r.notification_mode
|
||||||
|
: 'Not yet'
|
||||||
|
}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (r.status !== 'LIFTED' || (r.grievance_raised_on && !r.grievance_resolved_on)) {
|
||||||
|
<button mat-stroked-button (click)="openStep(r)">Next step</button>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@case ('channel-blocks') {
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{ r.placed_at | date: 'dd MMM yyyy' }}
|
||||||
|
<div class="sub">{{ r.placed_by }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<b>{{ r.account_no }}</b>
|
||||||
|
<div class="sub">{{ r.account_type }}</div>
|
||||||
|
</td>
|
||||||
|
<td class="wrap">{{ r.channels }}</td>
|
||||||
|
<td class="wrap">{{ r.reason }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="i4c-badge" [class.i4c-badge--red]="r.status === 'ACTIVE'">{{ r.status }}</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ r.lifted_at ? (r.lifted_at | date: 'dd MMM yyyy') + ' · ' + r.lifted_by : '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@if (editing?.id === r.id && tab === 'bank-holds') {
|
||||||
|
<tr>
|
||||||
|
<td [attr.colspan]="8">
|
||||||
|
<div class="i4c-form">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Step</mat-label>
|
||||||
|
<mat-select [(ngModel)]="step" (selectionChange)="form = {}">
|
||||||
|
@for (b of bankSteps; track b.value) {
|
||||||
|
<mat-option [value]="b.value">{{ b.label }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
@switch (step) {
|
||||||
|
@case ('notice') {
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>How (PHONE, LETTER...)</mat-label>
|
||||||
|
<input matInput [(ngModel)]="form['mode']" maxlength="20" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="wide">
|
||||||
|
<mat-label>Reference</mat-label>
|
||||||
|
<input matInput [(ngModel)]="form['reference']" maxlength="200" />
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
@case ('explanation') {
|
||||||
|
<mifosx-i4c-date-field
|
||||||
|
label="Received on"
|
||||||
|
[(value)]="form['receivedOn']"
|
||||||
|
></mifosx-i4c-date-field>
|
||||||
|
<mat-form-field appearance="outline" class="wide">
|
||||||
|
<mat-label>Explanation</mat-label>
|
||||||
|
<input matInput [(ngModel)]="form['explanation']" maxlength="2000" />
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
@case ('decision') {
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Decision</mat-label>
|
||||||
|
<mat-select [(ngModel)]="form['decision']">
|
||||||
|
<mat-option value="LIFT">Satisfied: lift</mat-option>
|
||||||
|
<mat-option value="REFER">Not satisfied: refer to police</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
@if (form['decision'] === 'REFER') {
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>NCRP reference</mat-label>
|
||||||
|
<input matInput [(ngModel)]="form['ncrpRef']" maxlength="100" />
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
<mat-form-field appearance="outline" class="wide">
|
||||||
|
<mat-label>Basis</mat-label>
|
||||||
|
<input matInput [(ngModel)]="form['remarks']" maxlength="1000" />
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
@case ('police') {
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Instruction</mat-label>
|
||||||
|
<mat-select [(ngModel)]="form['instruction']">
|
||||||
|
<mat-option value="CONTINUE">Continue the hold</mat-option>
|
||||||
|
<mat-option value="LIFT">Lift the hold</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="wide">
|
||||||
|
<mat-label>Police reference</mat-label>
|
||||||
|
<input matInput [(ngModel)]="form['reference']" maxlength="200" />
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
@case ('grievance') {
|
||||||
|
<mat-form-field appearance="outline" class="wide">
|
||||||
|
<mat-label>Customer's grievance (resolve within 30 days)</mat-label>
|
||||||
|
<input matInput [(ngModel)]="form['text']" maxlength="2000" />
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
@case ('grievance-resolution') {
|
||||||
|
<mat-form-field appearance="outline" class="wide">
|
||||||
|
<mat-label>Resolution</mat-label>
|
||||||
|
<input matInput [(ngModel)]="form['resolution']" maxlength="2000" />
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="i4c-actions">
|
||||||
|
<button mat-flat-button color="primary" (click)="saveStep()" [disabled]="busy">Save</button>
|
||||||
|
<button mat-button (click)="editing = null">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@if (editing?.id === r.id && tab !== 'bank-holds') {
|
||||||
|
<tr>
|
||||||
|
<td [attr.colspan]="8">
|
||||||
|
<div class="i4c-form">
|
||||||
|
@if (tab === 'fraud-reports') {
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Police / LEA ref</mat-label>
|
||||||
|
<input matInput [(ngModel)]="form['policeRef']" maxlength="200" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mifosx-i4c-date-field
|
||||||
|
label="Reported on"
|
||||||
|
[(value)]="form['policeReportedOn']"
|
||||||
|
></mifosx-i4c-date-field>
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>FMR ref</mat-label>
|
||||||
|
<input matInput [(ngModel)]="form['fmrRef']" maxlength="100" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mifosx-i4c-date-field
|
||||||
|
label="FMR filed on"
|
||||||
|
[(value)]="form['fmrFiledOn']"
|
||||||
|
></mifosx-i4c-date-field>
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>STR ref</mat-label>
|
||||||
|
<input matInput [(ngModel)]="form['strRef']" maxlength="100" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mifosx-i4c-date-field
|
||||||
|
label="STR filed on"
|
||||||
|
[(value)]="form['strFiledOn']"
|
||||||
|
></mifosx-i4c-date-field>
|
||||||
|
} @else {
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Outcome</mat-label>
|
||||||
|
<mat-select [(ngModel)]="form['outcome']">
|
||||||
|
@if (tab === 'mule-flags') {
|
||||||
|
<mat-option value="CONFIRMED">Confirmed mule</mat-option>
|
||||||
|
<mat-option value="CLEARED">Not a mule (clear)</mat-option>
|
||||||
|
} @else {
|
||||||
|
<mat-option value="ACTIONED">Actioned</mat-option>
|
||||||
|
<mat-option value="DISMISSED">Dismissed</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
<mat-form-field appearance="outline" class="wide">
|
||||||
|
<mat-label>Remarks</mat-label>
|
||||||
|
<input matInput [(ngModel)]="form['remarks']" maxlength="1000" />
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="i4c-actions">
|
||||||
|
<button mat-flat-button color="primary" (click)="save()" [disabled]="busy">Save</button>
|
||||||
|
<button mat-button (click)="editing = null">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="i4c-empty">Nothing here for these filters.</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<mat-paginator
|
||||||
|
class="i4c-paginator"
|
||||||
|
[length]="total"
|
||||||
|
[pageIndex]="pageIndex"
|
||||||
|
[pageSize]="pageSize"
|
||||||
|
[pageSizeOptions]="pageSizes"
|
||||||
|
(page)="onPage($event)"
|
||||||
|
showFirstLastButtons
|
||||||
|
aria-label="Register pages"
|
||||||
|
></mat-paginator>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@use '../i4c' as *;
|
||||||
|
@use '../i4c-cases' as *;
|
||||||
|
|
||||||
|
@include i4c-skin;
|
||||||
|
@include i4c-case-page;
|
||||||
|
|
||||||
|
.err-text {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wide {
|
||||||
|
flex: 1 1 280px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
/**
|
||||||
|
* 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 { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
|
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, saveBlob } from '../i4c.service';
|
||||||
|
import { I4cDateFieldComponent } from '../i4c-date-field/i4c-date-field.component';
|
||||||
|
import { ALL_DATES, I4cDateRange, I4cDateRangeComponent } from '../i4c-date-range/i4c-date-range.component';
|
||||||
|
|
||||||
|
type Register = 'mule-flags' | 'fraud-reports' | 'linked-alerts' | 'channel-blocks' | 'exclusions' | 'bank-holds';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bank's own duties around suspect accounts (MHA SOP, RBI fraud and AML rules): the mule-account register, fraud
|
||||||
|
* reporting to police / RBI (FMR) / FIU-IND (STR) with due dates, fraud money that repaid loans, and digital-channel
|
||||||
|
* suspensions.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-i4c-risk',
|
||||||
|
templateUrl: './i4c-risk.component.html',
|
||||||
|
styleUrls: ['./i4c-risk.component.scss'],
|
||||||
|
imports: [
|
||||||
|
DatePipe,
|
||||||
|
DecimalPipe,
|
||||||
|
FormsModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatCheckboxModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatProgressBarModule,
|
||||||
|
MatPaginatorModule,
|
||||||
|
FaIconComponent,
|
||||||
|
I4cDateRangeComponent,
|
||||||
|
I4cDateFieldComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class I4cRiskComponent implements OnInit {
|
||||||
|
private i4c = inject(I4cService);
|
||||||
|
private snackBar = inject(MatSnackBar);
|
||||||
|
|
||||||
|
readonly tabs: { value: Register; label: string; icon: string; statuses: string[] }[] = [
|
||||||
|
{ value: 'mule-flags', label: 'Mule register', icon: 'user-shield', statuses: [
|
||||||
|
'SUSPECTED',
|
||||||
|
'CONFIRMED',
|
||||||
|
'CLEARED'
|
||||||
|
] },
|
||||||
|
{ value: 'fraud-reports', label: 'Fraud reporting (FMR / STR)', icon: 'file-alt', statuses: [
|
||||||
|
'SHOW_CAUSE',
|
||||||
|
'OPEN',
|
||||||
|
'CLOSED'
|
||||||
|
] },
|
||||||
|
{ value: 'linked-alerts', label: 'Loan repayments', icon: 'exchange-alt', statuses: [
|
||||||
|
'OPEN',
|
||||||
|
'ACTIONED',
|
||||||
|
'DISMISSED'
|
||||||
|
] },
|
||||||
|
{ value: 'channel-blocks', label: 'Digital suspensions', icon: 'ban', statuses: [
|
||||||
|
'ACTIVE',
|
||||||
|
'LIFTED'
|
||||||
|
] },
|
||||||
|
{ value: 'bank-holds', label: 'Bank-initiated holds (RBI draft)', icon: 'lock', statuses: [
|
||||||
|
'OPEN',
|
||||||
|
'HELD',
|
||||||
|
'EXPLAINED',
|
||||||
|
'REFERRED',
|
||||||
|
'CONTINUED',
|
||||||
|
'LIFTED'
|
||||||
|
] },
|
||||||
|
{ value: 'exclusions', label: 'Nodal / pool / escrow', icon: 'university', statuses: [
|
||||||
|
'ACTIVE',
|
||||||
|
'REMOVED'
|
||||||
|
] }
|
||||||
|
];
|
||||||
|
exclusionForm = { accountNo: '', kind: 'NODAL', reason: '' };
|
||||||
|
bankHoldForm = {
|
||||||
|
accountNo: '',
|
||||||
|
scope: 'TRANSACTION',
|
||||||
|
trigger: 'PROFILE_MISMATCH',
|
||||||
|
txnRef: '',
|
||||||
|
amount: null as number | null,
|
||||||
|
reason: ''
|
||||||
|
};
|
||||||
|
showPlace = false;
|
||||||
|
step = '';
|
||||||
|
readonly bankSteps: { value: string; label: string }[] = [
|
||||||
|
{ value: 'notice', label: 'Customer told (by hand)' },
|
||||||
|
{ value: 'explanation', label: "Customer's explanation" },
|
||||||
|
{ value: 'decision', label: 'Decision: lift or refer to police' },
|
||||||
|
{ value: 'police', label: "Police's instruction" },
|
||||||
|
{ value: 'grievance', label: 'Customer grievance' },
|
||||||
|
{ value: 'grievance-resolution', label: 'Grievance resolved' }
|
||||||
|
];
|
||||||
|
readonly pageSizes = [
|
||||||
|
10,
|
||||||
|
20,
|
||||||
|
50,
|
||||||
|
100
|
||||||
|
];
|
||||||
|
|
||||||
|
tab: Register = 'mule-flags';
|
||||||
|
status = '';
|
||||||
|
overdueOnly = false;
|
||||||
|
search = '';
|
||||||
|
dates: I4cDateRange = { ...ALL_DATES };
|
||||||
|
rows: any[] = [];
|
||||||
|
total = 0;
|
||||||
|
pageIndex = 0;
|
||||||
|
pageSize = 20;
|
||||||
|
loading = false;
|
||||||
|
busy = false;
|
||||||
|
error = '';
|
||||||
|
|
||||||
|
editing: any = null;
|
||||||
|
form: Record<string, any> = {};
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
get statuses(): string[] {
|
||||||
|
return this.tabs.find((t) => t.value === this.tab)?.statuses || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
setTab(t: Register): void {
|
||||||
|
this.tab = t;
|
||||||
|
this.status = '';
|
||||||
|
this.overdueOnly = false;
|
||||||
|
this.editing = null;
|
||||||
|
this.load(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
load(fromFirstPage = false): void {
|
||||||
|
if (fromFirstPage) {
|
||||||
|
this.pageIndex = 0;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
this.error = '';
|
||||||
|
this.i4c
|
||||||
|
.register(this.tab, {
|
||||||
|
status: this.status,
|
||||||
|
overdue: (this.tab === 'fraud-reports' && this.overdueOnly) || null,
|
||||||
|
due: (this.tab === 'bank-holds' && this.overdueOnly) || null,
|
||||||
|
search: this.search.trim(),
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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.overdueOnly || this.search || this.dates.preset !== 'ALL');
|
||||||
|
}
|
||||||
|
|
||||||
|
clearFilters(): void {
|
||||||
|
this.status = '';
|
||||||
|
this.overdueOnly = false;
|
||||||
|
this.search = '';
|
||||||
|
this.dates = { ...ALL_DATES };
|
||||||
|
this.load(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
isPast(date: string | null | undefined): boolean {
|
||||||
|
return !!date && new Date(date).getTime() < Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
edit(row: any): void {
|
||||||
|
this.editing = row;
|
||||||
|
if (this.tab === 'mule-flags') {
|
||||||
|
this.form = { outcome: 'CONFIRMED', remarks: '' };
|
||||||
|
} else if (this.tab === 'fraud-reports') {
|
||||||
|
this.form = {
|
||||||
|
policeRef: row.police_ref || '',
|
||||||
|
policeReportedOn: row.police_reported_on || '',
|
||||||
|
fmrRef: row.fmr_ref || '',
|
||||||
|
fmrFiledOn: row.fmr_filed_on || '',
|
||||||
|
strRef: row.str_ref || '',
|
||||||
|
strFiledOn: row.str_filed_on || '',
|
||||||
|
remarks: row.remarks || ''
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.form = { outcome: 'ACTIONED', remarks: '' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
save(): void {
|
||||||
|
const row = this.editing;
|
||||||
|
const call =
|
||||||
|
this.tab === 'mule-flags'
|
||||||
|
? this.i4c.casePost('mule-flags/' + row.id + '/review', this.form)
|
||||||
|
: this.tab === 'fraud-reports'
|
||||||
|
? this.i4c.casePut('fraud-reports/' + row.id, this.form)
|
||||||
|
: this.i4c.casePost('linked-alerts/' + row.id, this.form);
|
||||||
|
this.busy = true;
|
||||||
|
call.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.busy = false;
|
||||||
|
this.editing = null;
|
||||||
|
this.snackBar.open('Saved.', 'OK', { duration: 3000 });
|
||||||
|
this.load();
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
this.busy = false;
|
||||||
|
this.snackBar.open(i4cErrorMessage(e), 'Close', { duration: 7000 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private post(path: string, body: Record<string, any>, done: string, after?: () => void): void {
|
||||||
|
this.busy = true;
|
||||||
|
this.i4c.casePost(path, body).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.busy = false;
|
||||||
|
this.snackBar.open(done, 'OK', { duration: 4000 });
|
||||||
|
after?.();
|
||||||
|
this.load();
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
this.busy = false;
|
||||||
|
this.snackBar.open(i4cErrorMessage(e), 'Close', { duration: 7000 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addExclusion(): void {
|
||||||
|
this.post('exclusions', this.exclusionForm, 'Account excluded from I4C holds.', () => {
|
||||||
|
this.exclusionForm = { accountNo: '', kind: 'NODAL', reason: '' };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeExclusion(row: any): void {
|
||||||
|
this.post(
|
||||||
|
'exclusions/' + row.id + '/remove',
|
||||||
|
{},
|
||||||
|
'Exclusion removed; I4C complaints will hold this account again.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
placeBankHold(): void {
|
||||||
|
this.post('bank-holds', this.bankHoldForm, 'Hold placed; the customer is told on the automatic channels.', () => {
|
||||||
|
this.showPlace = false;
|
||||||
|
this.bankHoldForm = {
|
||||||
|
accountNo: '',
|
||||||
|
scope: 'TRANSACTION',
|
||||||
|
trigger: 'PROFILE_MISMATCH',
|
||||||
|
txnRef: '',
|
||||||
|
amount: null,
|
||||||
|
reason: ''
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
openStep(row: any): void {
|
||||||
|
this.editing = row;
|
||||||
|
this.step =
|
||||||
|
row.status === 'HELD'
|
||||||
|
? 'explanation'
|
||||||
|
: row.status === 'EXPLAINED'
|
||||||
|
? 'decision'
|
||||||
|
: row.status === 'REFERRED'
|
||||||
|
? 'police'
|
||||||
|
: 'grievance';
|
||||||
|
this.form = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
saveStep(): void {
|
||||||
|
this.post('bank-holds/' + this.editing.id + '/' + this.step, this.form, 'Recorded.', () => (this.editing = null));
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadFraudCsv(): void {
|
||||||
|
this.i4c.fraudReportsCsv().subscribe({
|
||||||
|
next: (blob) => saveBlob(blob, 'i4c-fraud-reports-' + new Date().toISOString().slice(0, 10) + '.csv'),
|
||||||
|
error: (e) => this.snackBar.open(i4cErrorMessage(e), 'Close', { duration: 6000 })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -124,23 +124,23 @@
|
|||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 2px 8px rgb(15 23 42 / 3%);
|
box-shadow: 0 2px 8px rgb(15 23 42 / 3%);
|
||||||
overflow-x: auto;
|
|
||||||
|
|
||||||
|
/* No horizontal scroll: tabs wrap onto a second row when the screen is narrow. */
|
||||||
&__links {
|
&__links {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 2px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__link {
|
&__link {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 5px;
|
||||||
padding: 6px 12px;
|
padding: 5px 9px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 11.5px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #475569;
|
color: #475569;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -162,7 +162,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--active {
|
&--active,
|
||||||
|
&--active:hover {
|
||||||
background: #2563eb;
|
background: #2563eb;
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
box-shadow: 0 3px 10px rgb(37 99 235 / 28%);
|
box-shadow: 0 3px 10px rgb(37 99 235 / 28%);
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export class I4cShellComponent implements OnInit {
|
|||||||
{ path: 'grievances', label: 'GRM Grievances', icon: 'balance-scale' },
|
{ path: 'grievances', label: 'GRM Grievances', icon: 'balance-scale' },
|
||||||
{ path: 'restorations', label: 'MRM Restorations', icon: 'hand-holding-usd' },
|
{ path: 'restorations', label: 'MRM Restorations', icon: 'hand-holding-usd' },
|
||||||
{ path: 'mrm-report', label: 'MRM Balance Report', icon: 'file-csv' },
|
{ path: 'mrm-report', label: 'MRM Balance Report', icon: 'file-csv' },
|
||||||
|
{ path: 'risk', label: 'Risk Registers', icon: 'user-shield' },
|
||||||
{ path: 'outbox', label: 'Responses & API Log', icon: 'paper-plane' },
|
{ path: 'outbox', label: 'Responses & API Log', icon: 'paper-plane' },
|
||||||
{ path: 'bank-master', label: 'Bank Code Master', icon: 'university' },
|
{ path: 'bank-master', label: 'Bank Code Master', icon: 'university' },
|
||||||
{ path: 'intimation-settings', label: 'Intimation Settings', icon: 'cog' }
|
{ path: 'intimation-settings', label: 'Intimation Settings', icon: 'cog' }
|
||||||
|
|||||||
@@ -50,6 +50,21 @@ export interface I4cHold {
|
|||||||
documents?: I4cDocument[];
|
documents?: I4cDocument[];
|
||||||
released_amount?: number;
|
released_amount?: number;
|
||||||
restored_amount?: number;
|
restored_amount?: number;
|
||||||
|
digital_suspended?: boolean;
|
||||||
|
lapse_on?: string;
|
||||||
|
lea_notice_due_on?: string;
|
||||||
|
lea_notice_sent_on?: string;
|
||||||
|
lea_notice_ref?: string;
|
||||||
|
extension_until?: string;
|
||||||
|
extension_ref?: string;
|
||||||
|
linkedAlerts?: any[];
|
||||||
|
relatedAccounts?: any[];
|
||||||
|
excludedKind?: { kind: string }[];
|
||||||
|
muleFlags?: any[];
|
||||||
|
fraudReports?: any[];
|
||||||
|
channelBlocks?: any[];
|
||||||
|
reportsOnAccount?: number;
|
||||||
|
canLapseRelease?: boolean;
|
||||||
auto_intimation_attempts?: number;
|
auto_intimation_attempts?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +156,7 @@ export interface I4cPage<T> {
|
|||||||
export type I4cListParams = Record<string, string | number | boolean | null | undefined>;
|
export type I4cListParams = Record<string, string | number | boolean | null | undefined>;
|
||||||
|
|
||||||
export interface HoldActionRequest {
|
export interface HoldActionRequest {
|
||||||
action: 'RELEASE' | 'FREEZE' | 'UNFREEZE' | 'UNFREEZE_AND_RELEASE';
|
action: 'RELEASE' | 'FREEZE' | 'UNFREEZE' | 'UNFREEZE_AND_RELEASE' | 'SUSPEND_DIGITAL' | 'RESTORE_DIGITAL';
|
||||||
/** RELEASE only: how much to release; empty releases all. */
|
/** RELEASE only: how much to release; empty releases all. */
|
||||||
amount?: number | null;
|
amount?: number | null;
|
||||||
reasonType: string;
|
reasonType: string;
|
||||||
@@ -259,6 +274,19 @@ export class I4cService {
|
|||||||
return this.http.get<I4cPage<any>>(`${this.base}/bank-master`, { params: this.params(query) });
|
return this.http.get<I4cPage<any>>(`${this.base}/bank-master`, { params: this.params(query) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The bank's IFSC / prefix -> I4C Bank Code map, and reported IFSCs that had no code. */
|
||||||
|
ifscMap(): Observable<{ mappings: any[]; unknown: any[] }> {
|
||||||
|
return this.http.get<{ mappings: any[]; unknown: any[] }>(`${this.cases}/ifsc-map`);
|
||||||
|
}
|
||||||
|
|
||||||
|
mapIfsc(body: { ifscKey: string; bankCode: number; note?: string }): Observable<{ resourceId: number }> {
|
||||||
|
return this.http.post<{ resourceId: number }>(`${this.cases}/ifsc-map`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
unmapIfsc(id: number): Observable<{ resourceId: number }> {
|
||||||
|
return this.http.post<{ resourceId: number }>(`${this.cases}/ifsc-map/${id}/remove`, {});
|
||||||
|
}
|
||||||
|
|
||||||
// ── GRM / MRM (/v1/i4c/cases) ──────────────────────────────────────────
|
// ── GRM / MRM (/v1/i4c/cases) ──────────────────────────────────────────
|
||||||
|
|
||||||
grievances(query: I4cListParams = {}): Observable<I4cPage<any>> {
|
grievances(query: I4cListParams = {}): Observable<I4cPage<any>> {
|
||||||
@@ -335,6 +363,24 @@ export class I4cService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A POST on /i4c/cases/{path}, e.g. restorations/5/contest or holds/3/lea-notice. */
|
||||||
|
casePost(path: string, body: Record<string, any> = {}): Observable<any> {
|
||||||
|
return this.http.post<any>(`${this.cases}/${path}`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
casePut(path: string, body: Record<string, any> = {}): Observable<any> {
|
||||||
|
return this.http.put<any>(`${this.cases}/${path}`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A paged register on /i4c/cases: mule-flags, fraud-reports, linked-alerts, channel-blocks. */
|
||||||
|
register(name: string, query: I4cListParams = {}): Observable<I4cPage<any>> {
|
||||||
|
return this.http.get<I4cPage<any>>(`${this.cases}/${name}`, { params: this.params(query) });
|
||||||
|
}
|
||||||
|
|
||||||
|
fraudReportsCsv(): Observable<Blob> {
|
||||||
|
return this.http.get(`${this.cases}/fraud-reports.csv`, { responseType: 'blob' });
|
||||||
|
}
|
||||||
|
|
||||||
documents(ownerType: I4cDocumentOwner, ownerId: number): Observable<I4cDocument[]> {
|
documents(ownerType: I4cDocumentOwner, ownerId: number): Observable<I4cDocument[]> {
|
||||||
return this.http.get<I4cDocument[]>(`${this.cases}/documents`, { params: this.params({ ownerType, ownerId }) });
|
return this.http.get<I4cDocument[]>(`${this.cases}/documents`, { params: this.params({ ownerType, ownerId }) });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user