feat: KYC document update, FIU & AML recurring alerts integration, Sponsor DD revoke workflow, and TDS reports

This commit is contained in:
2026-09-14 11:43:26 +05:30
parent 7afa9c7a50
commit 2124bb4b76
40 changed files with 3028 additions and 929 deletions
@@ -150,33 +150,62 @@
} @else {
<!-- SUMMARY -->
<div class="rr-kpis">
<div class="rr-kpi">
<!-- Each tile reads as a question, so each one answers it by narrowing the table to the rows
it counted. They were plain text before, which invited a click that did nothing and left
the reviewer to rebuild the same filter by hand from the dropdowns below. -->
<button type="button" class="rr-kpi" (click)="focusAllCustomers()" title="Show every customer in the period">
<div class="rr-kpi-n">{{ summary.customers }}</div>
<div class="rr-kpi-l">Customers for review</div>
<div class="rr-kpi-sub">
{{ summary.byRisk.LOW }} Low · {{ summary.byRisk.MEDIUM }} Medium · {{ summary.byRisk.HIGH }} High
</div>
</div>
<div class="rr-kpi rr-kpi--blue">
</button>
<button
type="button"
class="rr-kpi rr-kpi--blue"
[class.rr-kpi--on]="openOnly"
(click)="focusOpenAlerts()"
title="Show only the cases still awaiting a decision"
>
<div class="rr-kpi-n">{{ summary.alerts }}</div>
<div class="rr-kpi-l">AML alerts triggered</div>
<div class="rr-kpi-sub">on {{ summary.txns }} transactions · {{ summary.openAlerts }} awaiting a decision</div>
</div>
<div
</button>
<button
type="button"
class="rr-kpi rr-kpi--amber"
[class.rr-kpi--dim]="summary.upgradeCandidates + summary.downgradeCandidates === 0"
[class.rr-kpi--on]="direction !== ''"
(click)="focusRatingsToMove()"
[title]="
summary.upgradeCandidates + summary.downgradeCandidates === 0
? 'No rating needs moving in this period'
: 'Show the customers whose rating should move'
"
>
<div class="rr-kpi-n">{{ summary.upgradeCandidates + summary.downgradeCandidates }}</div>
<div class="rr-kpi-l">Ratings to be moved</div>
<div class="rr-kpi-sub">
{{ summary.upgradeCandidates }} upward · {{ summary.downgradeCandidates }} downward
</div>
</div>
<div class="rr-kpi" [class.rr-kpi--red]="summary.escalatedCustomers > 0">
</button>
<button
type="button"
class="rr-kpi"
[class.rr-kpi--red]="summary.escalatedCustomers > 0"
[class.rr-kpi--dim]="summary.escalatedCustomers === 0"
[class.rr-kpi--on]="status === 'ESCALATED_STR'"
(click)="focusEscalated()"
[title]="
summary.escalatedCustomers === 0
? 'No case has gone to the Principal Officer in this period'
: 'Show only the cases with the Principal Officer'
"
>
<div class="rr-kpi-n">{{ summary.escalatedCustomers }}</div>
<div class="rr-kpi-l">Escalated to STR</div>
<div class="rr-kpi-sub">₹{{ summary.totalAmount | number: '1.0-0' }} under review</div>
</div>
</button>
</div>
<!-- RULE MIX — doubles as a one-click filter -->
@@ -220,7 +249,7 @@
row does not have to repeat it in a Move To and a Position column. -->
@for (g of customerGroups; track g.key) {
<div class="rr-group" [class]="'rr-group--' + g.tone">
<button class="rr-group-head" (click)="toggleGroup(g.key)">
<button type="button" class="rr-group-head" (click)="toggleGroup(g.key)">
<fa-icon [icon]="collapsedGroups[g.key] ? 'caret-right' : 'caret-down'"></fa-icon>
<span class="rr-group-t">{{ g.title }}</span>
<span class="rr-group-n">{{ g.rows.length }}</span>
@@ -238,6 +267,7 @@
<th>Account Number</th>
<th>Branch</th>
<th>Rating on file</th>
<th>Review says</th>
<th class="num">Alerts</th>
<th>AML Rules Triggered</th>
<th class="num">Amount under review</th>
@@ -268,6 +298,20 @@
<td>
<span [class]="riskClass(c.currentRisk)">{{ riskText(c.currentRisk) }}</span>
</td>
<!-- The verdict itself, on the row. The section heading says it once for the
whole group, but a reviewer reading a single row - or an exported sheet -
could not tell whether the rating was to be raised, lowered or left. The
reason sits under it rather than under the button, because it explains
this conclusion and not the act of opening the KYC screen. -->
<td class="rr-move-cell">
<span class="rr-move" [class]="'rr-move--' + c.direction">
<fa-icon [icon]="directionIcon(c)"></fa-icon>
{{ directionText(c) }}
</span>
@if (c.directionReason) {
<div class="rr-verdict-why">{{ c.directionReason }}</div>
}
</td>
<td class="num">{{ c.alertCount }}</td>
<td class="rr-rules-cell">
@for (label of c.ruleLabels; track label) {
@@ -276,10 +320,11 @@
</td>
<td class="num strong">{{ c.totalAmount | number: '1.0-0' }}</td>
<td class="mono">{{ c.lastAlertDate }}</td>
<!-- The verdict is the section this row sits in, so the cell only carries the
button that acts on it, and the reason behind it. -->
<td class="rr-verdict-cell">
<!-- Only the control. The verdict and its reason moved to their own column,
so this no longer wraps to two lines under the button. -->
<td class="rr-action-cell">
<button
type="button"
class="rr-kyc-link"
[class.rr-kyc-link--act]="c.direction !== 'same' && !c.escalated"
(click)="openKyc(c, $event)"
@@ -287,15 +332,12 @@
>
<fa-icon icon="id-card"></fa-icon> {{ kycActionText(c) }}
</button>
@if (c.directionReason) {
<div class="rr-verdict-why">{{ c.directionReason }}</div>
}
</td>
</tr>
@if (expanded === c.clientId) {
<tr class="rr-detail">
<td colspan="11">
<td colspan="12">
<!-- One block per alert: what the rule is, then the entries it fired on. -->
@for (a of c.alerts; track a.alertId) {
<div class="rr-case">