Replaces the in-memory mock (LOAN_APPLICATIONS array + seeded RNG, reset on every refresh) with real Fineract-backed persistence: a new LoanOriginationService for CRUD plus one API call per stage transition, a loan-fineract-bridge that creates a real loan account (create + approve + disburse) at disbursement for Existing Customers, and file uploads through Fineract's generic document store keyed by a stable checklist slot so "On file"/View controls survive a reload. Also fixes several gaps found while verifying full-cycle persistence: - CIC charge-account selection never saved (missing persist() call) - Co-borrower relationship/monthly income lived in local-only component state instead of on the application, so they reset on refresh - Field Investigation "View Full Details" showed local-session photos only; stored photos are now downloaded and shown as real thumbnails - "Assign Field Officer" was free text; now a dropdown of real CBS loan officers pulled from /staff - site photos and cheque/signature images now open in the same zoomable, pannable lightgallery viewer used elsewhere in the app Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2526 lines
120 KiB
HTML
2526 lines
120 KiB
HTML
<!--
|
||
Copyright since 2025 Mifos Initiative
|
||
|
||
This Source Code Form is subject to the terms of the Mozilla Public
|
||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||
-->
|
||
|
||
<!-- (input)/(change) are bound once here rather than per field: both bubble, and Angular's own
|
||
ngModel listener sits on the input itself, so by the time this fires the model already holds
|
||
the new value. Any inline field on this screen — including ones added later — therefore saves
|
||
without needing its own handler wired up. -->
|
||
<div class="pg" (input)="onInlineEdit()" (change)="onInlineEdit()">
|
||
@if (loadingApplication) {
|
||
<p class="pg-sub">Loading application…</p>
|
||
}
|
||
@if (application; as a) {
|
||
<header class="pg-head">
|
||
<div>
|
||
<div class="pg-eyebrow">CREDIT · LOAN ORIGINATION · {{ a.branchName | uppercase }}</div>
|
||
<h1 class="pg-title">
|
||
{{ a.applicationNo }} — {{ a.applicantName }}
|
||
<span [class]="badgeClass(a)">{{ statusLabels[a.overallStatus] }}</span>
|
||
</h1>
|
||
<p class="pg-sub npa-report-note">
|
||
{{ a.productName }} · ₹{{ a.requestedAmount | number: '1.0-0' }} · Applied {{ a.applicationDate }} ·
|
||
{{ progress(a) }}% through the sanctioning workflow.
|
||
</p>
|
||
</div>
|
||
<div class="pg-toolbar">
|
||
@if (uploadingDocs > 0) {
|
||
<span class="loan-badge loan-badge--progress">
|
||
Uploading {{ uploadingDocs }} file{{ uploadingDocs > 1 ? 's' : '' }}…
|
||
</span>
|
||
} @else if (docUploadFailed) {
|
||
<span class="loan-badge loan-badge--rejected">File upload failed</span>
|
||
}
|
||
@if (saving) {
|
||
<span class="loan-badge loan-badge--progress">Saving…</span>
|
||
} @else if (saveFailed) {
|
||
<span class="loan-badge loan-badge--rejected">Not saved — check connection</span>
|
||
}
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="goBack()">‹ Back to Pipeline</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="pg-card">
|
||
<dl class="loan-kv-grid">
|
||
<div class="loan-kv-field">
|
||
<dt>Branch</dt>
|
||
<dd>{{ a.branchName }}</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Channel</dt>
|
||
<dd>
|
||
{{ a.channel === 'BC' ? a.bcAgentName : a.channel === 'SELF_APPLY' ? 'Self-Apply (Digital)' : 'Branch' }}
|
||
</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Customer Type</dt>
|
||
<dd>{{ a.customerType === 'EXISTING' ? 'Existing Customer' : 'New Customer' }}</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Tenure / Rate</dt>
|
||
<dd>{{ a.tenureMonths }} months @ {{ a.interestRate }}%</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Monthly Income / Eligible Amount</dt>
|
||
<dd>₹{{ a.monthlyIncome | number: '1.0-0' }} / ₹{{ a.eligibleLoanAmount | number: '1.0-0' }}</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Sanctioning Authority</dt>
|
||
<dd>{{ sanctioningAuthorityOf(a) }}</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Share Member</dt>
|
||
<dd>
|
||
@if (a.clientId) {
|
||
<!-- Real Existing Customer — checked against actual Fineract share accounts, not
|
||
the mock isShareMember fields every other (dummy-data) application uses. -->
|
||
@if (loadingShareAccounts) {
|
||
Checking membership…
|
||
} @else if (realShareAccounts.length > 0) {
|
||
<div class="loi-share-account-list">
|
||
@for (sa of realShareAccounts; 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--hold">Not a Member</span>
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="allotMembership(a)">Allot Shares</button>
|
||
}
|
||
} @else if (a.isShareMember) {
|
||
Yes — {{ a.shareCertNo }} (since {{ a.shareMemberSince }})
|
||
} @else {
|
||
<span class="loan-badge loan-badge--hold">Not a Member</span>
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="allotMembership(a)">Allot Shares</button>
|
||
}
|
||
</dd>
|
||
</div>
|
||
@if (a.coBorrowerRequired) {
|
||
<div class="loan-kv-field">
|
||
<dt>Co-Borrower</dt>
|
||
<dd>
|
||
{{ a.coBorrowerName }}
|
||
@if (a.coBorrowerWanted && a.coBorrowerRelationship) {
|
||
({{ a.coBorrowerRelationship }})
|
||
}
|
||
</dd>
|
||
</div>
|
||
}
|
||
@if (a.guarantorRequired) {
|
||
<div class="loan-kv-field">
|
||
<dt>Guarantor</dt>
|
||
<dd>{{ a.guarantorName }}</dd>
|
||
</div>
|
||
}
|
||
</dl>
|
||
</div>
|
||
|
||
<div class="pg-card">
|
||
<div class="pos-section-label">Sanctioning Workflow Progress</div>
|
||
<div class="loan-stepper">
|
||
@for (s of stageOrder; track s) {
|
||
@if (canJumpToStage(a, s)) {
|
||
<button
|
||
type="button"
|
||
[class]="stepStatusClass(a, s) + ' loan-step--clickable'"
|
||
title="Jump to {{ stageLabels[s] }} — nothing already entered is cleared"
|
||
(click)="goToStage(a, s)"
|
||
>
|
||
<span class="loan-step__connector"></span>
|
||
<span class="loan-step__dot">{{ stepMarker(a, s) }}</span>
|
||
<span class="loan-step__label">{{ stageLabels[s] }}</span>
|
||
</button>
|
||
} @else {
|
||
<div [class]="stepStatusClass(a, s)">
|
||
<span class="loan-step__connector"></span>
|
||
<span class="loan-step__dot">{{ stepMarker(a, s) }}</span>
|
||
<span class="loan-step__label">{{ stageLabels[s] }}</span>
|
||
</div>
|
||
}
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
@if (a.overallStatus === 'REJECTED') {
|
||
<div class="pg-card">
|
||
<div class="pos-section-label">Application Rejected — at {{ stageLabels[a.currentStage] }}</div>
|
||
<p class="pg-sub npa-report-note">See the approval summary below for the recorded reason.</p>
|
||
</div>
|
||
} @else if (a.overallStatus === 'ON_HOLD') {
|
||
<div class="pg-card">
|
||
<div class="pos-section-label">Personal Discussion — {{ discrepancyLabels[a.discrepancyType] }}</div>
|
||
<div class="loan-action-card">
|
||
<p>{{ a.personalDiscussionNotes }}</p>
|
||
<label class="inv-form-field">
|
||
<span>Resolution Notes</span>
|
||
<input type="text" [(ngModel)]="actionRemarks" placeholder="How was this resolved?" />
|
||
</label>
|
||
<div class="loan-action-row">
|
||
<button type="button" class="pg-btn pg-btn--success" (click)="resolveDiscrepancy(a)">
|
||
Resolve & Proceed to Loan Committee
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
} @else if (a.overallStatus === 'DISBURSED') {
|
||
<div class="pg-card">
|
||
<div class="pos-section-label">Disbursed</div>
|
||
<dl class="loan-kv-grid">
|
||
<div class="loan-kv-field">
|
||
<dt>Disbursement Date</dt>
|
||
<dd>{{ a.disbursementDate }}</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Mode</dt>
|
||
<dd>{{ a.disbursementMode }}</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>ACH Confirmation</dt>
|
||
<dd>{{ a.achConfirmationRef }}</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>EMI Activation Confirmation</dt>
|
||
<dd>{{ a.emiActivationConfirmed ? 'Confirmed on ' + a.emiActivationDate : 'Pending' }}</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Core Banking Loan Account</dt>
|
||
<dd>
|
||
@if (a.fineractLoanAccountNo) {
|
||
<span class="loan-badge loan-badge--verified">A/C {{ a.fineractLoanAccountNo }}</span>
|
||
} @else {
|
||
<span class="pg-sub">Mock disbursement only — no core banking account</span>
|
||
}
|
||
</dd>
|
||
</div>
|
||
</dl>
|
||
</div>
|
||
|
||
<div class="pg-card">
|
||
<div class="pos-section-label">EMI Repayment Schedule</div>
|
||
@if (isRevolving(a)) {
|
||
<p class="pg-sub npa-report-note">
|
||
{{ a.productName }} is a revolving/renewable limit — repaid on demand and renewed annually, not on a fixed
|
||
EMI schedule.
|
||
</p>
|
||
} @else {
|
||
<div class="inv-kpis npa-kpis--ratio">
|
||
<div class="inv-kpi">
|
||
<span class="inv-kpi__label">Loan Amount</span>
|
||
<span class="inv-kpi__value">₹{{ a.requestedAmount | number: '1.0-0' }}</span>
|
||
<span class="inv-kpi__sub">{{ a.tenureMonths }} months @ {{ a.interestRate }}%</span>
|
||
</div>
|
||
<div class="inv-kpi">
|
||
<span class="inv-kpi__label">Monthly EMI</span>
|
||
<span class="inv-kpi__value">₹{{ emiAmountFor(a) | number: '1.0-0' }}</span>
|
||
<span class="inv-kpi__sub">Reducing balance</span>
|
||
</div>
|
||
<div class="inv-kpi">
|
||
<span class="inv-kpi__label">Total Interest Payable</span>
|
||
<span class="inv-kpi__value">₹{{ totalInterestFor(a) | number: '1.0-0' }}</span>
|
||
<span class="inv-kpi__sub">over the full tenure</span>
|
||
</div>
|
||
<div class="inv-kpi">
|
||
<span class="inv-kpi__label">Total Amount Payable</span>
|
||
<span class="inv-kpi__value">₹{{ totalPayableFor(a) | number: '1.0-0' }}</span>
|
||
<span class="inv-kpi__sub">principal + interest</span>
|
||
</div>
|
||
</div>
|
||
<div class="pg-table-scroll loan-emi-table-scroll">
|
||
<table class="pg-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="num">Inst. No</th>
|
||
<th>Due Date</th>
|
||
<th class="num">Opening Balance</th>
|
||
<th class="num">EMI</th>
|
||
<th class="num">Principal</th>
|
||
<th class="num col-out">Interest</th>
|
||
<th class="num">Closing Balance</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@for (row of emiScheduleFor(a); track row.installmentNo) {
|
||
<tr>
|
||
<td class="num">{{ row.installmentNo }}</td>
|
||
<td>{{ row.dueDate }}</td>
|
||
<td class="num">₹{{ row.openingBalance | number: '1.0-0' }}</td>
|
||
<td class="num">₹{{ row.emiAmount | number: '1.0-0' }}</td>
|
||
<td class="num col-in">₹{{ row.principalComponent | number: '1.0-0' }}</td>
|
||
<td class="num col-out">₹{{ row.interestComponent | number: '1.0-0' }}</td>
|
||
<td class="num">₹{{ row.closingBalance | number: '1.0-0' }}</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
}
|
||
</div>
|
||
} @else {
|
||
<!-- ===================== APPLICATION ===================== -->
|
||
@if (a.currentStage === 'APPLICATION') {
|
||
<div class="pg-card">
|
||
<div class="pos-section-label">Application Received</div>
|
||
<div class="loan-action-card">
|
||
<p>
|
||
Received via
|
||
{{ a.channel === 'BC' ? a.bcAgentName : a.channel === 'SELF_APPLY' ? 'Self-Apply (Digital)' : 'Branch' }}
|
||
on {{ a.applicationDate }}.
|
||
</p>
|
||
<div class="loan-action-row">
|
||
<button
|
||
type="button"
|
||
class="pg-btn pg-btn--primary"
|
||
(click)="proceed(a, 'Application logged and forwarded for KYC.')"
|
||
>
|
||
Proceed to KYC & Document Collection
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- ===================== KYC & DOCUMENTS ===================== -->
|
||
@if (a.currentStage === 'KYC_DOCUMENTS') {
|
||
<div class="pg-card">
|
||
<div class="pos-section-label">KYC & Document Collection — OVD (RBI KYC Master Direction)</div>
|
||
<div class="loan-action-card">
|
||
@if (a.clientId) {
|
||
<!-- Real Existing Customer -- Aadhaar/PAN read straight from Fineract's KYC record
|
||
(verified status + masked number + click-to-view image); the other three
|
||
proofs are checked against the client's real uploaded documents where
|
||
available, falling back to the same local-only capture UI as a New Customer
|
||
application when nothing is on file yet. -->
|
||
@if (loadingKyc) {
|
||
<p class="pg-sub npa-report-note">Loading KYC record…</p>
|
||
} @else {
|
||
<div class="loi-kyc-doc-grid">
|
||
<div class="loi-kyc-card">
|
||
<span class="loi-kyc-card-title">Aadhaar</span>
|
||
<div class="loi-kyc-row">
|
||
<span
|
||
[class]="'loan-badge ' + (a.aadhaarVerified ? 'loan-badge--verified' : 'loan-badge--missing')"
|
||
>
|
||
{{ a.aadhaarVerified ? 'Verified' : 'Not Verified' }}
|
||
</span>
|
||
@if (realKyc?.aadhaarNumberMasked) {
|
||
<span class="loi-kyc-masked">{{ realKyc.aadhaarNumberMasked }}</span>
|
||
}
|
||
</div>
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="viewKycIdentifierDoc('aadhaar')">
|
||
View Document
|
||
</button>
|
||
</div>
|
||
<div class="loi-kyc-card">
|
||
<span class="loi-kyc-card-title">PAN</span>
|
||
<div class="loi-kyc-row">
|
||
<span [class]="'loan-badge ' + (a.panVerified ? 'loan-badge--verified' : 'loan-badge--missing')">
|
||
{{ a.panVerified ? 'Verified' : 'Not Verified' }}
|
||
</span>
|
||
@if (realKyc?.panNumber) {
|
||
<span class="loi-kyc-masked">{{ maskPan(realKyc.panNumber) }}</span>
|
||
}
|
||
</div>
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="viewKycIdentifierDoc('pan')">
|
||
View Document
|
||
</button>
|
||
</div>
|
||
<div class="loi-kyc-card">
|
||
<span class="loi-kyc-card-title">Income Proof</span>
|
||
@if (findDocument('income'); as incomeDoc) {
|
||
<span class="loan-badge loan-badge--verified">Submitted</span>
|
||
<span class="loi-kyc-masked">{{ incomeDoc.name || incomeDoc.fileName }}</span>
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="viewGenericDocument(incomeDoc)">
|
||
View
|
||
</button>
|
||
} @else {
|
||
<!-- Nothing on file in Fineract for this client -- let the officer capture it
|
||
here instead of just ticking a box with nothing behind it. -->
|
||
<label class="inv-form-field">
|
||
<span>Employment Type</span>
|
||
<select
|
||
[(ngModel)]="newKycCapture.incomeProof.employmentType"
|
||
(ngModelChange)="onIncomeProofEmploymentTypeChange(a)"
|
||
>
|
||
<option value="SALARIED">Salaried</option>
|
||
<option value="BUSINESS">Business / Self-Employed</option>
|
||
</select>
|
||
</label>
|
||
@if (newKycCapture.incomeProof.employmentType === 'SALARIED') {
|
||
<div class="loi-kyc-card-files">
|
||
<label class="pg-btn pg-btn--ghost">
|
||
{{
|
||
newKycCapture.incomeProof.salaried.file
|
||
? 'Form 16 / 3 Month Salary Slip ✓'
|
||
: 'Upload Form 16 / 3 Month Salary Slip'
|
||
}}
|
||
<input
|
||
type="file"
|
||
accept="image/*,application/pdf"
|
||
hidden
|
||
(change)="onNewSalariedProofFile(a, $event)"
|
||
/>
|
||
</label>
|
||
@if (newKycCapture.incomeProof.salaried.file || hasStoredDoc('KYC:incomeProof:salaried')) {
|
||
<button
|
||
type="button"
|
||
class="loi-kyc-thumb"
|
||
title="View document"
|
||
(click)="
|
||
viewKycSlot(a, newKycCapture.incomeProof.salaried.file, 'KYC:incomeProof:salaried')
|
||
"
|
||
>
|
||
@if (newKycCapture.incomeProof.salaried.previewUrl) {
|
||
<img [src]="newKycCapture.incomeProof.salaried.previewUrl" alt="Salary proof" />
|
||
} @else {
|
||
<span class="loi-kyc-thumb-label">View file</span>
|
||
}
|
||
</button>
|
||
}
|
||
</div>
|
||
} @else {
|
||
<p class="pg-sub npa-report-note">Last 3 years' Income Tax Returns:</p>
|
||
@for (doc of newKycCapture.incomeProof.itr; track $index) {
|
||
<div class="loi-kyc-card-files">
|
||
@if (!doc.exception) {
|
||
<label class="pg-btn pg-btn--ghost">
|
||
{{
|
||
doc.file ? 'ITR — Year ' + ($index + 1) + ' ✓' : 'Upload ITR — Year ' + ($index + 1)
|
||
}}
|
||
<input
|
||
type="file"
|
||
accept="image/*,application/pdf"
|
||
hidden
|
||
(change)="onNewItrFile(a, $index, $event)"
|
||
/>
|
||
</label>
|
||
@if (doc.file || hasStoredDoc('KYC:incomeProof:itr:' + $index)) {
|
||
<button
|
||
type="button"
|
||
class="loi-kyc-thumb"
|
||
title="View document"
|
||
(click)="viewKycSlot(a, doc.file, 'KYC:incomeProof:itr:' + $index)"
|
||
>
|
||
@if (doc.previewUrl) {
|
||
<img [src]="doc.previewUrl" alt="ITR Year {{ $index + 1 }}" />
|
||
} @else {
|
||
<span class="loi-kyc-thumb-label">View file</span>
|
||
}
|
||
</button>
|
||
}
|
||
}
|
||
@if (!doc.file) {
|
||
<label class="loan-checklist-item">
|
||
<input
|
||
type="checkbox"
|
||
[(ngModel)]="doc.exception"
|
||
(ngModelChange)="onItrExceptionChange(a)"
|
||
/>
|
||
Not Available — Add Exception
|
||
</label>
|
||
}
|
||
</div>
|
||
@if (doc.exception) {
|
||
<label class="inv-form-field">
|
||
<span>Exception Remark — Year {{ $index + 1 }}</span>
|
||
<input
|
||
type="text"
|
||
[(ngModel)]="doc.exceptionRemark"
|
||
(ngModelChange)="onItrExceptionChange(a)"
|
||
placeholder="e.g. Business started in 2024, ITR not filed for this year"
|
||
/>
|
||
</label>
|
||
}
|
||
}
|
||
}
|
||
@if (a.incomeProofSubmitted) {
|
||
@if (itrHasException) {
|
||
<span class="loan-badge loan-badge--missing">Submitted — with exception(s)</span>
|
||
} @else {
|
||
<span class="loan-badge loan-badge--verified">Submitted</span>
|
||
}
|
||
} @else {
|
||
<span class="loan-badge loan-badge--missing">Missing</span>
|
||
}
|
||
}
|
||
</div>
|
||
<div class="loi-kyc-card">
|
||
<span class="loi-kyc-card-title">Bank Statement</span>
|
||
@if (findDocument('bank statement'); as bankDoc) {
|
||
<span class="loan-badge loan-badge--verified">Submitted</span>
|
||
<span class="loi-kyc-masked">{{ bankDoc.name || bankDoc.fileName }}</span>
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="viewGenericDocument(bankDoc)">
|
||
View
|
||
</button>
|
||
} @else {
|
||
<div class="loi-kyc-card-files">
|
||
<label class="pg-btn pg-btn--ghost">
|
||
{{ newKycCapture.bankStatement.file ? 'Statement ✓' : 'Upload Bank Statement' }}
|
||
<input
|
||
type="file"
|
||
accept="image/*,application/pdf,.csv,text/csv"
|
||
hidden
|
||
(change)="onNewBankStatementFile(a, $event)"
|
||
/>
|
||
</label>
|
||
@if (newKycCapture.bankStatement.file || hasStoredDoc('KYC:bankStatement')) {
|
||
<button
|
||
type="button"
|
||
class="loi-kyc-thumb"
|
||
title="View document"
|
||
(click)="viewKycSlot(a, newKycCapture.bankStatement.file, 'KYC:bankStatement')"
|
||
>
|
||
@if (newKycCapture.bankStatement.previewUrl) {
|
||
<img [src]="newKycCapture.bankStatement.previewUrl" alt="Bank statement" />
|
||
} @else {
|
||
<span class="loi-kyc-thumb-label">View file</span>
|
||
}
|
||
</button>
|
||
@if (canReopenBankStatementAnalysis()) {
|
||
<button
|
||
type="button"
|
||
class="loi-bsa-reopen"
|
||
title="Re-open the transaction analysis"
|
||
(click)="reopenBankStatementAnalysisFor(a)"
|
||
>
|
||
📊 Re-check Analysis
|
||
</button>
|
||
}
|
||
}
|
||
</div>
|
||
@if (a.bankStatementSubmitted) {
|
||
<span class="loan-badge loan-badge--verified">Submitted</span>
|
||
} @else {
|
||
<span class="loan-badge loan-badge--missing">Missing</span>
|
||
}
|
||
}
|
||
</div>
|
||
<div class="loi-kyc-card">
|
||
<span class="loi-kyc-card-title">Electricity Bill (Address Proof)</span>
|
||
@if (findDocument('electricity') || findDocument('address proof'); as billDoc) {
|
||
<span class="loan-badge loan-badge--verified">Submitted</span>
|
||
<span class="loi-kyc-masked">{{ billDoc.name || billDoc.fileName }}</span>
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="viewGenericDocument(billDoc)">
|
||
View
|
||
</button>
|
||
} @else {
|
||
<label class="inv-form-field">
|
||
<span>Bill Date</span>
|
||
<input
|
||
type="date"
|
||
[(ngModel)]="newKycCapture.electricityBill.billDate"
|
||
(ngModelChange)="onElectricityBillDateChange(a)"
|
||
/>
|
||
</label>
|
||
@if (electricityBillTooOld) {
|
||
<p class="loi-kyc-error">Must not be older than 3 months.</p>
|
||
}
|
||
<div class="loi-kyc-card-files">
|
||
<label class="pg-btn pg-btn--ghost">
|
||
{{ newKycCapture.electricityBill.file ? 'Bill ✓' : 'Upload Bill' }}
|
||
<input
|
||
type="file"
|
||
accept="image/*,application/pdf"
|
||
hidden
|
||
(change)="onNewElectricityBillFile(a, $event)"
|
||
/>
|
||
</label>
|
||
@if (newKycCapture.electricityBill.file || hasStoredDoc('KYC:electricityBill')) {
|
||
<button
|
||
type="button"
|
||
class="loi-kyc-thumb"
|
||
title="View document"
|
||
(click)="viewKycSlot(a, newKycCapture.electricityBill.file, 'KYC:electricityBill')"
|
||
>
|
||
@if (newKycCapture.electricityBill.previewUrl) {
|
||
<img [src]="newKycCapture.electricityBill.previewUrl" alt="Electricity bill" />
|
||
} @else {
|
||
<span class="loi-kyc-thumb-label">View file</span>
|
||
}
|
||
</button>
|
||
}
|
||
</div>
|
||
@if (a.electricityBillSubmitted) {
|
||
<span class="loan-badge loan-badge--verified">Submitted</span>
|
||
} @else {
|
||
<span class="loan-badge loan-badge--missing">Missing</span>
|
||
}
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
} @else {
|
||
<!-- No real Fineract client linked to this application (whether it's a "New
|
||
Customer" intake or an "Existing Customer" mock application raised without a
|
||
real client pick) -- there's nowhere to read real KYC/documents from, so this
|
||
mirrors the client-creation KYC step's capture UX (front/back image + OTP) and
|
||
generic document upload, kept local-only/simulated. -->
|
||
<p class="loi-kyc-note">
|
||
Captured for this application — final Aadhaar/PAN verification happens when the customer's CBS client
|
||
profile is created.
|
||
</p>
|
||
<div class="loi-kyc-doc-grid">
|
||
<div class="loi-kyc-card">
|
||
<span class="loi-kyc-card-title">Aadhaar</span>
|
||
<div class="loi-kyc-card-files">
|
||
<label class="pg-btn pg-btn--ghost">
|
||
{{ newKycCapture.aadhaar.front ? 'Front ✓' : 'Upload Front' }}
|
||
<input
|
||
type="file"
|
||
accept="image/*,application/pdf"
|
||
hidden
|
||
(change)="onNewKycFile('aadhaar', 'front', $event)"
|
||
/>
|
||
</label>
|
||
<label class="pg-btn pg-btn--ghost">
|
||
{{ newKycCapture.aadhaar.back ? 'Back ✓' : 'Upload Back' }}
|
||
<input
|
||
type="file"
|
||
accept="image/*,application/pdf"
|
||
hidden
|
||
(change)="onNewKycFile('aadhaar', 'back', $event)"
|
||
/>
|
||
</label>
|
||
</div>
|
||
@if (newKycCapture.aadhaar.front || newKycCapture.aadhaar.back) {
|
||
<div class="loi-kyc-thumbs">
|
||
@if (newKycCapture.aadhaar.front) {
|
||
<button
|
||
type="button"
|
||
class="loi-kyc-thumb"
|
||
title="View front"
|
||
(click)="viewNewSingleDoc(newKycCapture.aadhaar.front)"
|
||
>
|
||
@if (newKycCapture.aadhaar.frontPreviewUrl) {
|
||
<img [src]="newKycCapture.aadhaar.frontPreviewUrl" alt="Aadhaar front" />
|
||
} @else {
|
||
<span class="loi-kyc-thumb-label">Front file</span>
|
||
}
|
||
</button>
|
||
}
|
||
@if (newKycCapture.aadhaar.back) {
|
||
<button
|
||
type="button"
|
||
class="loi-kyc-thumb"
|
||
title="View back"
|
||
(click)="viewNewSingleDoc(newKycCapture.aadhaar.back)"
|
||
>
|
||
@if (newKycCapture.aadhaar.backPreviewUrl) {
|
||
<img [src]="newKycCapture.aadhaar.backPreviewUrl" alt="Aadhaar back" />
|
||
} @else {
|
||
<span class="loi-kyc-thumb-label">Back file</span>
|
||
}
|
||
</button>
|
||
}
|
||
</div>
|
||
}
|
||
@if (newKycCapture.aadhaar.verified) {
|
||
<span class="loan-badge loan-badge--verified">Aadhaar Verified</span>
|
||
} @else {
|
||
<div class="loi-kyc-card-verify">
|
||
<input
|
||
type="text"
|
||
maxlength="12"
|
||
placeholder="Aadhaar Number"
|
||
[(ngModel)]="newKycCapture.aadhaar.number"
|
||
/>
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="sendNewAadhaarOtp()">Send OTP</button>
|
||
@if (newKycCapture.aadhaar.otpSent) {
|
||
<div class="loi-otp-digits">
|
||
@for (d of newKycCapture.aadhaar.otpDigits; track $index) {
|
||
<input
|
||
type="text"
|
||
maxlength="1"
|
||
[value]="d"
|
||
(input)="onNewAadhaarOtpDigit($index, $event)"
|
||
(keydown)="onNewAadhaarOtpKeydown($index, $event)"
|
||
/>
|
||
}
|
||
</div>
|
||
<button type="button" class="pg-btn pg-btn--primary" (click)="verifyNewAadhaarOtp(a)">
|
||
Verify
|
||
</button>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
<div class="loi-kyc-card">
|
||
<span class="loi-kyc-card-title">PAN</span>
|
||
<div class="loi-kyc-card-files">
|
||
<label class="pg-btn pg-btn--ghost">
|
||
{{ newKycCapture.pan.front ? 'Front ✓' : 'Upload Front' }}
|
||
<input
|
||
type="file"
|
||
accept="image/*,application/pdf"
|
||
hidden
|
||
(change)="onNewKycFile('pan', 'front', $event)"
|
||
/>
|
||
</label>
|
||
<label class="pg-btn pg-btn--ghost">
|
||
{{ newKycCapture.pan.back ? 'Back ✓' : 'Upload Back' }}
|
||
<input
|
||
type="file"
|
||
accept="image/*,application/pdf"
|
||
hidden
|
||
(change)="onNewKycFile('pan', 'back', $event)"
|
||
/>
|
||
</label>
|
||
</div>
|
||
@if (newKycCapture.pan.front || newKycCapture.pan.back) {
|
||
<div class="loi-kyc-thumbs">
|
||
@if (newKycCapture.pan.front) {
|
||
<button
|
||
type="button"
|
||
class="loi-kyc-thumb"
|
||
title="View front"
|
||
(click)="viewNewSingleDoc(newKycCapture.pan.front)"
|
||
>
|
||
@if (newKycCapture.pan.frontPreviewUrl) {
|
||
<img [src]="newKycCapture.pan.frontPreviewUrl" alt="PAN front" />
|
||
} @else {
|
||
<span class="loi-kyc-thumb-label">Front file</span>
|
||
}
|
||
</button>
|
||
}
|
||
@if (newKycCapture.pan.back) {
|
||
<button
|
||
type="button"
|
||
class="loi-kyc-thumb"
|
||
title="View back"
|
||
(click)="viewNewSingleDoc(newKycCapture.pan.back)"
|
||
>
|
||
@if (newKycCapture.pan.backPreviewUrl) {
|
||
<img [src]="newKycCapture.pan.backPreviewUrl" alt="PAN back" />
|
||
} @else {
|
||
<span class="loi-kyc-thumb-label">Back file</span>
|
||
}
|
||
</button>
|
||
}
|
||
</div>
|
||
}
|
||
@if (newKycCapture.pan.verified) {
|
||
<span class="loan-badge loan-badge--verified">PAN Verified</span>
|
||
} @else {
|
||
<div class="loi-kyc-card-verify">
|
||
<input
|
||
type="text"
|
||
maxlength="10"
|
||
placeholder="PAN Number"
|
||
[(ngModel)]="newKycCapture.pan.number"
|
||
/>
|
||
<button type="button" class="pg-btn pg-btn--primary" (click)="verifyNewPan(a)">Verify PAN</button>
|
||
</div>
|
||
}
|
||
</div>
|
||
<div class="loi-kyc-card">
|
||
<span class="loi-kyc-card-title">Income Proof</span>
|
||
<label class="inv-form-field">
|
||
<span>Employment Type</span>
|
||
<select
|
||
[(ngModel)]="newKycCapture.incomeProof.employmentType"
|
||
(ngModelChange)="onIncomeProofEmploymentTypeChange(a)"
|
||
>
|
||
<option value="SALARIED">Salaried</option>
|
||
<option value="BUSINESS">Business / Self-Employed</option>
|
||
</select>
|
||
</label>
|
||
@if (newKycCapture.incomeProof.employmentType === 'SALARIED') {
|
||
<div class="loi-kyc-card-files">
|
||
<label class="pg-btn pg-btn--ghost">
|
||
{{
|
||
newKycCapture.incomeProof.salaried.file
|
||
? 'Form 16 / 3 Month Salary Slip ✓'
|
||
: 'Upload Form 16 / 3 Month Salary Slip'
|
||
}}
|
||
<input
|
||
type="file"
|
||
accept="image/*,application/pdf"
|
||
hidden
|
||
(change)="onNewSalariedProofFile(a, $event)"
|
||
/>
|
||
</label>
|
||
@if (newKycCapture.incomeProof.salaried.file) {
|
||
<button
|
||
type="button"
|
||
class="loi-kyc-thumb"
|
||
title="View document"
|
||
(click)="viewNewSingleDoc(newKycCapture.incomeProof.salaried.file)"
|
||
>
|
||
@if (newKycCapture.incomeProof.salaried.previewUrl) {
|
||
<img [src]="newKycCapture.incomeProof.salaried.previewUrl" alt="Salary proof" />
|
||
} @else {
|
||
<span class="loi-kyc-thumb-label">View file</span>
|
||
}
|
||
</button>
|
||
}
|
||
</div>
|
||
} @else {
|
||
<p class="pg-sub npa-report-note">Last 3 years' Income Tax Returns:</p>
|
||
@for (doc of newKycCapture.incomeProof.itr; track $index) {
|
||
<div class="loi-kyc-card-files">
|
||
@if (!doc.exception) {
|
||
<label class="pg-btn pg-btn--ghost">
|
||
{{ doc.file ? 'ITR — Year ' + ($index + 1) + ' ✓' : 'Upload ITR — Year ' + ($index + 1) }}
|
||
<input
|
||
type="file"
|
||
accept="image/*,application/pdf"
|
||
hidden
|
||
(change)="onNewItrFile(a, $index, $event)"
|
||
/>
|
||
</label>
|
||
@if (doc.file) {
|
||
<button
|
||
type="button"
|
||
class="loi-kyc-thumb"
|
||
title="View document"
|
||
(click)="viewNewSingleDoc(doc.file)"
|
||
>
|
||
@if (doc.previewUrl) {
|
||
<img [src]="doc.previewUrl" alt="ITR Year {{ $index + 1 }}" />
|
||
} @else {
|
||
<span class="loi-kyc-thumb-label">View file</span>
|
||
}
|
||
</button>
|
||
}
|
||
}
|
||
@if (!doc.file) {
|
||
<label class="loan-checklist-item">
|
||
<input
|
||
type="checkbox"
|
||
[(ngModel)]="doc.exception"
|
||
(ngModelChange)="onItrExceptionChange(a)"
|
||
/>
|
||
Not Available — Add Exception
|
||
</label>
|
||
}
|
||
</div>
|
||
@if (doc.exception) {
|
||
<label class="inv-form-field">
|
||
<span>Exception Remark — Year {{ $index + 1 }}</span>
|
||
<input
|
||
type="text"
|
||
[(ngModel)]="doc.exceptionRemark"
|
||
(ngModelChange)="onItrExceptionChange(a)"
|
||
placeholder="e.g. Business started in 2024, ITR not filed for this year"
|
||
/>
|
||
</label>
|
||
}
|
||
}
|
||
}
|
||
@if (a.incomeProofSubmitted) {
|
||
@if (itrHasException) {
|
||
<span class="loan-badge loan-badge--missing">Submitted — with exception(s)</span>
|
||
} @else {
|
||
<span class="loan-badge loan-badge--verified">Submitted</span>
|
||
}
|
||
} @else {
|
||
<span class="loan-badge loan-badge--missing">Missing</span>
|
||
}
|
||
</div>
|
||
<div class="loi-kyc-card">
|
||
<span class="loi-kyc-card-title">Bank Statement</span>
|
||
<div class="loi-kyc-card-files">
|
||
<label class="pg-btn pg-btn--ghost">
|
||
{{ newKycCapture.bankStatement.file ? 'Statement ✓' : 'Upload Bank Statement' }}
|
||
<input
|
||
type="file"
|
||
accept="image/*,application/pdf,.csv,text/csv"
|
||
hidden
|
||
(change)="onNewBankStatementFile(a, $event)"
|
||
/>
|
||
</label>
|
||
@if (newKycCapture.bankStatement.file) {
|
||
<button
|
||
type="button"
|
||
class="loi-kyc-thumb"
|
||
title="View document"
|
||
(click)="viewNewSingleDoc(newKycCapture.bankStatement.file)"
|
||
>
|
||
@if (newKycCapture.bankStatement.previewUrl) {
|
||
<img [src]="newKycCapture.bankStatement.previewUrl" alt="Bank statement" />
|
||
} @else {
|
||
<span class="loi-kyc-thumb-label">View file</span>
|
||
}
|
||
</button>
|
||
@if (isCsvFile(newKycCapture.bankStatement.file)) {
|
||
<button
|
||
type="button"
|
||
class="loi-bsa-reopen"
|
||
title="Re-open the transaction analysis"
|
||
(click)="reopenBankStatementAnalysis(newKycCapture.bankStatement.file)"
|
||
>
|
||
📊 Re-check Analysis
|
||
</button>
|
||
}
|
||
}
|
||
</div>
|
||
@if (a.bankStatementSubmitted) {
|
||
<span class="loan-badge loan-badge--verified">Submitted</span>
|
||
} @else {
|
||
<span class="loan-badge loan-badge--missing">Missing</span>
|
||
}
|
||
</div>
|
||
<div class="loi-kyc-card">
|
||
<span class="loi-kyc-card-title">Electricity Bill (Address Proof)</span>
|
||
<label class="inv-form-field">
|
||
<span>Bill Date</span>
|
||
<input
|
||
type="date"
|
||
[(ngModel)]="newKycCapture.electricityBill.billDate"
|
||
(ngModelChange)="onElectricityBillDateChange(a)"
|
||
/>
|
||
</label>
|
||
@if (electricityBillTooOld) {
|
||
<p class="loi-kyc-error">Must not be older than 3 months.</p>
|
||
}
|
||
<div class="loi-kyc-card-files">
|
||
<label class="pg-btn pg-btn--ghost">
|
||
{{ newKycCapture.electricityBill.file ? 'Bill ✓' : 'Upload Bill' }}
|
||
<input
|
||
type="file"
|
||
accept="image/*,application/pdf"
|
||
hidden
|
||
(change)="onNewElectricityBillFile(a, $event)"
|
||
/>
|
||
</label>
|
||
@if (newKycCapture.electricityBill.file) {
|
||
<button
|
||
type="button"
|
||
class="loi-kyc-thumb"
|
||
title="View document"
|
||
(click)="viewNewSingleDoc(newKycCapture.electricityBill.file)"
|
||
>
|
||
@if (newKycCapture.electricityBill.previewUrl) {
|
||
<img [src]="newKycCapture.electricityBill.previewUrl" alt="Electricity bill" />
|
||
} @else {
|
||
<span class="loi-kyc-thumb-label">View file</span>
|
||
}
|
||
</button>
|
||
}
|
||
</div>
|
||
@if (a.electricityBillSubmitted) {
|
||
<span class="loan-badge loan-badge--verified">Submitted</span>
|
||
} @else {
|
||
<span class="loan-badge loan-badge--missing">Missing</span>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
@if (a.productDocuments.length > 0) {
|
||
<div class="loi-cic-subsection">
|
||
<div class="loi-cic-subsection-title">Loan-Specific Documents — {{ a.productName }}</div>
|
||
<div class="loi-kyc-doc-grid">
|
||
@for (doc of a.productDocuments; track doc.key) {
|
||
@if (doc.key === 'coOwnerKyc') {
|
||
<div class="loi-kyc-card">
|
||
<span class="loi-kyc-card-title">{{ doc.label }}</span>
|
||
@if (doc.hint) {
|
||
<span class="loi-kyc-masked">{{ doc.hint }}</span>
|
||
}
|
||
<label class="loan-checklist-item">
|
||
<input
|
||
type="checkbox"
|
||
[(ngModel)]="coOwnerCapture.hasCoOwner"
|
||
(ngModelChange)="onCoOwnerToggle(a)"
|
||
/>
|
||
This property has a co-owner
|
||
</label>
|
||
@if (coOwnerCapture.hasCoOwner) {
|
||
<div class="loi-kyc-card-files">
|
||
<button
|
||
type="button"
|
||
class="pg-btn pg-btn--ghost"
|
||
(click)="openCoOwnerVerify(a, 'aadhaar')"
|
||
>
|
||
{{ coOwnerCapture.aadhaar.verified ? 'Aadhaar ✓ Verified' : 'Verify Co-Owner Aadhaar' }}
|
||
</button>
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="openCoOwnerVerify(a, 'pan')">
|
||
{{ coOwnerCapture.pan.verified ? 'PAN ✓ Verified' : 'Verify Co-Owner PAN' }}
|
||
</button>
|
||
</div>
|
||
}
|
||
@if (doc.submitted) {
|
||
<span class="loan-badge loan-badge--verified">
|
||
{{ coOwnerCapture.hasCoOwner ? 'Verified' : 'Not Applicable' }}
|
||
</span>
|
||
} @else {
|
||
<span class="loan-badge loan-badge--missing">Missing</span>
|
||
}
|
||
</div>
|
||
} @else {
|
||
<div class="loi-kyc-card">
|
||
<span class="loi-kyc-card-title">
|
||
{{ doc.label }}
|
||
@if (doc.custom) {
|
||
<button
|
||
type="button"
|
||
class="loi-doc-remove"
|
||
title="Remove this document"
|
||
(click)="removeCustomProductDoc(a, doc)"
|
||
>
|
||
✕
|
||
</button>
|
||
}
|
||
</span>
|
||
@if (doc.hint) {
|
||
<span class="loi-kyc-masked">{{ doc.hint }}</span>
|
||
}
|
||
<div class="loi-kyc-card-files">
|
||
<label class="pg-btn pg-btn--ghost">
|
||
{{ productDocCapture[doc.key]?.file ? 'Uploaded ✓' : 'Upload' }}
|
||
<input
|
||
type="file"
|
||
accept="image/*,application/pdf"
|
||
hidden
|
||
(change)="onProductDocFile(doc, $event)"
|
||
/>
|
||
</label>
|
||
@if (productDocCapture[doc.key]; as capture) {
|
||
@if (capture.file || hasStoredDoc('PRODUCT_DOC:' + doc.key)) {
|
||
<button
|
||
type="button"
|
||
class="loi-kyc-thumb"
|
||
title="View document"
|
||
(click)="viewKycSlot(a, capture.file, 'PRODUCT_DOC:' + doc.key)"
|
||
>
|
||
@if (capture.previewUrl) {
|
||
<img [src]="capture.previewUrl" alt="{{ doc.label }}" />
|
||
} @else {
|
||
<span class="loi-kyc-thumb-label">View file</span>
|
||
}
|
||
</button>
|
||
}
|
||
}
|
||
</div>
|
||
@if (doc.submitted) {
|
||
<span class="loan-badge loan-badge--verified">Submitted</span>
|
||
} @else {
|
||
<span class="loan-badge loan-badge--missing">Missing</span>
|
||
}
|
||
</div>
|
||
}
|
||
}
|
||
</div>
|
||
<div class="loan-action-row">
|
||
<input
|
||
type="text"
|
||
[(ngModel)]="newCustomDocLabel"
|
||
placeholder="Add More Doc"
|
||
class="loi-custom-doc-input"
|
||
/>
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="addCustomProductDoc(a)">
|
||
+ Add Custom Document
|
||
</button>
|
||
</div>
|
||
</div>
|
||
}
|
||
<label class="inv-form-field">
|
||
<span>Monthly Income (₹)</span>
|
||
<input type="number" [(ngModel)]="monthlyIncomeInput" placeholder="e.g. 45000" />
|
||
</label>
|
||
@if (!isRevolving(a)) {
|
||
<label class="inv-form-field">
|
||
<span>Requested Loan Amount (₹)</span>
|
||
<input type="number" [(ngModel)]="requestedAmountInput" />
|
||
</label>
|
||
@if (amountOutOfRange(a)) {
|
||
<p class="loi-kyc-error">
|
||
{{ a.productName }} allows ₹{{ productLimitsFor(a)?.minAmt | number: '1.0-0' }} – ₹{{
|
||
productLimitsFor(a)?.maxAmt | number: '1.0-0'
|
||
}}
|
||
— this amount won't be saved until it's in range.
|
||
</p>
|
||
}
|
||
<label class="inv-form-field">
|
||
<span>Tenure (Months)</span>
|
||
<input type="number" [(ngModel)]="tenureMonthsInput" />
|
||
</label>
|
||
@if (tenureOutOfRange(a)) {
|
||
<p class="loi-kyc-error">
|
||
{{ a.productName }} allows {{ productLimitsFor(a)?.minTenure }}–{{ productLimitsFor(a)?.maxTenure }}
|
||
months — this tenure won't be saved until it's in range.
|
||
</p>
|
||
}
|
||
}
|
||
<p class="pg-sub npa-report-note">
|
||
Eligible Loan Amount preview: ₹{{ eligiblePreview(a) | number: '1.0-0' }}
|
||
</p>
|
||
@if (isRevolving(a)) {
|
||
<p class="pg-sub npa-report-note">
|
||
{{ a.productName }} is a revolving/renewable limit — repaid on demand, no fixed EMI.
|
||
</p>
|
||
} @else {
|
||
<p class="pg-sub npa-report-note">
|
||
Estimated EMI: ₹{{ emiPreview(a) | number: '1.0-0' }} / month ({{
|
||
emiToIncomePctPreview(a) | number: '1.0-1'
|
||
}}% of monthly income)
|
||
</p>
|
||
@if (emiToIncomePctPreview(a) > 40) {
|
||
@if (a.coBorrowerRequired && coBorrowerDetailsComplete(a) && combinedEligibilityOk(a)) {
|
||
<p class="pg-sub npa-report-note">
|
||
EMI exceeds 40% of the applicant's own income, but the Co-Borrower added below already covers it —
|
||
see the Co-Borrower section.
|
||
</p>
|
||
} @else {
|
||
<p class="loi-kyc-error">
|
||
EMI exceeds 40% of monthly income — add a Co-Borrower, reduce the loan amount, or increase the
|
||
tenure before proceeding.
|
||
</p>
|
||
}
|
||
}
|
||
}
|
||
<div class="loan-action-row">
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="setIncomeNow(a)">
|
||
{{ a.monthlyIncome > 0 ? 'Update Income & Terms' : 'Assess Income & Calculate Eligible Amount' }}
|
||
</button>
|
||
</div>
|
||
@if (a.monthlyIncome > 0) {
|
||
<dl class="loan-kv-grid" style="padding: 0 0 14px">
|
||
<div class="loan-kv-field">
|
||
<dt>Monthly Income (Last Assessed)</dt>
|
||
<dd>₹{{ a.monthlyIncome | number: '1.0-0' }}</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Eligible Loan Amount</dt>
|
||
<dd>₹{{ a.eligibleLoanAmount | number: '1.0-0' }}</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Estimated EMI</dt>
|
||
<dd>
|
||
@if (isRevolving(a)) {
|
||
No fixed EMI (revolving limit)
|
||
} @else {
|
||
₹{{ emiAmountFor(a) | number: '1.0-0' }} / month
|
||
}
|
||
</dd>
|
||
</div>
|
||
</dl>
|
||
}
|
||
@if (!isRevolving(a)) {
|
||
<div class="loi-emi-breakdown">
|
||
<div class="loi-emi-breakdown-title">EMI Calculation Basis</div>
|
||
<dl class="loan-kv-grid">
|
||
<div class="loan-kv-field">
|
||
<dt>Principal (Loan Amount)</dt>
|
||
<dd>₹{{ a.requestedAmount | number: '1.0-0' }}</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Interest Rate</dt>
|
||
<dd>{{ a.interestRate }}% p.a. ({{ a.interestRate / 12 | number: '1.2-2' }}% / month)</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Tenure</dt>
|
||
<dd>{{ a.tenureMonths }} months</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Total Interest Payable</dt>
|
||
<dd>₹{{ totalInterestFor(a) | number: '1.0-0' }}</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Total Amount Payable</dt>
|
||
<dd>₹{{ totalPayableFor(a) | number: '1.0-0' }}</dd>
|
||
</div>
|
||
</dl>
|
||
<p class="pg-sub npa-report-note loi-emi-formula">
|
||
Reducing-balance EMI formula: EMI = [P × r × (1+r)<sup>n</sup>] / [(1+r)<sup>n</sup> − 1] — where P =
|
||
Principal, r = Monthly Interest Rate, n = Tenure in months. On P = ₹{{
|
||
a.requestedAmount | number: '1.0-0'
|
||
}}, r = {{ a.interestRate / 12 / 100 | number: '1.4-4' }}, n = {{ a.tenureMonths }} → EMI = ₹{{
|
||
emiAmountFor(a) | number: '1.0-0'
|
||
}}
|
||
/ month.
|
||
</p>
|
||
</div>
|
||
}
|
||
@if (a.coBorrowerRequired) {
|
||
<div class="loi-cic-subsection">
|
||
<div class="loi-cic-subsection-title">Co-Borrower (raises household eligibility)</div>
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [(ngModel)]="a.coBorrowerWanted" />
|
||
Add a Co-Borrower to increase eligibility
|
||
</label>
|
||
@if (a.coBorrowerWanted) {
|
||
<label class="inv-form-field">
|
||
<span>Co-Borrower Name</span>
|
||
<input type="text" [(ngModel)]="a.coBorrowerName" placeholder="Full name" />
|
||
</label>
|
||
<label class="inv-form-field">
|
||
<span>Relationship with Applicant</span>
|
||
<select [(ngModel)]="a.coBorrowerRelationship">
|
||
<option value="">Select</option>
|
||
<option value="Father">Father</option>
|
||
<option value="Mother">Mother</option>
|
||
<option value="Wife">Wife</option>
|
||
<option value="Husband">Husband</option>
|
||
<option value="Son">Son</option>
|
||
<option value="Daughter">Daughter</option>
|
||
<option value="Other">Other</option>
|
||
</select>
|
||
</label>
|
||
<label class="inv-form-field">
|
||
<span>Co-Borrower Monthly Income (₹)</span>
|
||
<input type="number" [(ngModel)]="a.coBorrowerMonthlyIncome" placeholder="e.g. 30000" />
|
||
</label>
|
||
<p class="pg-sub npa-report-note">Upload the co-borrower's last 3 months' salary slips:</p>
|
||
<div class="loi-kyc-card-files">
|
||
@for (doc of coBorrowerCapture.salarySlips; track $index) {
|
||
<label class="pg-btn pg-btn--ghost">
|
||
{{
|
||
doc.file
|
||
? 'Salary Slip — Month ' + ($index + 1) + ' ✓'
|
||
: 'Upload Salary Slip — Month ' + ($index + 1)
|
||
}}
|
||
<input
|
||
type="file"
|
||
accept="image/*,application/pdf"
|
||
hidden
|
||
(change)="onCoBorrowerSalarySlipFile($index, $event)"
|
||
/>
|
||
</label>
|
||
@if (doc.file || hasStoredDoc('COBORROWER:salarySlip:' + $index)) {
|
||
<button
|
||
type="button"
|
||
class="loi-kyc-thumb"
|
||
title="View document"
|
||
(click)="viewKycSlot(a, doc.file, 'COBORROWER:salarySlip:' + $index)"
|
||
>
|
||
@if (doc.previewUrl) {
|
||
<img [src]="doc.previewUrl" alt="Salary Slip Month {{ $index + 1 }}" />
|
||
} @else {
|
||
<span class="loi-kyc-thumb-label">View file</span>
|
||
}
|
||
</button>
|
||
}
|
||
}
|
||
</div>
|
||
@if (coBorrowerDetailsComplete(a)) {
|
||
<div class="loi-emi-breakdown">
|
||
<div class="loi-emi-breakdown-title">Combined Household Eligibility Calculation</div>
|
||
<dl class="loan-kv-grid">
|
||
<div class="loan-kv-field">
|
||
<dt>Applicant's Monthly Income</dt>
|
||
<dd>₹{{ a.monthlyIncome | number: '1.0-0' }}</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Applicant's EMI Capacity (40%)</dt>
|
||
<dd>₹{{ applicantEmiCapacity(a) | number: '1.0-0' }}</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Co-Borrower's Monthly Income</dt>
|
||
<dd>₹{{ a.coBorrowerMonthlyIncome | number: '1.0-0' }}</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Co-Borrower's EMI Capacity (40%)</dt>
|
||
<dd>₹{{ coBorrowerEmiCapacity(a) | number: '1.0-0' }}</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Combined Household EMI Capacity</dt>
|
||
<dd>₹{{ combinedEmiCapacity(a) | number: '1.0-0' }}</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Estimated EMI (this loan)</dt>
|
||
<dd>₹{{ emiAmountFor(a) | number: '1.0-0' }}</dd>
|
||
</div>
|
||
</dl>
|
||
<p class="pg-sub npa-report-note loi-emi-formula">
|
||
Combined Capacity = (40% × ₹{{ a.monthlyIncome | number: '1.0-0' }} applicant) + (40% × ₹{{
|
||
a.coBorrowerMonthlyIncome | number: '1.0-0'
|
||
}}
|
||
co-borrower) = ₹{{ applicantEmiCapacity(a) | number: '1.0-0' }} + ₹{{
|
||
coBorrowerEmiCapacity(a) | number: '1.0-0'
|
||
}}
|
||
= ₹{{ combinedEmiCapacity(a) | number: '1.0-0' }}. Estimated EMI ₹{{
|
||
emiAmountFor(a) | number: '1.0-0'
|
||
}}
|
||
is {{ combinedEligibilityOk(a) ? 'within' : 'above' }} this combined capacity.
|
||
</p>
|
||
@if (combinedEligibilityOk(a)) {
|
||
<span class="loan-badge loan-badge--verified">Eligible with Co-Borrower</span>
|
||
} @else {
|
||
<span class="loan-badge loan-badge--missing"
|
||
>Still not eligible — increase co-borrower income, reduce the amount, or extend tenure</span
|
||
>
|
||
}
|
||
</div>
|
||
}
|
||
}
|
||
@if (!affordabilityOk(a)) {
|
||
<p class="loi-kyc-error">
|
||
@if (!coBorrowerDetailsComplete(a)) {
|
||
@if (!a.coBorrowerWanted) {
|
||
Check "Add a Co-Borrower" and complete their details to proceed.
|
||
} @else {
|
||
Add the co-borrower's name, relationship, monthly income and all 3 salary slips to proceed.
|
||
}
|
||
} @else {
|
||
Combined household income still doesn't cover the EMI at 40% — adjust the amount, tenure, or
|
||
co-borrower before proceeding.
|
||
}
|
||
</p>
|
||
}
|
||
</div>
|
||
}
|
||
<div class="loan-action-row">
|
||
<button
|
||
type="button"
|
||
class="pg-btn pg-btn--primary"
|
||
[disabled]="!allKycDone(a)"
|
||
(click)="completeKycStage(a)"
|
||
>
|
||
Proceed to CIC / Credit Bureau Check
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- ===================== CIC / CREDIT BUREAU CHECK ===================== -->
|
||
@if (a.currentStage === 'CIC_CHECK') {
|
||
<div class="pg-card">
|
||
<div class="pos-section-label">CIC / Credit Bureau Check — {{ a.cicBureau }}</div>
|
||
<div class="loan-action-card">
|
||
<!-- Consent and charge stay visible for the whole CIC stage, not just until the score
|
||
is checked -- same layout either way, so what was collected (and by whom) is never
|
||
a click away from disappearing once the score card below takes over. -->
|
||
<div class="loi-cic-subsection">
|
||
<div class="loi-cic-subsection-title">Customer Consent — Credit Bureau Check</div>
|
||
<p class="pg-sub npa-report-note">
|
||
RBI's Fair Practices Code requires the customer's signed consent before a credit bureau pull. Generate
|
||
the declaration letter below, get it signed by the customer, then upload the signed copy.
|
||
</p>
|
||
<div class="loan-action-row">
|
||
@if (a.channel === 'BRANCH') {
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="printCicConsentLetter(a)">
|
||
Print Declaration Letter
|
||
</button>
|
||
} @else {
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="downloadCicConsentLetter(a)">
|
||
Download Declaration Letter
|
||
</button>
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="shareCicConsentLetter(a)">
|
||
Share (Email / WhatsApp / Drive…)
|
||
</button>
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="emailCicConsentLetter(a)">Email</button>
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="whatsappCicConsentLetter(a)">
|
||
WhatsApp
|
||
</button>
|
||
}
|
||
<label class="pg-btn pg-btn--ghost">
|
||
{{ cicConsentCapture.file ? 'Signed Declaration ✓' : 'Upload Signed Declaration' }}
|
||
<input type="file" accept="image/*,application/pdf" hidden (change)="onCicConsentFile(a, $event)" />
|
||
</label>
|
||
</div>
|
||
@if (cicConsentCapture.file || hasStoredDoc('CIC:consent')) {
|
||
<div class="loi-kyc-thumbs">
|
||
<button
|
||
type="button"
|
||
class="loi-kyc-thumb"
|
||
title="View document"
|
||
(click)="viewKycSlot(a, cicConsentCapture.file, 'CIC:consent')"
|
||
>
|
||
@if (cicConsentCapture.previewUrl) {
|
||
<img [src]="cicConsentCapture.previewUrl" alt="Signed consent declaration" />
|
||
} @else {
|
||
<span class="loi-kyc-thumb-label">View file</span>
|
||
}
|
||
</button>
|
||
</div>
|
||
}
|
||
@if (a.cicConsentSigned) {
|
||
<span class="loan-badge loan-badge--verified">Signed Declaration Uploaded</span>
|
||
} @else {
|
||
<span class="loan-badge loan-badge--missing">Awaiting Signed Declaration</span>
|
||
}
|
||
</div>
|
||
|
||
<div class="loi-cic-subsection">
|
||
<div class="loi-cic-subsection-title">CIC Check Charge — ₹{{ cicCheckCharge }}</div>
|
||
@if (a.clientId && realDebitableAccounts.length > 0) {
|
||
<!-- Existing Customer with a real account -- pick which one to debit. This
|
||
pipeline never posts an actual Fineract transaction, so the "debit" is
|
||
recorded as a label for audit only. -->
|
||
<p class="pg-sub npa-report-note">
|
||
Select which account to debit the ₹{{ cicCheckCharge }} charge from:
|
||
</p>
|
||
<div class="loi-kyc-doc-grid">
|
||
@for (acc of realDebitableAccounts; track acc.id) {
|
||
<button
|
||
type="button"
|
||
class="loi-kyc-card loi-charge-account"
|
||
[class.loi-charge-account--selected]="a.cicChargeAccountLabel.startsWith(acc.accountNo)"
|
||
(click)="selectCicChargeAccount(a, acc)"
|
||
>
|
||
<span class="loi-kyc-card-title">{{ acc.kind }}</span>
|
||
<span class="loi-kyc-masked">{{ acc.accountNo }} — {{ acc.productName }}</span>
|
||
<span class="loi-kyc-masked">Balance: ₹{{ acc.accountBalance | number: '1.0-0' }}</span>
|
||
</button>
|
||
}
|
||
</div>
|
||
} @else {
|
||
<!-- New Customer, or an Existing Customer with no open account on file --
|
||
collect the charge the way a real-world merchant would: a payment link the
|
||
customer pays externally, with the reference they hand back recorded as
|
||
proof. -->
|
||
@if (a.clientId && loadingShareAccounts) {
|
||
<p class="pg-sub npa-report-note">Loading accounts…</p>
|
||
} @else {
|
||
@if (a.clientId) {
|
||
<p class="pg-sub npa-report-note">
|
||
No open savings/current account on file for this customer — collect the charge via payment link
|
||
instead.
|
||
</p>
|
||
}
|
||
@if (!a.cicPaymentLinkUrl || a.cicPaymentLinkUrl === '—') {
|
||
<div class="loan-action-row">
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="generateCicPaymentLink(a)">
|
||
Generate Payment Link
|
||
</button>
|
||
</div>
|
||
} @else {
|
||
<p class="loi-kyc-masked">{{ a.cicPaymentLinkUrl }}</p>
|
||
<div class="loan-action-row">
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="copyCicPaymentLink(a)">
|
||
Copy Link
|
||
</button>
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="emailCicPaymentLink(a)">Email</button>
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="whatsappCicPaymentLink(a)">
|
||
WhatsApp
|
||
</button>
|
||
</div>
|
||
@if (!a.cicChargeCollected) {
|
||
<label class="inv-form-field">
|
||
<span>Payment Reference No. (from customer)</span>
|
||
<input type="text" [(ngModel)]="cicPaymentRefInput" placeholder="e.g. UTR / Txn ID" />
|
||
</label>
|
||
<div class="loan-action-row">
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="confirmCicPaymentReceived(a)">
|
||
Confirm Payment Received
|
||
</button>
|
||
</div>
|
||
}
|
||
}
|
||
}
|
||
}
|
||
@if (a.cicChargeCollected) {
|
||
<span class="loan-badge loan-badge--verified">
|
||
Charge Collected —
|
||
{{ a.cicChargeMode === 'ACCOUNT_DEBIT' ? a.cicChargeAccountLabel : 'Ref ' + a.cicPaymentReference }}
|
||
</span>
|
||
} @else {
|
||
<span class="loan-badge loan-badge--missing">Charge Not Collected</span>
|
||
}
|
||
</div>
|
||
|
||
@if (a.cibilScore === 0) {
|
||
<label class="inv-form-field">
|
||
<span>Bureau</span>
|
||
<select [(ngModel)]="a.cicBureau">
|
||
@for (bureau of cicBureaus; track bureau) {
|
||
<option [value]="bureau">{{ bureau }}</option>
|
||
}
|
||
</select>
|
||
</label>
|
||
<div class="loan-action-row">
|
||
<button
|
||
type="button"
|
||
class="pg-btn pg-btn--primary"
|
||
[disabled]="!a.cicConsentSigned || !a.cicChargeCollected"
|
||
(click)="checkCreditScoreNow(a)"
|
||
>
|
||
Check Credit Score
|
||
</button>
|
||
</div>
|
||
@if (!a.cicConsentSigned || !a.cicChargeCollected) {
|
||
<p class="pg-sub npa-report-note">
|
||
Print/share the declaration and get it signed, and collect the ₹{{ cicCheckCharge }} check charge,
|
||
before checking the credit score.
|
||
</p>
|
||
}
|
||
} @else {
|
||
<div class="loi-cic-hero-card">
|
||
<div
|
||
class="loi-cic-score-badge"
|
||
[class.loi-cic-score-badge--excellent]="a.cibilScore >= 750"
|
||
[class.loi-cic-score-badge--good]="a.cibilScore >= 650 && a.cibilScore < 750"
|
||
[class.loi-cic-score-badge--fair]="a.cibilScore >= 550 && a.cibilScore < 650"
|
||
[class.loi-cic-score-badge--poor]="a.cibilScore < 550"
|
||
>
|
||
<span class="loi-cic-score-num">{{ a.cibilScore }}</span>
|
||
<span class="loi-cic-score-band">{{ cibilBandOf(a.cibilScore) }}</span>
|
||
</div>
|
||
|
||
<div class="loi-cic-details-grid">
|
||
<div class="loi-cic-detail-item">
|
||
<span class="loi-cic-label">Credit Bureau</span>
|
||
<strong class="loi-cic-val">{{ a.cicBureau }}</strong>
|
||
</div>
|
||
<div class="loi-cic-detail-item">
|
||
<span class="loi-cic-label">Uploaded Report File</span>
|
||
<strong class="loi-cic-val loi-cic-file-name">{{
|
||
a.cicReportUploaded ? a.cicReportFileName : 'Not uploaded yet'
|
||
}}</strong>
|
||
</div>
|
||
<div class="loi-cic-detail-item">
|
||
<span class="loi-cic-label">Field Inquiry Status</span>
|
||
<strong class="loi-cic-val">
|
||
{{
|
||
a.fiRequestPushed
|
||
? 'Pushed to ' + a.fiAgencyAssigned + ' (' + a.fiRequestDate + ')'
|
||
: 'Not pushed yet'
|
||
}}
|
||
</strong>
|
||
</div>
|
||
</div>
|
||
|
||
@if (a.cicReportUploaded) {
|
||
<div class="loi-cic-action-box">
|
||
<button
|
||
type="button"
|
||
class="pg-btn pg-btn--primary loi-cic-analysis-btn"
|
||
(click)="openCicReportAnalysis(a)"
|
||
>
|
||
📊 View Analysis
|
||
</button>
|
||
@if (hasStoredDoc('CIC:report')) {
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="viewKycSlot(a, null, 'CIC:report')">
|
||
View file
|
||
</button>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
@if (a.cibilScore > 0) {
|
||
<div class="loan-action-row loi-cic-action-row">
|
||
<label
|
||
class="pg-btn pg-btn--ghost loi-cic-upload-btn"
|
||
title="Upload or change PDF Credit Bureau Report"
|
||
>
|
||
<span>{{
|
||
a.cicReportUploaded ? '📄 Re-upload PDF Report' : 'Upload ' + a.cicBureau + ' Report (PDF)'
|
||
}}</span>
|
||
<input type="file" hidden accept=".pdf,.jpg,.jpeg,.png" (change)="onCicFileSelected($event, a)" />
|
||
</label>
|
||
@if (a.cicReportUploaded && !a.fiRequestPushed) {
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="pushFieldInquiryNow(a)">
|
||
Push Field Inquiry to Agency
|
||
</button>
|
||
} @else if (a.cicReportUploaded && a.fiRequestPushed) {
|
||
<button type="button" class="pg-btn pg-btn--primary" (click)="completeCicStage(a)">
|
||
Proceed to Field Investigation
|
||
</button>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- ===================== FIELD INVESTIGATION ===================== -->
|
||
@if (a.currentStage === 'FIELD_INVESTIGATION') {
|
||
<div class="pg-card">
|
||
<div class="pos-section-label">Field Investigation (FI) — Agent Report</div>
|
||
<div class="loan-action-card">
|
||
@if (a.clientId) {
|
||
<div class="loi-cic-subsection">
|
||
<div class="loi-cic-subsection-title">Registered Addresses on File (Fineract)</div>
|
||
@if (loadingAddresses) {
|
||
<p class="pg-sub npa-report-note">Loading addresses…</p>
|
||
} @else if (realAddresses.length === 0) {
|
||
<p class="pg-sub npa-report-note">No addresses on file for this client.</p>
|
||
} @else {
|
||
<div class="loi-fi-grid">
|
||
@for (addr of realAddresses; track $index) {
|
||
<div class="loi-fi-card">
|
||
<div class="loi-fi-card-title">{{ addr.addressType || 'Address' }}</div>
|
||
<p class="pg-sub npa-report-note">{{ formatRealAddress(addr) }}</p>
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [(ngModel)]="a.hasShop" />
|
||
Applicant has a shop / business premises
|
||
</label>
|
||
|
||
<div class="loi-fi-grid">
|
||
@for (key of fiRows(a); track key) {
|
||
<div class="loi-fi-card">
|
||
<div class="loi-fi-card-title">{{ fiCheckLabels[key] }}</div>
|
||
|
||
@if (a[key].officerAssigned === '—') {
|
||
<div class="inv-form-grid">
|
||
<label class="inv-form-field">
|
||
<span>Assign Field Officer</span>
|
||
<select [(ngModel)]="fiOfficerNameInput[key]">
|
||
<option value="">Select an officer</option>
|
||
@for (staffMember of fiStaffOptions; track staffMember.id) {
|
||
<option [value]="staffMember.displayName">{{ staffMember.displayName }}</option>
|
||
}
|
||
</select>
|
||
</label>
|
||
</div>
|
||
<div class="loan-action-row">
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="assignFieldOfficerNow(a, key)">
|
||
Assign Officer
|
||
</button>
|
||
</div>
|
||
} @else {
|
||
<p class="pg-sub npa-report-note">
|
||
Assigned to <strong>{{ a[key].officerAssigned }}</strong> on {{ a[key].assignedDate }}
|
||
</p>
|
||
|
||
@if (a[key].status === 'PENDING') {
|
||
@if (fiIsOverdue(a, key)) {
|
||
<p class="loi-kyc-error">⚠ {{ fiSlaLabel(a, key) }} — this visit is overdue.</p>
|
||
} @else {
|
||
<p class="pg-sub npa-report-note">{{ fiSlaLabel(a, key) }}</p>
|
||
}
|
||
|
||
<div class="inv-form-grid">
|
||
<label class="inv-form-field">
|
||
<span>Latitude</span>
|
||
<input type="text" [(ngModel)]="a[key].latitude" placeholder="e.g. 23.259933" />
|
||
</label>
|
||
<label class="inv-form-field">
|
||
<span>Longitude</span>
|
||
<input type="text" [(ngModel)]="a[key].longitude" placeholder="e.g. 77.412615" />
|
||
</label>
|
||
</div>
|
||
|
||
<p class="pg-sub npa-report-note">Site Photos ({{ fiPhotoCapture[key].length }}):</p>
|
||
<div class="loi-kyc-card-files">
|
||
<label class="pg-btn pg-btn--ghost">
|
||
Add Photo(s)
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
multiple
|
||
hidden
|
||
(change)="onFiPhotoFile(a, key, $event)"
|
||
/>
|
||
</label>
|
||
@for (photo of fiPhotoCapture[key]; track $index) {
|
||
<button
|
||
type="button"
|
||
class="loi-kyc-thumb"
|
||
title="Remove photo"
|
||
(click)="removeFiPhoto(a, key, $index)"
|
||
>
|
||
@if (photo.previewUrl) {
|
||
<img [src]="photo.previewUrl" alt="Site photo {{ $index + 1 }}" />
|
||
} @else {
|
||
<span class="loi-kyc-thumb-label">File</span>
|
||
}
|
||
</button>
|
||
}
|
||
</div>
|
||
|
||
<div class="loan-action-row">
|
||
<button
|
||
type="button"
|
||
class="pg-btn pg-btn--primary"
|
||
[disabled]="!fiCanVerify(a, key)"
|
||
(click)="verifyFieldVisit(a, key)"
|
||
>
|
||
Mark Verified — Address Correct
|
||
</button>
|
||
</div>
|
||
@if (!fiCanVerify(a, key)) {
|
||
<p class="loi-kyc-error">
|
||
Capture latitude, longitude and at least one site photo before marking Verified.
|
||
</p>
|
||
}
|
||
|
||
<label class="inv-form-field">
|
||
<span>Reject — Remark (if address is incorrect)</span>
|
||
<input
|
||
type="text"
|
||
[(ngModel)]="fiRejectRemarkInput[key]"
|
||
placeholder="e.g. House locked, neighbours confirm a different family lives there"
|
||
/>
|
||
</label>
|
||
<div class="loan-action-row">
|
||
<button
|
||
type="button"
|
||
class="pg-btn pg-btn--ghost"
|
||
[disabled]="!fiRejectRemarkInput[key].trim()"
|
||
(click)="rejectFieldVisit(a, key)"
|
||
>
|
||
Reject with Remark
|
||
</button>
|
||
</div>
|
||
} @else if (a[key].status === 'VERIFIED') {
|
||
<span class="loan-badge loan-badge--verified">Verified</span>
|
||
<button type="button" class="loi-fi-view-details" (click)="openFiVisitDetails(a, key)">
|
||
📋 View Full Details
|
||
</button>
|
||
} @else {
|
||
<span class="loan-badge loan-badge--missing">Failed</span>
|
||
<button type="button" class="loi-fi-view-details" (click)="openFiVisitDetails(a, key)">
|
||
📋 View Full Details
|
||
</button>
|
||
<div class="inv-form-grid">
|
||
<label class="inv-form-field">
|
||
<span>Re-Assign a New Field Officer</span>
|
||
<select [(ngModel)]="fiOfficerNameInput[key]">
|
||
<option value="">Select an officer</option>
|
||
@for (staffMember of fiStaffOptions; track staffMember.id) {
|
||
<option [value]="staffMember.displayName">{{ staffMember.displayName }}</option>
|
||
}
|
||
</select>
|
||
</label>
|
||
</div>
|
||
<div class="loan-action-row">
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="assignFieldOfficerNow(a, key)">
|
||
Re-Assign Officer
|
||
</button>
|
||
</div>
|
||
}
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
@if (a.secured) {
|
||
<div class="loi-emi-breakdown">
|
||
<div class="loi-emi-breakdown-title">Property Valuation</div>
|
||
<div class="inv-form-grid">
|
||
<label class="inv-form-field">
|
||
<span>Property Type</span>
|
||
<select [(ngModel)]="a.propertyType">
|
||
@for (t of propertyTypes; track t) {
|
||
<option [value]="t">{{ t }}</option>
|
||
}
|
||
</select>
|
||
</label>
|
||
<label class="inv-form-field">
|
||
<span>Built-Up Area (sq ft)</span>
|
||
<input type="number" [(ngModel)]="a.builtUpAreaSqFt" (ngModelChange)="updatePropertyValuation(a)" />
|
||
</label>
|
||
<label class="inv-form-field">
|
||
<span>Market Rate (₹ / sq ft)</span>
|
||
<input
|
||
type="number"
|
||
[(ngModel)]="a.marketRatePerSqFt"
|
||
(ngModelChange)="updatePropertyValuation(a)"
|
||
/>
|
||
</label>
|
||
<label class="inv-form-field">
|
||
<span>Guideline / Circle Rate (₹ / sq ft)</span>
|
||
<input type="number" [(ngModel)]="a.guidelineRatePerSqFt" />
|
||
</label>
|
||
<label class="inv-form-field">
|
||
<span>Valuer Name</span>
|
||
<input type="text" [(ngModel)]="a.valuerName" placeholder="Empanelled valuer" />
|
||
</label>
|
||
<label class="inv-form-field">
|
||
<span>Valuation Date</span>
|
||
<input type="text" [(ngModel)]="a.valuationDate" placeholder="DD-MM-YYYY" />
|
||
</label>
|
||
</div>
|
||
<dl class="loan-kv-grid">
|
||
<div class="loan-kv-field">
|
||
<dt>Market Value (Area × Market Rate)</dt>
|
||
<dd>₹{{ a.propertyValuation | number: '1.0-0' }}</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Guideline Value (Area × Circle Rate)</dt>
|
||
<dd>₹{{ guidelineValue(a) | number: '1.0-0' }}</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Realizable Value (90% of Market)</dt>
|
||
<dd>₹{{ realizableValue(a) | number: '1.0-0' }}</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Distress Sale Value (75% of Market)</dt>
|
||
<dd>₹{{ distressValue(a) | number: '1.0-0' }}</dd>
|
||
</div>
|
||
<div class="loan-kv-field">
|
||
<dt>Loan-to-Value (vs Realizable)</dt>
|
||
<dd>{{ ltvPercent(a) | number: '1.0-1' }}%</dd>
|
||
</div>
|
||
</dl>
|
||
@if (ltvHigh(a)) {
|
||
<p class="loi-kyc-error">
|
||
LTV exceeds 80% of the realizable value — this loan amount is aggressive against the property's
|
||
worth; consider a lower amount or additional security.
|
||
</p>
|
||
}
|
||
</div>
|
||
}
|
||
<div class="loan-kv-grid" style="padding: 0 0 14px">
|
||
<div class="loan-kv-field">
|
||
<dt>Agent Report</dt>
|
||
<dd>{{ a.agentReportUploaded ? a.agentReportFileName : 'Not uploaded yet' }}</dd>
|
||
</div>
|
||
@if (a.agentReportUploaded) {
|
||
<div class="loan-kv-field">
|
||
<dt>Report Remark</dt>
|
||
<dd>{{ a.agentReportRemark }}</dd>
|
||
</div>
|
||
}
|
||
</div>
|
||
@if (a.agentReportUploaded && hasStoredDoc('FI:agentReport')) {
|
||
<div class="loan-action-row">
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="viewKycSlot(a, null, 'FI:agentReport')">
|
||
View file
|
||
</button>
|
||
</div>
|
||
}
|
||
@if (!a.agentReportUploaded) {
|
||
<label class="inv-form-field">
|
||
<span>Agent Report Reference / Remark</span>
|
||
<input
|
||
type="text"
|
||
[(ngModel)]="agentReportRemarkInput"
|
||
placeholder="e.g. All addresses visited and cross-verified — no discrepancies noted"
|
||
/>
|
||
</label>
|
||
<div class="loan-action-row">
|
||
<label class="pg-btn pg-btn--ghost" style="cursor: pointer">
|
||
Upload Agent Report
|
||
<input
|
||
type="file"
|
||
hidden
|
||
accept=".pdf,.jpg,.jpeg,.png"
|
||
(change)="onAgentReportFileSelected($event, a)"
|
||
/>
|
||
</label>
|
||
</div>
|
||
}
|
||
<div class="loan-action-row">
|
||
<button
|
||
type="button"
|
||
class="pg-btn pg-btn--primary"
|
||
[disabled]="!fiComplete(a) || !a.agentReportUploaded"
|
||
(click)="completeFi(a)"
|
||
>
|
||
Record FI & Proceed to Personal Discussion
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- ===================== PERSONAL DISCUSSION ===================== -->
|
||
@if (a.currentStage === 'PERSONAL_DISCUSSION') {
|
||
<div class="pg-card">
|
||
<div class="pos-section-label">Personal Discussion with Manager</div>
|
||
<div class="loan-action-card">
|
||
<div class="loi-cic-subsection">
|
||
<div class="loi-cic-subsection-title">Briefing Report — For the Meeting</div>
|
||
<p class="pg-sub npa-report-note">
|
||
Everything captured so far — KYC, CIC score, Field Investigation, property valuation, agent report and
|
||
the full stage history — as one PDF for whoever takes this meeting.
|
||
</p>
|
||
<div class="loan-action-row">
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="printApplicationSummary(a)">
|
||
🖨️ Print Summary Report
|
||
</button>
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="downloadApplicationSummary(a)">
|
||
⬇️ Download Summary Report
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="loi-cic-subsection">
|
||
<div class="loi-cic-subsection-title">Meeting Scheduler</div>
|
||
<div class="inv-form-grid">
|
||
<label class="inv-form-field">
|
||
<span>Meeting Date</span>
|
||
<input type="text" [(ngModel)]="a.meetingDate" placeholder="DD-MM-YYYY" />
|
||
</label>
|
||
<label class="inv-form-field">
|
||
<span>Meeting Time</span>
|
||
<input type="text" [(ngModel)]="a.meetingTime" placeholder="e.g. 14:30" />
|
||
</label>
|
||
<label class="inv-form-field">
|
||
<span>Mode of Meeting</span>
|
||
<select [(ngModel)]="a.meetingMode">
|
||
<option value="">Select</option>
|
||
<option value="OFFLINE">Offline (In-Branch)</option>
|
||
<option value="ONLINE">Online (Video Call)</option>
|
||
<option value="CALL">Phone Call</option>
|
||
</select>
|
||
</label>
|
||
<label class="inv-form-field">
|
||
<span>Bank Officer Taking the Meeting</span>
|
||
<select [(ngModel)]="a.meetingWith">
|
||
<option value="">Select</option>
|
||
@for (m of meetingManagers; track m) {
|
||
<option [value]="m">{{ m }}</option>
|
||
}
|
||
</select>
|
||
</label>
|
||
</div>
|
||
<div class="loan-action-row">
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="scheduleMeetingNow(a)">
|
||
{{ a.meetingScheduled ? 'Update Schedule' : 'Schedule Meeting' }}
|
||
</button>
|
||
</div>
|
||
@if (a.meetingScheduled) {
|
||
<p class="pg-sub npa-report-note">
|
||
Scheduled with <strong>{{ a.meetingWith }}</strong> on {{ a.meetingDate }} at {{ a.meetingTime }} ({{
|
||
a.meetingMode === 'ONLINE' ? 'Online' : a.meetingMode === 'CALL' ? 'Phone Call' : 'Offline'
|
||
}})
|
||
</p>
|
||
}
|
||
</div>
|
||
|
||
@if (a.meetingScheduled) {
|
||
<label class="inv-form-field">
|
||
<span>Discrepancy (if any)</span>
|
||
<select [(ngModel)]="discrepancySelection">
|
||
<option value="NONE">No Discrepancy</option>
|
||
<option value="DOCUMENT_DISCREPANCY">Document Discrepancy</option>
|
||
<option value="NAME_MISMATCH">Name Mismatch</option>
|
||
<option value="OFFICE_VERIFICATION_FAILED">Office Verification Failed</option>
|
||
<option value="PROPERTY_DISCREPANCY">Property Discrepancy</option>
|
||
<option value="FI_REPORT_DISCREPANCY">FI Report Discrepancy</option>
|
||
<option value="FAKE_DECLARATION">Fake Declaration Suspected</option>
|
||
</select>
|
||
</label>
|
||
<label class="inv-form-field">
|
||
<span>Discussion Report</span>
|
||
<input
|
||
type="text"
|
||
[(ngModel)]="actionRemarks"
|
||
placeholder="What was discussed and decided on the call/meeting"
|
||
/>
|
||
</label>
|
||
<div class="loan-action-row">
|
||
@if (discrepancySelection === 'NONE') {
|
||
<button type="button" class="pg-btn pg-btn--success" (click)="clearPersonalDiscussion(a)">
|
||
Clear, Take Declaration & Proceed to Loan Committee
|
||
</button>
|
||
} @else {
|
||
<button type="button" class="pg-btn pg-btn--warn" (click)="flagPersonalDiscussion(a)">
|
||
Flag Discrepancy — Hold Application
|
||
</button>
|
||
}
|
||
</div>
|
||
} @else {
|
||
<p class="loi-kyc-error">Schedule the meeting above before recording the discussion report.</p>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- ===================== LOAN COMMITTEE APPROVAL ===================== -->
|
||
@if (a.currentStage === 'CREDIT_COMMITTEE') {
|
||
<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">
|
||
<input type="checkbox" [ngModel]="a.cicReportUploaded" disabled /> CIBIL / Credit Bureau Report
|
||
</label>
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [(ngModel)]="a.backgroundCheckDone" /> Background Check
|
||
</label>
|
||
@if (a.secured) {
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [(ngModel)]="a.cersaiSearchDone" /> CSE — CERSAI Search Enquiry
|
||
</label>
|
||
}
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [ngModel]="a.aadhaarVerified && a.panVerified" disabled /> OVD
|
||
</label>
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [ngModel]="a.incomeProofSubmitted" disabled /> Income Proof
|
||
</label>
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [ngModel]="fiComplete(a)" disabled /> FI Report
|
||
</label>
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [(ngModel)]="a.managerMeetingDone" /> Manager Meeting
|
||
</label>
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [ngModel]="a.isShareMember" disabled /> Share Member Approval
|
||
</label>
|
||
@if (a.secured) {
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [ngModel]="a.propertyValuation > 0" disabled /> Property Valuation
|
||
</label>
|
||
}
|
||
</div>
|
||
<label class="inv-form-field">
|
||
<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]="!committeeCanApprove(a)"
|
||
(click)="approveCommitteeStage(a)"
|
||
>
|
||
Approve
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="pg-btn pg-btn--danger"
|
||
(click)="rejectStage(a, 'creditCommitteeApproval', 'Sanctioning Authority')"
|
||
>
|
||
Reject
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- ===================== RISK APPROVAL ===================== -->
|
||
@if (a.currentStage === 'RISK_APPROVAL') {
|
||
<div class="pg-card">
|
||
<div class="pos-section-label">Risk Approval — Verification Checklist</div>
|
||
<div class="loan-action-card">
|
||
<div class="loan-checklist">
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [ngModel]="a.addressVerification.status === 'VERIFIED'" disabled /> ID / Address
|
||
Verified
|
||
</label>
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [ngModel]="a.incomeProofSubmitted" disabled /> Income Proof Verified
|
||
</label>
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [(ngModel)]="a.previousLoanDeclarationDone" /> Previous Loan Declaration
|
||
</label>
|
||
@if (a.secured) {
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [(ngModel)]="a.propertySearchReportDone" /> Property Search Report
|
||
</label>
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [(ngModel)]="a.valuationReportConfirmed" /> Valuation Report
|
||
</label>
|
||
}
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [ngModel]="fiComplete(a)" disabled /> FI Report
|
||
</label>
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [ngModel]="a.backgroundCheckDone" disabled /> Personal Background Check
|
||
</label>
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [(ngModel)]="a.guarantorRequired" /> Guarantor Required
|
||
</label>
|
||
</div>
|
||
@if (a.guarantorRequired) {
|
||
<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>
|
||
<input
|
||
type="text"
|
||
[(ngModel)]="actionRemarks"
|
||
placeholder="ID/address, income adequacy, previous-loan declaration"
|
||
/>
|
||
</label>
|
||
<div class="loan-action-row">
|
||
<button
|
||
type="button"
|
||
class="pg-btn pg-btn--success"
|
||
[disabled]="!riskChecklistReady(a)"
|
||
(click)="approveStage(a, 'riskApproval', 'Risk Approval')"
|
||
>
|
||
Approve
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="pg-btn pg-btn--danger"
|
||
(click)="rejectStage(a, 'riskApproval', 'Risk Approval')"
|
||
>
|
||
Reject
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- ===================== LEGAL APPROVAL ===================== -->
|
||
@if (a.currentStage === 'LEGAL_APPROVAL') {
|
||
<div class="pg-card">
|
||
<div class="pos-section-label">Legal Approval — Fees & A/c Opened By</div>
|
||
<div class="loan-action-card">
|
||
<div class="loan-checklist">
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [(ngModel)]="a.feesAndAccountOpened" /> Fees & A/c Opening Done
|
||
</label>
|
||
@if (a.secured) {
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [(ngModel)]="a.mortgageCreated" /> Mortgage Created
|
||
</label>
|
||
}
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [(ngModel)]="a.legalStampDone" /> Legal Stamp Done
|
||
</label>
|
||
<label class="loan-checklist-item">
|
||
<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>
|
||
}
|
||
<!-- Already in the document store from an earlier session. -->
|
||
@for (stored of storedDocsFor('LEGAL_DOC:' + doc.key); track stored.id) {
|
||
<div class="loi-legal-doc-thumb-wrap">
|
||
<button
|
||
type="button"
|
||
class="loi-kyc-thumb"
|
||
title="{{ stored.fileName }}"
|
||
(click)="viewStoredDoc(a, stored)"
|
||
>
|
||
<span class="loi-kyc-thumb-label">View file</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="loi-legal-doc-thumb-remove"
|
||
title="Delete this stored file"
|
||
(click)="removeStoredDoc(a, stored)"
|
||
>
|
||
✕
|
||
</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" />
|
||
</label>
|
||
<label class="inv-form-field">
|
||
<span>Legal Remarks</span>
|
||
<input type="text" [(ngModel)]="actionRemarks" placeholder="Remarks" />
|
||
</label>
|
||
<div class="loan-action-row">
|
||
<button
|
||
type="button"
|
||
class="pg-btn pg-btn--success"
|
||
[disabled]="!legalChecklistDone(a)"
|
||
(click)="approveStage(a, 'legalApproval', 'Legal Approval')"
|
||
>
|
||
Approve & Allot Signing Slot
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="pg-btn pg-btn--danger"
|
||
(click)="rejectStage(a, 'legalApproval', 'Legal Approval')"
|
||
>
|
||
Reject
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- ===================== CUSTOMER SIGNING SLOT ===================== -->
|
||
@if (a.currentStage === 'CUSTOMER_SIGNING') {
|
||
<div class="pg-card">
|
||
<div class="pos-section-label">Customer Signing Slot — {{ a.agreementSigningSlot }}</div>
|
||
<div class="loan-action-card">
|
||
<p>Loan Agreement and Sanction Letter ready for the customer's acknowledgement and signature.</p>
|
||
<div class="loan-action-row">
|
||
<button type="button" class="pg-btn pg-btn--primary" (click)="markSigned(a)">
|
||
Mark Signed by Customer
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- ===================== CHEQUE RECEIVED / ACH SIGN ===================== -->
|
||
@if (a.currentStage === 'CHEQUE_ACH') {
|
||
<div class="pg-card">
|
||
<div class="pos-section-label">Cheque Received / ACH Sign</div>
|
||
<div class="loan-action-card">
|
||
<div class="loan-checklist">
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [(ngModel)]="a.chequeReceived" /> Cheque Received
|
||
</label>
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [(ngModel)]="a.achSigned" /> ACH Mandate Signed
|
||
</label>
|
||
</div>
|
||
<div class="loan-action-row">
|
||
<button
|
||
type="button"
|
||
class="pg-btn pg-btn--primary"
|
||
[disabled]="!a.chequeReceived || !a.achSigned"
|
||
(click)="confirmChequeAndProceed(a)"
|
||
>
|
||
Confirm & Proceed to Post-Disbursement Check
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- ===================== POST-DISBURSEMENT CHECK ===================== -->
|
||
@if (a.currentStage === 'POST_CHECK') {
|
||
<div class="pg-card">
|
||
<div class="pos-section-label">Post-Disbursement Check — Final Compliance Gate</div>
|
||
<div class="loan-action-card">
|
||
<p class="pg-sub npa-report-note">Document Signing by:</p>
|
||
<div class="loan-checklist">
|
||
<label class="loan-checklist-item"> <input type="checkbox" [(ngModel)]="a.signedByAgent" /> Agent </label>
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [(ngModel)]="a.signedByManager" /> Manager
|
||
</label>
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [(ngModel)]="a.signedByCommittee" /> Committee
|
||
</label>
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [(ngModel)]="a.signedByRiskApproval" /> Risk Approval
|
||
</label>
|
||
<label class="loan-checklist-item">
|
||
<input type="checkbox" [(ngModel)]="a.signedByLegalApproval" /> Legal Approval
|
||
</label>
|
||
</div>
|
||
<label class="inv-form-field">
|
||
<span>Remarks</span>
|
||
<input type="text" [(ngModel)]="actionRemarks" placeholder="Remarks" />
|
||
</label>
|
||
<div class="loan-action-row">
|
||
<button
|
||
type="button"
|
||
class="pg-btn pg-btn--success"
|
||
[disabled]="!postCheckSignaturesDone(a)"
|
||
(click)="approveStage(a, 'postCheckApproval', 'Post-Disbursement Check')"
|
||
>
|
||
Clear for Disbursement
|
||
</button>
|
||
<button type="button" class="pg-btn pg-btn--warn" (click)="exceptionApprove(a)">
|
||
Approve on Exception
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="pg-btn pg-btn--danger"
|
||
(click)="rejectStage(a, 'postCheckApproval', 'Post-Disbursement Check')"
|
||
>
|
||
Reject
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- ===================== DISBURSEMENT ===================== -->
|
||
@if (a.currentStage === 'DISBURSED') {
|
||
<div class="pg-card">
|
||
<div class="pos-section-label">Ready for Disbursement</div>
|
||
<div class="loan-action-card">
|
||
<label class="inv-form-field">
|
||
<span>Disbursement Mode</span>
|
||
<select [(ngModel)]="disbursementMode">
|
||
<option value="Account Credit">Account Credit</option>
|
||
<option value="NEFT">NEFT</option>
|
||
<option value="RTGS">RTGS</option>
|
||
</select>
|
||
</label>
|
||
<p class="pg-sub npa-report-note">
|
||
Disbursing also confirms EMI Activation for this account in the same step.
|
||
@if (a.customerType === 'EXISTING' && a.clientId) {
|
||
A real Fineract loan account will be created, approved and disbursed in Core Banking at the same time.
|
||
} @else {
|
||
This is a mock disbursement only — no Core Banking account exists until this customer is fully onboarded
|
||
as a client.
|
||
}
|
||
</p>
|
||
@if (a.fineractLoanError) {
|
||
<p class="loi-kyc-error">{{ a.fineractLoanError }}</p>
|
||
}
|
||
<div class="loan-action-row">
|
||
<button
|
||
type="button"
|
||
class="pg-btn pg-btn--success"
|
||
[disabled]="creatingRealLoan"
|
||
(click)="disburseNow(a)"
|
||
>
|
||
@if (creatingRealLoan) {
|
||
Creating Real Loan Account in Core Banking…
|
||
} @else if (a.fineractLoanError) {
|
||
Retry Disburse & Confirm EMI Activation
|
||
} @else {
|
||
Disburse Now & Confirm EMI Activation
|
||
}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
}
|
||
|
||
<div class="pg-card">
|
||
<div class="pos-section-label">Approval Summary</div>
|
||
<div class="loan-approval-summary">
|
||
<div
|
||
class="loan-approval-tile"
|
||
[class.loan-approval-tile--approved]="
|
||
a.creditCommitteeApproval.decision === 'APPROVED' ||
|
||
a.creditCommitteeApproval.decision === 'EXCEPTION_APPROVED'
|
||
"
|
||
[class.loan-approval-tile--rejected]="a.creditCommitteeApproval.decision === 'REJECTED'"
|
||
[class.loan-approval-tile--pending]="a.creditCommitteeApproval.decision === 'PENDING'"
|
||
>
|
||
<span class="loan-approval-tile__role">Loan Committee</span>
|
||
<span class="loan-approval-tile__decision">{{ a.creditCommitteeApproval.decision }}</span>
|
||
<span>{{ a.creditCommitteeApproval.approverName }} · {{ a.creditCommitteeApproval.date }}</span>
|
||
<span>{{ a.creditCommitteeApproval.remarks }}</span>
|
||
</div>
|
||
<div
|
||
class="loan-approval-tile"
|
||
[class.loan-approval-tile--approved]="
|
||
a.riskApproval.decision === 'APPROVED' || a.riskApproval.decision === 'EXCEPTION_APPROVED'
|
||
"
|
||
[class.loan-approval-tile--rejected]="a.riskApproval.decision === 'REJECTED'"
|
||
[class.loan-approval-tile--pending]="a.riskApproval.decision === 'PENDING'"
|
||
>
|
||
<span class="loan-approval-tile__role">Risk Approval</span>
|
||
<span class="loan-approval-tile__decision">{{ a.riskApproval.decision }}</span>
|
||
<span>{{ a.riskApproval.approverName }} · {{ a.riskApproval.date }}</span>
|
||
<span>{{ a.riskApproval.remarks }}</span>
|
||
</div>
|
||
<div
|
||
class="loan-approval-tile"
|
||
[class.loan-approval-tile--approved]="
|
||
a.legalApproval.decision === 'APPROVED' || a.legalApproval.decision === 'EXCEPTION_APPROVED'
|
||
"
|
||
[class.loan-approval-tile--rejected]="a.legalApproval.decision === 'REJECTED'"
|
||
[class.loan-approval-tile--pending]="a.legalApproval.decision === 'PENDING'"
|
||
>
|
||
<span class="loan-approval-tile__role">Legal Approval</span>
|
||
<span class="loan-approval-tile__decision">{{ a.legalApproval.decision }}</span>
|
||
<span>{{ a.legalApproval.approverName }} · {{ a.legalApproval.date }}</span>
|
||
<span>{{ a.legalApproval.remarks }}</span>
|
||
</div>
|
||
<div
|
||
class="loan-approval-tile"
|
||
[class.loan-approval-tile--approved]="
|
||
a.postCheckApproval.decision === 'APPROVED' || a.postCheckApproval.decision === 'EXCEPTION_APPROVED'
|
||
"
|
||
[class.loan-approval-tile--rejected]="a.postCheckApproval.decision === 'REJECTED'"
|
||
[class.loan-approval-tile--pending]="a.postCheckApproval.decision === 'PENDING'"
|
||
>
|
||
<span class="loan-approval-tile__role">Post-Disbursement Check</span>
|
||
<span class="loan-approval-tile__decision">{{ a.postCheckApproval.decision }}</span>
|
||
<span>{{ a.postCheckApproval.approverName }} · {{ a.postCheckApproval.date }}</span>
|
||
<span>{{ a.postCheckApproval.remarks }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pg-card">
|
||
<div class="pos-section-label">Application Journey — Date-wise Audit Trail</div>
|
||
<div class="npa-timeline" style="padding: 4px 18px 18px">
|
||
@for (h of a.stageHistory; track h.date + h.stage + h.outcome) {
|
||
<div [class]="historyToneClass(h.outcome)">
|
||
<span class="npa-timeline-item__icon">{{ stepMarker(a, h.stage) }}</span>
|
||
<div class="npa-timeline-item__body">
|
||
<div class="npa-timeline-item__row">
|
||
<span class="npa-timeline-item__type">{{ stageLabels[h.stage] }} — {{ h.outcome }}</span>
|
||
<span class="npa-timeline-item__date">{{ h.date }}</span>
|
||
</div>
|
||
<p class="npa-timeline-item__remarks">{{ h.remarks }}</p>
|
||
<span class="npa-timeline-item__channel">{{ h.actionBy }}</span>
|
||
</div>
|
||
</div>
|
||
} @empty {
|
||
<p class="pg-empty">No activity recorded yet.</p>
|
||
}
|
||
</div>
|
||
</div>
|
||
} @else {
|
||
<div class="pg-card">
|
||
<div class="pos-section-label">Application Not Found</div>
|
||
<div class="loan-action-card">
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="goBack()">‹ Back to Pipeline</button>
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<div class="inv-toast" [class.show]="toastVisible">{{ toastMsg }}</div>
|