218 lines
8.3 KiB
HTML
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>
|