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
This commit is contained in:
@@ -116,6 +116,14 @@
|
||||
>
|
||||
Target vs Actual
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeView === 'rates'"
|
||||
(click)="setView('rates')"
|
||||
>
|
||||
Interest & Share Rate Chart
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ===================== PIPELINE ===================== -->
|
||||
@@ -614,6 +622,103 @@
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ===================== INTEREST & SHARE RATE CHART ===================== -->
|
||||
@if (activeView === 'rates') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Product-wise — Interest Rate & Share Capital Required</div>
|
||||
<p class="pg-sub npa-report-note">
|
||||
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.
|
||||
</p>
|
||||
<div class="pg-table-scroll">
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>Type</th>
|
||||
<th class="num">Amount Range</th>
|
||||
<th class="num">Interest Rate (p.a.)</th>
|
||||
<th class="num">Tenure Range</th>
|
||||
<th class="num">Illustrative Total Interest</th>
|
||||
<th class="num">Share Capital Required</th>
|
||||
<th class="num">Illustrative Share Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (p of products; track p.code) {
|
||||
<tr>
|
||||
<td>{{ p.name }}</td>
|
||||
<td>
|
||||
<span
|
||||
class="loan-badge"
|
||||
[class.loan-badge--progress]="p.secured"
|
||||
[class.loan-badge--hold]="!p.secured"
|
||||
>
|
||||
{{ p.secured ? 'Secured' : 'Unsecured' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="num">₹{{ p.minAmt | number: '1.0-0' }} – ₹{{ p.maxAmt | number: '1.0-0' }}</td>
|
||||
<td class="num">{{ p.minRate | number: '1.1-1' }}% – {{ p.maxRate | number: '1.1-1' }}%</td>
|
||||
<td class="num">{{ p.minTenure }} – {{ p.maxTenure }} months</td>
|
||||
<td class="num">
|
||||
₹{{ illustrativeInterest(p.minAmt, p.minRate, p.minTenure) | number: '1.0-0' }} – ₹{{
|
||||
illustrativeInterest(p.maxAmt, p.maxRate, p.maxTenure) | number: '1.0-0'
|
||||
}}
|
||||
</td>
|
||||
<td class="num">{{ p.shareLinkagePercent | number: '1.1-1' }}%</td>
|
||||
<td class="num">
|
||||
₹{{ p.minAmt * (p.shareLinkagePercent / 100) | number: '1.0-0' }} – ₹{{
|
||||
p.maxAmt * (p.shareLinkagePercent / 100) | number: '1.0-0'
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Amount Slab-wise — Share Capital Required (All Products)</div>
|
||||
<p class="pg-sub npa-report-note">
|
||||
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.
|
||||
</p>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="num">Loan Amount Slab</th>
|
||||
<th class="num">Share Capital Required</th>
|
||||
<th class="num">Illustrative Share Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of shareLinkageSlabRows; track row.minAmt) {
|
||||
<tr>
|
||||
<td class="num">
|
||||
@if (row.maxAmt !== null) {
|
||||
₹{{ row.minAmt | number: '1.0-0' }} – ₹{{ row.maxAmt | number: '1.0-0' }}
|
||||
} @else {
|
||||
Above ₹{{ row.minAmt | number: '1.0-0' }}
|
||||
}
|
||||
</td>
|
||||
<td class="num">{{ row.sharePercent | number: '1.1-1' }}%</td>
|
||||
<td class="num">
|
||||
@if (row.shareAtMax !== null) {
|
||||
₹{{ row.shareAtMin | number: '1.0-0' }} – ₹{{ row.shareAtMax | number: '1.0-0' }}
|
||||
} @else {
|
||||
From ₹{{ row.shareAtMin | number: '1.0-0' }}
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="inv-toast" [class.show]="toastVisible">{{ toastMsg }}</div>
|
||||
|
||||
@@ -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[] = [
|
||||
|
||||
Reference in New Issue
Block a user