Merge branch 'dev' into feature/bugfix
Conflicts resolved in: - src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.html - src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.ts Kept dev's CKYC architecture (upload dialog, real per-artefact endpoints) and layered feature/bugfix's fixes on top: upload file validation, missing-file badge, silent preview download, and image/PDF view button. Also includes the CKYC "Image of Client" upload-state fix and the client-change state-leak fix in the same component. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -224,21 +224,40 @@ export class RiskReviewReportComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
// The alert carries the client but not their account number or branch, so both columns read
|
||||
// as an em dash on every live row. Both sit on the client record the KYC call already needs,
|
||||
// so they are fetched alongside it rather than left blank on a compliance report.
|
||||
forkJoin(
|
||||
clientIds.map((id) =>
|
||||
this.clients.getClientKyc(String(id), true).pipe(
|
||||
map((kyc: any) => ({ id, risk: riskFromCategory(kyc?.riskCategory) })),
|
||||
catchError(() => of({ id, risk: null as RiskLevel | null }))
|
||||
forkJoin({
|
||||
kyc: this.clients.getClientKyc(String(id), true).pipe(catchError(() => of(null))),
|
||||
client: this.clients.getClientData(String(id), true).pipe(catchError(() => of(null)))
|
||||
}).pipe(
|
||||
map((r: any) => ({
|
||||
id,
|
||||
risk: riskFromCategory(r.kyc?.riskCategory),
|
||||
accountNo: r.client?.accountNo || '',
|
||||
branch: r.client?.officeName || ''
|
||||
})),
|
||||
catchError(() => of({ id, risk: null as RiskLevel | null, accountNo: '', branch: '' }))
|
||||
)
|
||||
)
|
||||
).subscribe((results) => {
|
||||
const byClient = new Map(
|
||||
results.map((r) => [
|
||||
r.id,
|
||||
r.risk
|
||||
r
|
||||
])
|
||||
);
|
||||
this.liveAlerts = rows.map((r) => ({ ...r, currentRisk: byClient.get(r.clientId) ?? null }));
|
||||
this.liveAlerts = rows.map((r) => {
|
||||
const info = byClient.get(r.clientId);
|
||||
return {
|
||||
...r,
|
||||
currentRisk: info?.risk ?? null,
|
||||
accountNo: r.accountNo || info?.accountNo || '',
|
||||
branchCode: r.branchCode || info?.branch || ''
|
||||
};
|
||||
});
|
||||
this.compose();
|
||||
this.loading = false;
|
||||
});
|
||||
@@ -389,8 +408,62 @@ export class RiskReviewReportComponent implements OnInit {
|
||||
return groups.filter((g) => g.rows.length > 0);
|
||||
}
|
||||
|
||||
/** Collapsed sections, by group key. 'No change needed' starts closed — nothing to act on. */
|
||||
collapsedGroups: Record<string, boolean> = { same: true };
|
||||
/**
|
||||
* Collapsed sections, by group key. Every group starts open.
|
||||
*
|
||||
* 'No change needed' used to start closed on the reasoning that there was nothing to act on. On a
|
||||
* report that often holds one group, that meant opening the page to a heading and a count and no
|
||||
* rows — the customer was there, but reading the report took a click to discover it. The toggle
|
||||
* stays for folding a long section away.
|
||||
*/
|
||||
collapsedGroups: Record<string, boolean> = {};
|
||||
|
||||
/**
|
||||
* The summary tiles read as a question each — "which customers?", "which alerts still need a
|
||||
* decision?" — so each one narrows 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.
|
||||
*
|
||||
* Clicking an active tile clears it again, so a tile is never a one-way trip into a filter the
|
||||
* reviewer then has to hunt for.
|
||||
*/
|
||||
focusAllCustomers(): void {
|
||||
this.currentRisk = '';
|
||||
this.direction = '';
|
||||
this.openOnly = false;
|
||||
this.expanded = null;
|
||||
this.collapsedGroups = {};
|
||||
}
|
||||
|
||||
focusOpenAlerts(): void {
|
||||
this.openOnly = !this.openOnly;
|
||||
this.direction = '';
|
||||
this.expanded = null;
|
||||
this.collapsedGroups = {};
|
||||
}
|
||||
|
||||
/** Only the customers whose rating the review says should move, either way. */
|
||||
focusRatingsToMove(): void {
|
||||
// Nothing to narrow to when no rating needs moving; leaving the filter alone keeps the
|
||||
// table showing what is actually there rather than emptying it.
|
||||
if (this.summary.upgradeCandidates + this.summary.downgradeCandidates === 0) {
|
||||
return;
|
||||
}
|
||||
this.direction = this.direction === 'up' ? 'down' : this.direction === 'down' ? '' : 'up';
|
||||
this.openOnly = false;
|
||||
this.expanded = null;
|
||||
this.collapsedGroups = {};
|
||||
}
|
||||
|
||||
focusEscalated(): void {
|
||||
if (this.summary.escalatedCustomers === 0) {
|
||||
return;
|
||||
}
|
||||
this.status = this.status === 'ESCALATED_STR' ? '' : 'ESCALATED_STR';
|
||||
this.openOnly = false;
|
||||
this.expanded = null;
|
||||
this.collapsedGroups = {};
|
||||
}
|
||||
|
||||
toggleGroup(key: string): void {
|
||||
this.collapsedGroups[key] = !this.collapsedGroups[key];
|
||||
@@ -513,6 +586,47 @@ export class RiskReviewReportComponent implements OnInit {
|
||||
* An escalated case is worded as a review rather than a move: the rating is held
|
||||
* pending the Principal Officer's conclusion, so there is nothing to apply yet.
|
||||
*/
|
||||
/**
|
||||
* The verdict in the words a reviewer needs: which way the rating moves, and to what.
|
||||
*
|
||||
* An escalated case is deliberately not given a direction — the rating is held where it is
|
||||
* until the Principal Officer reports back, so showing "raise to High" there would invite a
|
||||
* change nobody is authorised to make yet.
|
||||
*/
|
||||
directionText(c: RiskCustomerRow): string {
|
||||
if (c.escalated) {
|
||||
return 'Held — with Principal Officer';
|
||||
}
|
||||
if (c.direction === 'up') {
|
||||
return `Raise to ${RISK_LABEL[c.recommendedRisk]}`;
|
||||
}
|
||||
if (c.direction === 'down') {
|
||||
return `Lower to ${RISK_LABEL[c.recommendedRisk]}`;
|
||||
}
|
||||
return c.currentRisk ? `Keep at ${RISK_LABEL[c.currentRisk]}` : 'No rating on record';
|
||||
}
|
||||
|
||||
directionIcon(c: RiskCustomerRow): string {
|
||||
if (c.escalated) {
|
||||
return 'lock';
|
||||
}
|
||||
// 'check' rather than an equals sign: faEquals is not in the shared library, and an
|
||||
// unregistered icon throws at render and takes the whole row down with it.
|
||||
return c.direction === 'up' ? 'arrow-up' : c.direction === 'down' ? 'arrow-down' : 'check';
|
||||
}
|
||||
|
||||
/**
|
||||
* A live row carries the office name straight off the client record, not a GL branch code, so
|
||||
* it is already the label to print. Only a coded row needs the lookup.
|
||||
*/
|
||||
branchLabelOf(branch: string): string {
|
||||
if (!branch) {
|
||||
return '—';
|
||||
}
|
||||
const named = glBranchName(branch);
|
||||
return named && named !== branch ? named : branch;
|
||||
}
|
||||
|
||||
kycActionText(c: RiskCustomerRow): string {
|
||||
if (c.escalated) {
|
||||
return 'Open KYC';
|
||||
@@ -689,7 +803,7 @@ export class RiskReviewReportComponent implements OnInit {
|
||||
String(c.clientId),
|
||||
c.customerName,
|
||||
c.accountNo || '—',
|
||||
c.branchCode ? glBranchName(c.branchCode) : '—',
|
||||
this.branchLabelOf(c.branchCode),
|
||||
this.riskText(c.currentRisk),
|
||||
c.alertCount,
|
||||
c.ruleLabels.join('; '),
|
||||
@@ -749,7 +863,7 @@ export class RiskReviewReportComponent implements OnInit {
|
||||
String(a.clientId),
|
||||
a.customerName,
|
||||
a.accountNo || '—',
|
||||
a.branchCode ? glBranchName(a.branchCode) : '—',
|
||||
this.branchLabelOf(a.branchCode),
|
||||
this.riskText(a.currentRisk),
|
||||
this.txnDetail(a),
|
||||
a.rule.label,
|
||||
@@ -815,7 +929,7 @@ export class RiskReviewReportComponent implements OnInit {
|
||||
String(t.clientId),
|
||||
t.customerName,
|
||||
t.accountNo || '—',
|
||||
t.branchCode ? glBranchName(t.branchCode) : '—',
|
||||
this.branchLabelOf(t.branchCode),
|
||||
this.riskText(t.currentRisk),
|
||||
channelLabel(t.channel),
|
||||
t.drCr,
|
||||
|
||||
Reference in New Issue
Block a user