feat(loan-origination): persist the sanctioning pipeline to the backend
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

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>
This commit is contained in:
2026-08-17 17:05:05 +05:30
co-authored by Claude Sonnet 5
parent 2d93f80d50
commit c18219fce5
10 changed files with 1333 additions and 301 deletions
@@ -6,7 +6,14 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="pg">
<!-- (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>
@@ -21,6 +28,18 @@
</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>
@@ -86,8 +105,8 @@
<dt>Co-Borrower</dt>
<dd>
{{ a.coBorrowerName }}
@if (coBorrowerCapture.wanted && coBorrowerCapture.relationship) {
({{ coBorrowerCapture.relationship }})
@if (a.coBorrowerWanted && a.coBorrowerRelationship) {
({{ a.coBorrowerRelationship }})
}
</dd>
</div>
@@ -168,6 +187,16 @@
<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>
@@ -336,12 +365,14 @@
(change)="onNewSalariedProofFile(a, $event)"
/>
</label>
@if (newKycCapture.incomeProof.salaried.file) {
@if (newKycCapture.incomeProof.salaried.file || hasStoredDoc('KYC:incomeProof:salaried')) {
<button
type="button"
class="loi-kyc-thumb"
title="View document"
(click)="viewNewSingleDoc(newKycCapture.incomeProof.salaried.file)"
(click)="
viewKycSlot(a, newKycCapture.incomeProof.salaried.file, 'KYC:incomeProof:salaried')
"
>
@if (newKycCapture.incomeProof.salaried.previewUrl) {
<img [src]="newKycCapture.incomeProof.salaried.previewUrl" alt="Salary proof" />
@@ -367,12 +398,12 @@
(change)="onNewItrFile(a, $index, $event)"
/>
</label>
@if (doc.file) {
@if (doc.file || hasStoredDoc('KYC:incomeProof:itr:' + $index)) {
<button
type="button"
class="loi-kyc-thumb"
title="View document"
(click)="viewNewSingleDoc(doc.file)"
(click)="viewKycSlot(a, doc.file, 'KYC:incomeProof:itr:' + $index)"
>
@if (doc.previewUrl) {
<img [src]="doc.previewUrl" alt="ITR Year {{ $index + 1 }}" />
@@ -436,12 +467,12 @@
(change)="onNewBankStatementFile(a, $event)"
/>
</label>
@if (newKycCapture.bankStatement.file) {
@if (newKycCapture.bankStatement.file || hasStoredDoc('KYC:bankStatement')) {
<button
type="button"
class="loi-kyc-thumb"
title="View document"
(click)="viewNewSingleDoc(newKycCapture.bankStatement.file)"
(click)="viewKycSlot(a, newKycCapture.bankStatement.file, 'KYC:bankStatement')"
>
@if (newKycCapture.bankStatement.previewUrl) {
<img [src]="newKycCapture.bankStatement.previewUrl" alt="Bank statement" />
@@ -449,12 +480,12 @@
<span class="loi-kyc-thumb-label">View file</span>
}
</button>
@if (isCsvFile(newKycCapture.bankStatement.file)) {
@if (canReopenBankStatementAnalysis()) {
<button
type="button"
class="loi-bsa-reopen"
title="Re-open the transaction analysis"
(click)="reopenBankStatementAnalysis(newKycCapture.bankStatement.file)"
(click)="reopenBankStatementAnalysisFor(a)"
>
📊 Re-check Analysis
</button>
@@ -498,12 +529,12 @@
(change)="onNewElectricityBillFile(a, $event)"
/>
</label>
@if (newKycCapture.electricityBill.file) {
@if (newKycCapture.electricityBill.file || hasStoredDoc('KYC:electricityBill')) {
<button
type="button"
class="loi-kyc-thumb"
title="View document"
(click)="viewNewSingleDoc(newKycCapture.electricityBill.file)"
(click)="viewKycSlot(a, newKycCapture.electricityBill.file, 'KYC:electricityBill')"
>
@if (newKycCapture.electricityBill.previewUrl) {
<img [src]="newKycCapture.electricityBill.previewUrl" alt="Electricity bill" />
@@ -949,12 +980,12 @@
/>
</label>
@if (productDocCapture[doc.key]; as capture) {
@if (capture.file) {
@if (capture.file || hasStoredDoc('PRODUCT_DOC:' + doc.key)) {
<button
type="button"
class="loi-kyc-thumb"
title="View document"
(click)="viewNewSingleDoc(capture.file)"
(click)="viewKycSlot(a, capture.file, 'PRODUCT_DOC:' + doc.key)"
>
@if (capture.previewUrl) {
<img [src]="capture.previewUrl" alt="{{ doc.label }}" />
@@ -1109,17 +1140,17 @@
<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)]="coBorrowerCapture.wanted" />
<input type="checkbox" [(ngModel)]="a.coBorrowerWanted" />
Add a Co-Borrower to increase eligibility
</label>
@if (coBorrowerCapture.wanted) {
@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)]="coBorrowerCapture.relationship">
<select [(ngModel)]="a.coBorrowerRelationship">
<option value="">Select</option>
<option value="Father">Father</option>
<option value="Mother">Mother</option>
@@ -1132,7 +1163,7 @@
</label>
<label class="inv-form-field">
<span>Co-Borrower Monthly Income (₹)</span>
<input type="number" [(ngModel)]="coBorrowerCapture.monthlyIncome" placeholder="e.g. 30000" />
<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">
@@ -1150,12 +1181,12 @@
(change)="onCoBorrowerSalarySlipFile($index, $event)"
/>
</label>
@if (doc.file) {
@if (doc.file || hasStoredDoc('COBORROWER:salarySlip:' + $index)) {
<button
type="button"
class="loi-kyc-thumb"
title="View document"
(click)="viewNewSingleDoc(doc.file)"
(click)="viewKycSlot(a, doc.file, 'COBORROWER:salarySlip:' + $index)"
>
@if (doc.previewUrl) {
<img [src]="doc.previewUrl" alt="Salary Slip Month {{ $index + 1 }}" />
@@ -1180,11 +1211,11 @@
</div>
<div class="loan-kv-field">
<dt>Co-Borrower's Monthly Income</dt>
<dd>₹{{ coBorrowerCapture.monthlyIncome | number: '1.0-0' }}</dd>
<dd>₹{{ a.coBorrowerMonthlyIncome | number: '1.0-0' }}</dd>
</div>
<div class="loan-kv-field">
<dt>Co-Borrower's EMI Capacity (40%)</dt>
<dd>₹{{ coBorrowerEmiCapacity() | number: '1.0-0' }}</dd>
<dd>₹{{ coBorrowerEmiCapacity(a) | number: '1.0-0' }}</dd>
</div>
<div class="loan-kv-field">
<dt>Combined Household EMI Capacity</dt>
@@ -1197,10 +1228,10 @@
</dl>
<p class="pg-sub npa-report-note loi-emi-formula">
Combined Capacity = (40% × ₹{{ a.monthlyIncome | number: '1.0-0' }} applicant) + (40% × ₹{{
coBorrowerCapture.monthlyIncome | number: '1.0-0'
a.coBorrowerMonthlyIncome | number: '1.0-0'
}}
co-borrower) = ₹{{ applicantEmiCapacity(a) | number: '1.0-0' }} + ₹{{
coBorrowerEmiCapacity() | number: '1.0-0'
coBorrowerEmiCapacity(a) | number: '1.0-0'
}}
= ₹{{ combinedEmiCapacity(a) | number: '1.0-0' }}. Estimated EMI ₹{{
emiAmountFor(a) | number: '1.0-0'
@@ -1220,7 +1251,7 @@
@if (!affordabilityOk(a)) {
<p class="loi-kyc-error">
@if (!coBorrowerDetailsComplete(a)) {
@if (!coBorrowerCapture.wanted) {
@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.
@@ -1238,7 +1269,7 @@
type="button"
class="pg-btn pg-btn--primary"
[disabled]="!allKycDone(a)"
(click)="proceed(a, 'KYC/OVD documents verified and collected.')"
(click)="completeKycStage(a)"
>
Proceed to CIC / Credit Bureau Check
</button>
@@ -1252,138 +1283,139 @@
<div class="pg-card">
<div class="pos-section-label">CIC / Credit Bureau Check — {{ a.cicBureau }}</div>
<div class="loan-action-card">
@if (a.cibilScore === 0) {
<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>
<!-- 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 (cicConsentCapture.file) {
<div class="loi-kyc-thumbs">
}
@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-thumb"
title="View document"
(click)="viewNewSingleDoc(cicConsentCapture.file)"
class="loi-kyc-card loi-charge-account"
[class.loi-charge-account--selected]="a.cicChargeAccountLabel.startsWith(acc.accountNo)"
(click)="selectCicChargeAccount(a, acc)"
>
@if (cicConsentCapture.previewUrl) {
<img [src]="cicConsentCapture.previewUrl" alt="Signed consent declaration" />
} @else {
<span class="loi-kyc-thumb-label">View file</span>
}
<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>
}
@if (a.cicConsentSigned) {
<span class="loan-badge loan-badge--verified">Signed Declaration Uploaded</span>
}
</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 {
<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>
@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 {
<!-- 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>
</div>
} @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 === '—') {
<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)="generateCicPaymentLink(a)">
Generate Payment Link
<button type="button" class="pg-btn pg-btn--ghost" (click)="confirmCicPaymentReceived(a)">
Confirm Payment Received
</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.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">
@@ -1453,6 +1485,11 @@
>
📊 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>
@@ -1473,21 +1510,7 @@
Push Field Inquiry to Agency
</button>
} @else if (a.cicReportUploaded && a.fiRequestPushed) {
<button
type="button"
class="pg-btn pg-btn--primary"
(click)="
proceed(
a,
a.cicBureau +
' report reviewed — score ' +
a.cibilScore +
' (' +
cibilBandOf(a.cibilScore) +
').'
)
"
>
<button type="button" class="pg-btn pg-btn--primary" (click)="completeCicStage(a)">
Proceed to Field Investigation
</button>
}
@@ -1536,7 +1559,12 @@
<div class="inv-form-grid">
<label class="inv-form-field">
<span>Assign Field Officer</span>
<input type="text" [(ngModel)]="fiOfficerNameInput[key]" placeholder="Officer name" />
<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">
@@ -1642,7 +1670,12 @@
<div class="inv-form-grid">
<label class="inv-form-field">
<span>Re-Assign a New Field Officer</span>
<input type="text" [(ngModel)]="fiOfficerNameInput[key]" placeholder="Officer name" />
<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">
@@ -1735,6 +1768,13 @@
</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>
@@ -2175,6 +2215,27 @@
</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">
@@ -2341,10 +2402,30 @@
</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" (click)="disburseNow(a)">
Disburse Now &amp; Confirm EMI Activation
<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 &amp; Confirm EMI Activation
} @else {
Disburse Now &amp; Confirm EMI Activation
}
</button>
</div>
</div>