feat(loan-origination): guarantor share-account verification, committee resolution gating, legal doc uploads, CIC score-detection fixes, and interest/share rate chart
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s

- 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:
2026-08-14 14:49:22 +05:30
parent 1d1e76497c
commit 2d93f80d50
8 changed files with 773 additions and 38 deletions
@@ -1879,6 +1879,22 @@
<div class="pg-card">
<div class="pos-section-label">Loan Committee Approval — {{ sanctioningAuthorityOf(a) }}</div>
<div class="loan-action-card">
<div class="loi-cic-subsection">
<div class="loi-cic-subsection-title">Full Application Report — For the Committee</div>
<p class="pg-sub npa-report-note">
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.
</p>
<div class="loan-action-row">
<button type="button" class="pg-btn pg-btn--ghost" (click)="printApplicationSummary(a)">
🖨️ Print Full Report
</button>
<button type="button" class="pg-btn pg-btn--ghost" (click)="downloadApplicationSummary(a)">
⬇️ Download Full Report
</button>
</div>
</div>
<p class="pg-sub npa-report-note">Reports Required — present below reports, then complete.</p>
<div class="loan-checklist">
<label class="loan-checklist-item">
@@ -1917,12 +1933,27 @@
<span>Committee Remarks</span>
<input type="text" [(ngModel)]="actionRemarks" placeholder="Remarks" />
</label>
<div class="inv-form-grid">
<label class="inv-form-field">
<span>Resolution No.</span>
<input type="text" [(ngModel)]="a.committeeResolutionNo" placeholder="e.g. LC/2026/0042" />
</label>
<label class="inv-form-field">
<span>Resolution Date</span>
<input type="text" [(ngModel)]="a.committeeResolutionDate" placeholder="DD-MM-YYYY" />
</label>
</div>
@if (committeeReportsReady(a) && !committeeCanApprove(a)) {
<p class="loi-kyc-error">
Enter the Resolution No. and Resolution Date the committee passed this decision under before approving.
</p>
}
<div class="loan-action-row">
<button
type="button"
class="pg-btn pg-btn--success"
[disabled]="!committeeReportsReady(a)"
(click)="approveStage(a, 'creditCommitteeApproval', 'Sanctioning Authority')"
[disabled]="!committeeCanApprove(a)"
(click)="approveCommitteeStage(a)"
>
Approve
</button>
@@ -1973,10 +2004,65 @@
</label>
</div>
@if (a.guarantorRequired) {
<label class="inv-form-field">
<span>Guarantor Name</span>
<input type="text" [(ngModel)]="a.guarantorName" />
</label>
<div class="loi-cic-subsection">
<div class="loi-cic-subsection-title">Guarantor — Must Be an Existing Share Member</div>
@if (!a.guarantorClientId) {
<label class="inv-form-field">
<span>Search Existing Client</span>
<input
type="text"
[(ngModel)]="guarantorSearchText"
(ngModelChange)="onGuarantorSearch($event)"
placeholder="Name, account no., mobile..."
/>
</label>
@if (searchingGuarantor) {
<p class="loi-search-hint">Searching…</p>
} @else if (guarantorSearchText.length >= 2 && guarantorSearchResults.length === 0) {
<p class="loi-search-hint">No clients found.</p>
}
@if (guarantorSearchResults.length > 0) {
<div class="loi-search-results">
@for (client of guarantorSearchResults; track client.id) {
<button type="button" class="loi-search-row" (click)="selectGuarantor(a, client)">
<span class="loi-search-name">{{ client.displayName }}</span>
<span class="loi-search-meta">{{ client.accountNo ?? client.officeName ?? '' }}</span>
</button>
}
</div>
}
<p class="loi-kyc-error">
The guarantor must already be a bank client with an active share account — search and select them
above, a name alone isn't enough.
</p>
} @else {
<label class="inv-form-field">
<span>Guarantor</span>
<div class="loi-selected-client">
<span>{{ a.guarantorName }}</span>
<button type="button" class="pg-btn" (click)="clearGuarantor(a)">Change</button>
</div>
</label>
@if (loadingGuarantorShareCheck) {
<p class="loi-search-hint">Checking share membership…</p>
} @else if (a.guarantorHasShareAccount) {
<span class="loan-badge loan-badge--verified">Share Member — Verified</span>
<div class="loi-share-account-list">
@for (sa of guarantorShareAccounts; track sa.id) {
<button type="button" class="loi-share-account-chip" (click)="viewShareAccountDetails(sa.id)">
{{ sa.accountNo }} — {{ sa.productName }}
</button>
}
</div>
} @else {
<span class="loan-badge loan-badge--missing">No Share Account Found</span>
<p class="loi-kyc-error">
This client has no active share account — a guarantor must already be a bank member. Pick a
different guarantor.
</p>
}
}
</div>
}
<label class="inv-form-field">
<span>Risk Remarks</span>
@@ -2028,6 +2114,98 @@
<input type="checkbox" [(ngModel)]="a.fileFilingDone" /> File Filing Done
</label>
</div>
@if (a.legalDocuments.length > 0) {
<div class="loi-cic-subsection">
<div class="loi-legal-doc-header">
<div class="loi-cic-subsection-title">Legal Documents</div>
<span class="loi-legal-doc-progress">
{{ legalDocsSubmittedCount(a) }} / {{ a.legalDocuments.length }} submitted
</span>
</div>
<div class="loi-legal-doc-grid">
@for (doc of a.legalDocuments; track doc.key) {
<div class="loi-legal-doc-card" [class.loi-legal-doc-card--submitted]="doc.submitted">
<div class="loi-legal-doc-title-row">
<span class="loi-legal-doc-icon">{{ legalDocIcon(doc.key) }}</span>
<span class="loi-legal-doc-title">{{ doc.label }}</span>
@if (doc.custom) {
<button
type="button"
class="loi-doc-remove"
title="Remove this document"
(click)="removeCustomLegalDoc(a, doc)"
>
✕
</button>
}
</div>
<div class="loi-kyc-card-files">
<label class="pg-btn pg-btn--ghost loi-legal-doc-upload-btn">
+ Add File(s)
<input
type="file"
accept="image/*,application/pdf"
multiple
hidden
(change)="onLegalDocFile(doc, $event)"
/>
</label>
@for (capture of legalDocCapture[doc.key]; track $index) {
<div class="loi-legal-doc-thumb-wrap">
<button
type="button"
class="loi-kyc-thumb"
title="View document"
(click)="viewNewSingleDoc(capture.file)"
>
@if (capture.previewUrl) {
<img [src]="capture.previewUrl" alt="{{ doc.label }} {{ $index + 1 }}" />
} @else {
<span class="loi-kyc-thumb-label">View file</span>
}
</button>
<button
type="button"
class="loi-legal-doc-thumb-remove"
title="Remove this file"
(click)="removeLegalDocFile(doc, $index)"
>
✕
</button>
</div>
}
</div>
@if (doc.submitted) {
<span class="loan-badge loan-badge--verified">
✓ Submitted
@if (legalDocCapture[doc.key]?.length) {
· {{ legalDocCapture[doc.key].length }} file{{
legalDocCapture[doc.key].length > 1 ? 's' : ''
}}
this session
}
</span>
} @else {
<span class="loan-badge loan-badge--missing">Missing</span>
}
</div>
}
</div>
<div class="loan-action-row">
<input
type="text"
[(ngModel)]="newLegalDocLabel"
placeholder="Add More Doc"
class="loi-custom-doc-input"
/>
<button type="button" class="pg-btn pg-btn--ghost" (click)="addCustomLegalDoc(a)">
+ Add Custom Document
</button>
</div>
</div>
}
<label class="inv-form-field">
<span>Agreement Signing Slot</span>
<input type="text" [(ngModel)]="a.agreementSigningSlot" placeholder="DD-MM-YYYY" />