feat: add KYC document renewal alerts, Aadhaar/PAN exceptions, and AML recurring alerts UI; remove old journal AML marking
This commit is contained in:
+194
@@ -0,0 +1,194 @@
|
||||
<!--
|
||||
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="flagged-toolbar">
|
||||
<div class="flagged-live">
|
||||
<span class="live-dot" [class.paused]="!live"></span>
|
||||
<span class="live-label">{{ live ? 'Live' : 'Paused' }}</span>
|
||||
<span *ngIf="lastUpdated" class="live-updated">Updated {{ lastUpdated | date: 'HH:mm:ss' }}</span>
|
||||
<mat-spinner *ngIf="loading" diameter="16" class="live-spinner"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<div class="flagged-actions">
|
||||
<button
|
||||
*ngIf="newAlertCount > 0"
|
||||
type="button"
|
||||
class="new-alert-pill"
|
||||
matTooltip="Clear the new-alert highlight"
|
||||
(click)="acknowledgeNew()"
|
||||
>
|
||||
<mat-icon>notifications_active</mat-icon>
|
||||
{{ newAlertCount }} new
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
[matTooltip]="live ? 'Pause live updates' : 'Resume live updates'"
|
||||
(click)="toggleLive()"
|
||||
>
|
||||
<mat-icon>{{ live ? 'pause_circle' : 'play_circle' }}</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button type="button" matTooltip="Refresh now" [disabled]="loading" (click)="fetch()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="loadFailed" class="flagged-error">
|
||||
<mat-icon>cloud_off</mat-icon>
|
||||
<span>Could not reach the alert feed. Retrying automatically.</span>
|
||||
</div>
|
||||
|
||||
<!-- Narrow the feed by where each alert sits in the review workflow -->
|
||||
<div class="status-filters">
|
||||
<span class="status-filters-label">Status</span>
|
||||
<button
|
||||
*ngFor="let s of statusFilters"
|
||||
type="button"
|
||||
class="status-chip"
|
||||
[class.active]="activeStatus === s.key"
|
||||
[disabled]="statusCount(s.key) === 0 && activeStatus !== s.key"
|
||||
(click)="setStatus(s.key)"
|
||||
>
|
||||
{{ s.label }}
|
||||
<span class="status-chip-n">{{ statusCount(s.key) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Severity mix of what the table is showing -->
|
||||
<div class="risk-summary">
|
||||
<div *ngFor="let tier of riskSummary" class="risk-tile" [ngClass]="tier.cls">
|
||||
<span class="risk-tile-n">{{ tier.count }}</span>
|
||||
<span class="risk-tile-l">{{ tier.label }}</span>
|
||||
</div>
|
||||
<div *ngIf="unclassifiedCount > 0" class="risk-tile risk-none" matTooltip="Raised by a user, not by a catalog rule">
|
||||
<span class="risk-tile-n">{{ unclassifiedCount }}</span>
|
||||
<span class="risk-tile-l">Unclassified</span>
|
||||
</div>
|
||||
<div class="risk-tile risk-total">
|
||||
<span class="risk-tile-n">{{ summaryTotal }}</span>
|
||||
<span class="risk-tile-l">Total Flagged</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table mat-table [dataSource]="alerts" class="flagged-table mat-elevation-z1">
|
||||
<!-- When the engine raised the alert -->
|
||||
<ng-container matColumnDef="raisedOn">
|
||||
<th mat-header-cell *matHeaderCellDef>Raised On</th>
|
||||
<td mat-cell *matCellDef="let alert" class="raised-cell">
|
||||
<span class="raised-time">{{
|
||||
alert.raisedOnDate ? (alert.raisedOnDate | date: 'dd MMM yyyy, HH:mm') : '—'
|
||||
}}</span>
|
||||
<span *ngIf="isNew(alert)" class="new-badge">NEW</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Which transaction tripped the rule -->
|
||||
<ng-container matColumnDef="transaction">
|
||||
<th mat-header-cell *matHeaderCellDef>Transaction</th>
|
||||
<td mat-cell *matCellDef="let alert">
|
||||
<span [ngClass]="txnTypeClass(alert.transactionType)">{{ alert.transactionType }}</span>
|
||||
<span class="txn-id">#{{ alert.transactionId }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="client">
|
||||
<th mat-header-cell *matHeaderCellDef>Client</th>
|
||||
<td mat-cell *matCellDef="let alert">
|
||||
<div class="stacked-cell">
|
||||
<span>{{ alert.clientDisplayName || '—' }}</span>
|
||||
<span *ngIf="alert.clientId" class="cell-sub">ID {{ alert.clientId }}</span>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="rule">
|
||||
<th mat-header-cell *matHeaderCellDef>Rule Triggered</th>
|
||||
<td mat-cell *matCellDef="let alert" class="rule-cell">
|
||||
<div class="stacked-cell">
|
||||
<span class="rule-code">{{ alert.ruleCode }}</span>
|
||||
<span class="cell-sub">{{ alert.ruleDescription }}</span>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="amount">
|
||||
<th mat-header-cell *matHeaderCellDef class="col-right">Amount</th>
|
||||
<td mat-cell *matCellDef="let alert" class="col-right amount-cell">{{ amount(alert) }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef class="col-center">Status</th>
|
||||
<td mat-cell *matCellDef="let alert" class="col-center">
|
||||
<span [ngClass]="statusClass(alert.status)">{{ alert.status }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Disposition. Hidden once an alert is already whitelisted or escalated. -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef class="col-center">Action</th>
|
||||
<td mat-cell *matCellDef="let alert" class="col-center actions-cell">
|
||||
<ng-container *ngIf="canReview(alert); else reviewed">
|
||||
<!-- Same gate the FIU-IND register uses; the endpoint enforces REVIEW_AML_ALERT server side,
|
||||
so without this a user who cannot review still sees buttons that only fail on click. -->
|
||||
<div class="action-group" *mifosxHasPermission="'REVIEW_AML_ALERT'">
|
||||
<button
|
||||
type="button"
|
||||
class="action-btn action-whitelist"
|
||||
matTooltip="Clear this transaction with no report"
|
||||
(click)="review(alert, 'WHITELIST')"
|
||||
>
|
||||
<mat-icon>verified</mat-icon>
|
||||
Whitelist
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="action-btn action-suspicious"
|
||||
matTooltip="Raise as suspicious — opens an STR case"
|
||||
(click)="review(alert, 'ESCALATE')"
|
||||
>
|
||||
<mat-icon>report</mat-icon>
|
||||
Suspicious
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #reviewed>
|
||||
<div class="reviewed-cell">
|
||||
<span class="reviewed-by">
|
||||
{{ alert.reviewedByUsername ? 'by ' + alert.reviewedByUsername : 'Reviewed' }}
|
||||
</span>
|
||||
<!-- The evidence recorded at review time, so the trail is readable without leaving the feed -->
|
||||
<mat-icon
|
||||
*ngIf="alert.reviewNotes"
|
||||
class="reviewed-evidence-icon"
|
||||
[matTooltip]="alert.reviewNotes"
|
||||
matTooltipClass="evidence-tooltip"
|
||||
>
|
||||
description
|
||||
</mat-icon>
|
||||
<span *ngIf="alert.status === 'ESCALATED'" class="str-routed" matTooltip="An STR case is open for this alert">
|
||||
→ STR
|
||||
</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="alert-row" [class.row-new]="isNew(row)"></tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons class="flagged-paginator"></mat-paginator>
|
||||
|
||||
<div *ngIf="alerts.data.length === 0 && !loading" class="no-alerts-message">
|
||||
<mat-icon>{{ activeStatus ? 'filter_alt_off' : 'verified_user' }}</mat-icon>
|
||||
<span *ngIf="!activeStatus"
|
||||
>No transactions flagged. The feed updates automatically as the engine raises alerts.</span
|
||||
>
|
||||
<span *ngIf="activeStatus">No alerts in this status.</span>
|
||||
</div>
|
||||
Reference in New Issue
Block a user