From 2d93f80d505dd7c8b9369deb86992f1d5140911f Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Fri, 14 Aug 2026 14:48:14 +0530 Subject: [PATCH] feat(loan-origination): guarantor share-account verification, committee resolution gating, legal doc uploads, CIC score-detection fixes, and interest/share rate chart - Guarantor must be a real existing client with a verified active share account, with clickable share-account chips - Loan Committee Approval now requires Resolution No./Date plus a full application report before approving - Loan Committee gets its own pre-filtered pipeline view via a menu link and stage query param - Legal Approval stage gets a multi-file document upload checklist with add-more support - CIC PDF parser: item-level score detection before line-based fallback, fixing font-size-driven line-merging misdetection - New Interest & Share Rate Chart tab: per-product interest/tenure bands and per-amount-slab Share Capital linkage requirements --- src/app/core/shell/shell.component.html | 8 + .../application-detail.component.html | 190 +++++++++++++++- .../application-detail.component.ts | 203 +++++++++++++++++- .../cic-pdf-parser.util.ts | 80 +++++-- .../loan-origination/loan-origination-data.ts | 93 +++++++- .../loan-origination-shared.scss | 91 ++++++++ .../loan-pipeline.component.html | 105 +++++++++ .../loan-pipeline/loan-pipeline.component.ts | 41 +++- 8 files changed, 773 insertions(+), 38 deletions(-) diff --git a/src/app/core/shell/shell.component.html b/src/app/core/shell/shell.component.html index e60dd5be3..49f3911e7 100644 --- a/src/app/core/shell/shell.component.html +++ b/src/app/core/shell/shell.component.html @@ -1386,6 +1386,14 @@ + diff --git a/src/app/loan-origination/application-detail/application-detail.component.html b/src/app/loan-origination/application-detail/application-detail.component.html index 38f0b59c9..9a2c29a6a 100644 --- a/src/app/loan-origination/application-detail/application-detail.component.html +++ b/src/app/loan-origination/application-detail/application-detail.component.html @@ -1879,6 +1879,22 @@
+
+
Full Application Report β€” For the Committee
+

+ Everything captured on this application β€” KYC, CIC score, Field Investigation, property valuation, agent + report, meeting and stage history β€” as one PDF for every committee member to review before deciding. +

+
+ + +
+
+

Reports Required β€” present below reports, then complete.

+
+ + +
+ @if (committeeReportsReady(a) && !committeeCanApprove(a)) { +

+ Enter the Resolution No. and Resolution Date the committee passed this decision under before approving. +

+ }
@@ -1973,10 +2004,65 @@
@if (a.guarantorRequired) { - +
+
Guarantor β€” Must Be an Existing Share Member
+ @if (!a.guarantorClientId) { + + @if (searchingGuarantor) { +

Searching…

+ } @else if (guarantorSearchText.length >= 2 && guarantorSearchResults.length === 0) { +

No clients found.

+ } + @if (guarantorSearchResults.length > 0) { +
+ @for (client of guarantorSearchResults; track client.id) { + + } +
+ } +

+ The guarantor must already be a bank client with an active share account β€” search and select them + above, a name alone isn't enough. +

+ } @else { + + @if (loadingGuarantorShareCheck) { +

Checking share membership…

+ } @else if (a.guarantorHasShareAccount) { + Share Member β€” Verified + + } @else { + No Share Account Found +

+ This client has no active share account β€” a guarantor must already be a bank member. Pick a + different guarantor. +

+ } + } +
}
+ + @if (a.legalDocuments.length > 0) { +
+ + +
+ + +
+
+ } +
@@ -614,6 +622,103 @@
} + + + @if (activeView === 'rates') { +
+ +

+ Total Interest is an illustrative simple-interest figure (Amount x Rate x Tenure in years) at the product's own + min/max amount and tenure band β€” actual repayment is on a reducing balance and depends on the amount and tenure + chosen at sanction. Share Capital Required is the minimum a borrower must hold per bank bye-laws, proportional + to the loan amount. +

+
+ + + + + + + + + + + + + + + @for (p of products; track p.code) { + + + + + + + + + + + } + +
ProductTypeAmount RangeInterest Rate (p.a.)Tenure RangeIllustrative Total InterestShare Capital RequiredIllustrative Share Amount
{{ p.name }} + + {{ p.secured ? 'Secured' : 'Unsecured' }} + + β‚Ή{{ p.minAmt | number: '1.0-0' }} – β‚Ή{{ p.maxAmt | number: '1.0-0' }}{{ p.minRate | number: '1.1-1' }}% – {{ p.maxRate | number: '1.1-1' }}%{{ p.minTenure }} – {{ p.maxTenure }} months + β‚Ή{{ illustrativeInterest(p.minAmt, p.minRate, p.minTenure) | number: '1.0-0' }} – β‚Ή{{ + illustrativeInterest(p.maxAmt, p.maxRate, p.maxTenure) | number: '1.0-0' + }} + {{ p.shareLinkagePercent | number: '1.1-1' }}% + β‚Ή{{ p.minAmt * (p.shareLinkagePercent / 100) | number: '1.0-0' }} – β‚Ή{{ + p.maxAmt * (p.shareLinkagePercent / 100) | number: '1.0-0' + }} +
+
+
+ +
+ +

+ A general slab schedule by loan quantum, independent of product β€” where this differs from the product-wise + minimum above, whichever is higher governs the actual requirement. +

+ + + + + + + + + + @for (row of shareLinkageSlabRows; track row.minAmt) { + + + + + + } + +
Loan Amount SlabShare Capital RequiredIllustrative Share Amount
+ @if (row.maxAmt !== null) { + β‚Ή{{ row.minAmt | number: '1.0-0' }} – β‚Ή{{ row.maxAmt | number: '1.0-0' }} + } @else { + Above β‚Ή{{ row.minAmt | number: '1.0-0' }} + } + {{ row.sharePercent | number: '1.1-1' }}% + @if (row.shareAtMax !== null) { + β‚Ή{{ row.shareAtMin | number: '1.0-0' }} – β‚Ή{{ row.shareAtMax | number: '1.0-0' }} + } @else { + From β‚Ή{{ row.shareAtMin | number: '1.0-0' }} + } +
+
+ }
{{ toastMsg }}
diff --git a/src/app/loan-origination/loan-pipeline/loan-pipeline.component.ts b/src/app/loan-origination/loan-pipeline/loan-pipeline.component.ts index 121713ba5..8bcdb5103 100644 --- a/src/app/loan-origination/loan-pipeline/loan-pipeline.component.ts +++ b/src/app/loan-origination/loan-pipeline/loan-pipeline.component.ts @@ -29,6 +29,7 @@ import { PRODUCT_TARGET_PCT, PRODUCT_TYPE_LABELS, ProductType, + SHARE_LINKAGE_SLABS, STAGE_LABELS, STAGE_ORDER, STATUS_LABELS, @@ -81,7 +82,17 @@ interface MixTargetRow { isRbiMandated: boolean; } -type ViewId = 'pipeline' | 'new' | 'channel' | 'bc' | 'share' | 'target'; +interface AmountSlabRow { + minAmt: number; + maxAmt: number | null; + sharePercent: number; + /** Share Capital required at the slab's lower and upper bound, so the rupee amount is visible + * without officers having to do the multiplication themselves. */ + shareAtMin: number; + shareAtMax: number | null; +} + +type ViewId = 'pipeline' | 'new' | 'channel' | 'bc' | 'share' | 'target' | 'rates'; /** * Loan Origination pipeline β€” every application currently moving through the sanctioning @@ -115,6 +126,7 @@ export class LoanPipelineComponent implements OnInit { readonly channelLabels = CHANNEL_LABELS; readonly branches = BRANCHES; readonly products = ORIGINATION_PRODUCTS; + readonly shareLinkageSlabs = SHARE_LINKAGE_SLABS; asOnDate = todayDDMMYYYY(); activeView: ViewId = 'pipeline'; @@ -185,11 +197,20 @@ export class LoanPipelineComponent implements OnInit { 'channel', 'bc', 'share', - 'target' + 'target', + 'rates' ].includes(requestedView)) { this.activeView = requestedView; } + // Lets a menu link (e.g. "Loan Committee") land here pre-filtered to just the applications + // waiting on that one stage, reusing this same pipeline list/filter instead of a separate + // queue view -- same pattern as the `view` query param above. + const requestedStage = this.route.snapshot.queryParamMap.get('stage') as OriginationStage | null; + if (requestedStage && this.stageOrder.includes(requestedStage)) { + this.stageFilter = requestedStage; + } + const creds = this.auth.getCredentials(); this.isHeadOffice = this.auth.isHeadOfficeUser(); this.currentOfficeName = creds?.officeName || 'Branch Office'; @@ -427,6 +448,22 @@ export class LoanPipelineComponent implements OnInit { }); } + // -------------------- Interest & Share Linkage Reference -------------------- + /** Illustrative total interest over the product's own tenure band, simple-interest approximation + * (principal x rate x years) -- flagged as illustrative in the template since actual repayment + * is on a reducing balance and varies by amount/tenure chosen at sanction. */ + illustrativeInterest(amount: number, ratePct: number, tenureMonths: number): number { + return amount * (ratePct / 100) * (tenureMonths / 12); + } + + get shareLinkageSlabRows(): AmountSlabRow[] { + return this.shareLinkageSlabs.map((slab) => ({ + ...slab, + shareAtMin: slab.minAmt * (slab.sharePercent / 100), + shareAtMax: slab.maxAmt !== null ? slab.maxAmt * (slab.sharePercent / 100) : null + })); + } + get channelTargetRows(): MixTargetRow[] { const total = this.disbursedInScope.reduce((s, a) => s + a.requestedAmount, 0); const channels: Channel[] = [