Files
cbsnexora/src/app/rbi-circulars/aml-rules/aml-recurring-alerts/aml-recurring-alerts.component.html
T
keshav 7b895a0e2b
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s
feat: add KYC document renewal alerts, Aadhaar/PAN exceptions, and AML recurring alerts UI; remove old journal AML marking
2026-08-11 11:11:18 +05:30

218 lines
8.3 KiB
HTML

<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="ra-intro">
<mat-icon class="ra-intro-icon">policy</mat-icon>
<span>
Customers the engine has flagged more than once. One alert is a question about a transaction — a pattern is a
question about the customer, which is what gets reviewed and recorded here.
</span>
</div>
<!-- What counts as "regularly" is a policy call, so it is set on screen rather than baked in -->
<div class="ra-controls">
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="ra-control">
<mat-label>Flag after</mat-label>
<mat-select [(ngModel)]="minAlerts" (ngModelChange)="load()">
<mat-option *ngFor="let t of thresholdOptions" [value]="t">{{ t }}+ alerts</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="ra-control">
<mat-label>Within</mat-label>
<mat-select [(ngModel)]="windowDays" (ngModelChange)="load()">
<mat-option *ngFor="let w of windowOptions" [value]="w.value">{{ w.label }}</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox [(ngModel)]="onlyNeedingReview" (ngModelChange)="load()" class="ra-check">
Only awaiting review
</mat-checkbox>
<span class="ra-spacer"></span>
<span *ngIf="needingReviewCount > 0" class="ra-pending-pill">
<mat-icon>pending_actions</mat-icon>
{{ needingReviewCount }} awaiting review
</span>
<mat-spinner *ngIf="loading" diameter="18"></mat-spinner>
<button mat-icon-button type="button" matTooltip="Refresh" [disabled]="loading" (click)="load()">
<mat-icon>refresh</mat-icon>
</button>
</div>
<div *ngIf="loadFailed" class="ra-error">
<mat-icon>cloud_off</mat-icon>
<span>Could not load the review queue.</span>
</div>
<!-- Named on screen, because sample rows must never be mistaken for engine output on an AML queue -->
<div *ngIf="usingDemoData && rows.data.length > 0" class="ra-demo">
<mat-icon>science</mat-icon>
<span>
Showing <strong>sample data</strong> — the AML engine returned no recurring alerts for this threshold and window.
These rows are for demonstration only and are not reportable.
</span>
</div>
<table mat-table [dataSource]="rows" class="ra-table mat-elevation-z1" multiTemplateDataRows>
<ng-container matColumnDef="customer">
<th mat-header-cell *matHeaderCellDef>Customer</th>
<td mat-cell *matCellDef="let row">
<div class="ra-stacked">
<span class="ra-name">{{ row.clientDisplayName }}</span>
<span class="ra-sub">
ID {{ row.clientId }}<span *ngIf="row.accountNo"> · {{ row.accountNo }}</span> · KYC risk
<span [ngClass]="riskClass(row.clientRiskCategory)">{{ riskLabel(row.clientRiskCategory) }}</span>
</span>
</div>
</td>
</ng-container>
<ng-container matColumnDef="alertCount">
<th mat-header-cell *matHeaderCellDef class="col-center">Alerts</th>
<td mat-cell *matCellDef="let row" class="col-center">
<span class="ra-count">{{ row.alertCount }}</span>
</td>
</ng-container>
<ng-container matColumnDef="severity">
<th mat-header-cell *matHeaderCellDef class="col-center">Severity</th>
<td mat-cell *matCellDef="let row" class="col-center">
<div class="ra-stacked ra-stacked-center">
<span class="ra-high" [class.ra-high-zero]="row.highRiskCount === 0">{{ row.highRiskCount }} High</span>
<span class="ra-sub">{{ row.openCount }} open · {{ row.escalatedCount }} escalated</span>
</div>
</td>
</ng-container>
<ng-container matColumnDef="rules">
<th mat-header-cell *matHeaderCellDef>Rules Tripped</th>
<td mat-cell *matCellDef="let row" class="ra-rules-cell">
<div class="ra-stacked">
<span class="ra-name">{{ row.distinctRules }} distinct</span>
<span class="ra-sub" [matTooltip]="row.ruleCodes?.join(', ')">{{ row.ruleCodes?.join(', ') }}</span>
</div>
</td>
</ng-container>
<ng-container matColumnDef="totalAmount">
<th mat-header-cell *matHeaderCellDef class="col-right">Total Value</th>
<td mat-cell *matCellDef="let row" class="col-right ra-amount">{{ amount(row.totalAmount) }}</td>
</ng-container>
<ng-container matColumnDef="lastAlert">
<th mat-header-cell *matHeaderCellDef>Activity</th>
<td mat-cell *matCellDef="let row">
<div class="ra-stacked">
<span>{{ row.lastAlertOnDate ? (row.lastAlertOnDate | date: 'dd MMM yyyy') : '—' }}</span>
<span class="ra-sub">
first {{ row.firstAlertOnDate ? (row.firstAlertOnDate | date: 'dd MMM yyyy') : '—' }}
</span>
</div>
</td>
</ng-container>
<ng-container matColumnDef="reviewState">
<th mat-header-cell *matHeaderCellDef class="col-center">Review</th>
<td mat-cell *matCellDef="let row" class="col-center">
<span *ngIf="row.needsReview && !row.lastReviewedOn" class="ra-state ra-state-new">Never reviewed</span>
<span
*ngIf="row.needsReview && row.lastReviewedOn"
class="ra-state ra-state-again"
[matTooltip]="
'Reviewed at ' + row.alertCountAtLastReview + ' alert(s); ' + row.alertCount + ' now — new activity since.'
"
>
New activity
</span>
<div *ngIf="!row.needsReview" class="ra-stacked ra-stacked-center">
<span class="ra-state ra-state-done">{{ row.lastReviewOutcome }}</span>
<span class="ra-sub" [matTooltip]="row.lastReviewNotes">
{{ row.lastReviewedByUsername }} · {{ row.lastReviewedOnDate | date: 'dd MMM' }}
</span>
</div>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="col-center">Action</th>
<td mat-cell *matCellDef="let row" class="col-center ra-actions">
<button
type="button"
class="ra-btn ra-btn-detail"
[matTooltip]="isExpanded(row) ? 'Hide transactions' : 'Show the triggering transactions'"
(click)="toggleExpanded(row)"
>
<mat-icon>{{ isExpanded(row) ? 'expand_less' : 'expand_more' }}</mat-icon>
{{ row.alertCount }} txns
</button>
<button
type="button"
class="ra-btn ra-btn-review"
*mifosxHasPermission="'REVIEW_AML_ALERT'"
matTooltip="Record why this customer keeps surfacing"
(click)="review(row)"
>
<mat-icon>fact_check</mat-icon>
Review
</button>
</td>
</ng-container>
<!-- Inline detail: the transactions behind the count, so the reviewer never has to guess -->
<ng-container matColumnDef="detail">
<td mat-cell *matCellDef="let row" [attr.colspan]="displayedColumns.length" class="ra-detail-cell">
<div class="ra-detail" *ngIf="isExpanded(row)">
<table class="ra-detail-tbl">
<thead>
<tr>
<th>Raised On</th>
<th>Transaction</th>
<th>Rule</th>
<th class="col-right">Amount</th>
<th>Status</th>
<th>Review Notes</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let a of row.alerts">
<td>{{ a.raisedOnDate ? (a.raisedOnDate | date: 'dd MMM yyyy, HH:mm') : '—' }}</td>
<td>{{ a.transactionType }} #{{ a.transactionId }}</td>
<td>
{{ a.ruleCode }}
<span class="ra-sub">{{ a.ruleDescription }}</span>
</td>
<td class="col-right ra-amount">{{ amount(a.amount) }}</td>
<td>{{ a.status }}</td>
<td class="ra-sub">{{ a.reviewNotes || '—' }}</td>
</tr>
</tbody>
</table>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr
mat-row
*matRowDef="let row; columns: displayedColumns"
class="ra-row"
[class.ra-row-pending]="row.needsReview"
></tr>
<tr mat-row *matRowDef="let row; columns: ['detail']" class="ra-detail-row"></tr>
</table>
<div *ngIf="rows.data.length === 0 && !loading" class="ra-empty">
<mat-icon>verified_user</mat-icon>
<span>
No customer has reached {{ minAlerts }} alerts in the selected window. Lower the threshold or widen the window to
see borderline cases.
</span>
</div>