feat(delinquency): maker-checker UI for Delinquency Ranges and Buckets

Checker inbox now resolves DELINQUENCY_RANGE/DELINQUENCY_BUCKET entries
to readable diffs instead of raw JSON, and each bucket/range screen
shows its own pending-approval state via a new pending-requests panel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 15:52:11 +05:30
co-authored by Claude Sonnet 5
parent d61584f291
commit afcd3af918
16 changed files with 539 additions and 3 deletions
@@ -63,6 +63,9 @@ const ENTITY_FETCHERS: Record<string, (id: string, s: EntityFetchServices) => Ob
FIXEDDEPOSITPRODUCT: (id, s) => s.products.getFixedDepositProductAndTemplate(id),
RECURRINGDEPOSITPRODUCT: (id, s) => s.products.getRecurringDepositProduct(id, true),
CHARGE: (id, s) => s.products.getCharge(id, true),
// SMA / NPA classification master. The bucket is read as-is; the dialog resolves its range ids to names.
DELINQUENCY_RANGE: (id, s) => s.products.getDelinquencyRange(id),
DELINQUENCY_BUCKET: (id, s) => s.products.getDelinquencyBucket(id),
// ---- Accounts ----------------------------------------------------------------
SAVINGSACCOUNT: (id, s) => s.savings.getSavingsAccountData(id),
@@ -199,6 +202,12 @@ const DELETE_SUMMARY_KEYS: Record<string, string[]> = {
'email',
'officeName'
],
DELINQUENCY_RANGE: [
'classification',
'minimumAgeDays',
'maximumAgeDays'
],
DELINQUENCY_BUCKET: ['name'],
GLACCOUNT: [
'name',
'glCode',
@@ -10,6 +10,7 @@
import { Component, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import * as _ from 'lodash';
import { forkJoin } from 'rxjs';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
/** Custom Services */
@@ -417,6 +418,9 @@ export class ViewCheckerInboxComponent {
if ((detail?.entityName ?? '').toUpperCase() === 'GLACCOUNT') {
this.resolveGlAccountCreateLabels(detail, this.jsondata);
}
if ((detail?.entityName ?? '').toUpperCase() === 'DELINQUENCY_BUCKET') {
this.resolveDelinquencyBucketCreateRanges(detail);
}
}
}
@@ -429,6 +433,10 @@ export class ViewCheckerInboxComponent {
* entity showed before.
*/
private loadUpdateChanges(detail: any): void {
if ((detail?.entityName ?? '').toUpperCase() === 'DELINQUENCY_BUCKET') {
this.loadDelinquencyBucketChanges(detail);
return;
}
const request$ = fetchCurrentEntity(detail?.entityName, detail?.resourceId, this.entityFetchServices);
if (!request$) {
return;
@@ -451,6 +459,78 @@ export class ViewCheckerInboxComponent {
});
}
/** One line per range for a delinquency bucket, e.g. 'SMA-1 (31 - 60 days)', oldest range first. */
private delinquencyRangeNames(ids: any[], rangesById: Map<number, any>): string {
return (ids ?? [])
.map((entry) => rangesById.get(Number(entry?.id ?? entry)) ?? { id: Number(entry?.id ?? entry) })
.sort((a, b) => (a.minimumAgeDays ?? 0) - (b.minimumAgeDays ?? 0))
.map((range) =>
range.classification
? `${range.classification} (${range.minimumAgeDays} - ${range.maximumAgeDays ?? 'no limit'} days)`
: `Range #${range.id}`
)
.join(', ');
}
/**
* Delinquency bucket UPDATE: the payload lists range ids, which mean nothing to a checker, and the stored
* bucket lists range objects, so a plain field diff would show two unreadable lists. Both sides are turned
* into the same readable range names first, so only a real difference is reported as OLD -> NEW.
*/
private loadDelinquencyBucketChanges(detail: any): void {
this.comparing = true;
forkJoin({
current: this.productsService.getDelinquencyBucket(detail?.resourceId),
ranges: this.productsService.getDelinquencyRanges()
}).subscribe({
next: ({ current, ranges }: { current: any; ranges: any }) => {
if (!this.isCurrent(detail)) {
return;
}
this.comparing = false;
const rangesById = new Map<number, any>(
(ranges ?? []).map((range: any) => [
Number(range.id),
range
])
);
const submitted = {
...(this.jsondata ?? {}),
ranges: this.delinquencyRangeNames(this.jsondata?.ranges, rangesById)
};
const existing = { ...(current ?? {}), ranges: this.delinquencyRangeNames(current?.ranges, rangesById) };
this.changes = diffCommandAgainstEntity(submitted, existing);
this.showChanges = true;
},
error: () => {
this.comparing = false;
}
});
}
/** Delinquency bucket CREATE: show the chosen ranges by name instead of their ids. */
private resolveDelinquencyBucketCreateRanges(detail: any): void {
this.productsService.getDelinquencyRanges().subscribe({
next: (ranges: any) => {
if (!this.isCurrent(detail)) {
return;
}
const rangesById = new Map<number, any>(
(ranges ?? []).map((range: any) => [
Number(range.id),
range
])
);
const resolved = {
...(this.jsondata ?? {}),
ranges: this.delinquencyRangeNames(this.jsondata?.ranges, rangesById)
};
this.createSections = this.buildCreateSections(resolved);
this.commandFields = this.buildCommandFields(resolved);
}
});
}
/**
* Loan Base Interest Rate: every submission is a CREATE of a whole new version (16 loan types x 10
* slabs plus CIBIL bands), so the raw payload hides the handful of rates the maker actually moved.