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:
2026-09-16 16:55:32 +05:30
co-authored by Claude Sonnet 5
105 changed files with 7140 additions and 1902 deletions
@@ -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,