feat: letter-mgmt module + digital UPI/IMPS/ATM-POS reports + header redesign

letter-mgmt (new module):
- Notice page: overdue loan list with filters, KPI strip, bulk actions,
  call disposition logging (Promise-to-Pay, Not Reachable, etc.), Speed
  Post dispatch dialog with consignment + post-office tracking,
  PDF preview + download, call-contact dialog with primary / alt /
  family / nominee numbers and per-contact disposition history.
- Certificates: Loan Interest, Share, Balance, Form 60 (no-PAN flow),
  Form 15G/H (auto age-based), Hypothecation, NOC - each with shared
  preview dialog (six tone variants) and jsPDF generator using a
  common letterhead / summary-grid / signature-seal helper.

digital:
- IMPS module: report (P2A/P2P/Aadhaar tabs) + entry form with
  MMID/mobile lookup, NPCI switch resolve-to-A/C+IFSC BANL flow,
  sender autocomplete, hide sender field for P2P/Aadhaar, charges +
  18% GST + waive checkbox, per-sender today-only history dialog.
- UPI report with app-mix tab and full transaction filtering.
- ATM/POS/ECOM hub with three ticket cards routing to today/status/cycle.
- NEFT/RTGS entry: sender-account autocomplete, charges + GST + waive,
  field reorder (sender before amount), NEFT cap / RTGS floor
  validation, History button opening per-sender today's transactions.
- NEFT/RTGS report: horizontal-bar trend leaderboard replacing tall bars.

shell + theme:
- Header redesigned with IBM Plex Sans 12/500 typography, 11-column grid
  per row, top-accent hover indicator, bordered cells via grid-gap.
- Live KPI ticker below menu (CASH, TXN TODAY, CLEARING IN/OUT, NPA,
  CASA, CD RATIO, RBI CIRC, REPO) with marquee scroll, pause-on-hover,
  brand-blue gradient, amber bottom strip, pulsing red LIVE chip.
- Sidebar 'KYC Management' -> 'KYC & ReKYC Management'.
- Letter Mgmt / certificate menu items wired with green icon hints for
  items that have a working page.

access-control:
- MFA card layout compacted; method header inlines back button + title.

icons:
- Register faTruck/TruckFast, faEllipsisV/H, faCommentDots.
- Add IBM Plex Sans Google Fonts import to index.html.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-05-19 10:09:06 +05:30
co-authored by Claude Opus 4.7
parent 4322f37c6d
commit 325228c514
138 changed files with 27473 additions and 3997 deletions
@@ -0,0 +1,703 @@
<!--
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/.
-->
<div class="cpd-root">
<!-- TOOLBAR -->
<div class="cpd-toolbar no-print">
<div class="cpd-toolbar-l">
<fa-icon [icon]="isInterest ? 'file-invoice' : 'certificate'" class="cpd-toolbar-ic"></fa-icon>
<div>
<div class="cpd-toolbar-t">{{ title }}</div>
<div class="cpd-toolbar-s">{{ refLine }}</div>
</div>
</div>
<div class="cpd-toolbar-r">
<button class="cpd-tb-btn" (click)="printCertificate()" matTooltip="Print or Save as PDF">
<fa-icon icon="print"></fa-icon> Print / PDF
</button>
<button class="cpd-tb-btn" (click)="downloadPdf()" matTooltip="Download as PDF">
<fa-icon icon="download"></fa-icon> Download PDF
</button>
<button class="cpd-tb-btn cpd-tb-x" (click)="close()" matTooltip="Close">
<fa-icon icon="times"></fa-icon>
</button>
</div>
</div>
<!-- CERTIFICATE PAGE -->
<div class="cpd-page" [ngClass]="pageToneClass">
<!-- Bank letterhead -->
<div class="cpd-letterhead">
<div class="cpd-bank-name">{{ bankName }}</div>
<div class="cpd-bank-sub">{{ bankAddress }}</div>
<div class="cpd-bank-sub">{{ bankRegNo }} · {{ bankContact }}</div>
</div>
<!-- Certificate title strip -->
<div class="cpd-title-strip">
<span class="cpd-title-l">{{ title }}</span>
</div>
<!-- Meta strip -->
<div class="cpd-meta-row">
<div>
<div class="cpd-meta-l">Certificate Reference</div>
<div class="cpd-meta-v mono">
@if (isInterest && interest) {
INT-{{ interest.record.accountNo }}-{{ interest.fy.fy | slice: 3 }}
} @else if (isShare && share) {
{{ share.certificateNo }}
}
</div>
</div>
<div>
<div class="cpd-meta-l">Date of Issue</div>
<div class="cpd-meta-v">{{ issuedOn | date: 'd MMMM y' }}</div>
</div>
<div>
<div class="cpd-meta-l">
@if (isInterest) {
Financial Year
} @else if (isShare) {
Folio No.
} @else if (isBalance) {
As-on Date
} @else if (isForm60) {
Trigger
} @else if (isForm15gh) {
Form
} @else if (isHypothecation) {
Status
} @else if (isNoc) {
Status
}
</div>
<div class="cpd-meta-v">
@if (isInterest && interest) {
<span class="cpd-stage-pill">{{ interest.fy.fy }}</span>
} @else if (isShare && share) {
<span class="cpd-stage-pill cpd-stage-pill--amber">{{ share.folioNo }}</span>
} @else if (isBalance && balance) {
<span class="cpd-stage-pill">{{ balance.asOnDate | date: 'd MMM y' }}</span>
} @else if (isForm60 && form60) {
<span class="cpd-stage-pill cpd-stage-pill--amber">No PAN — Form 60</span>
} @else if (isForm15gh && form15gh) {
<span class="cpd-stage-pill cpd-stage-pill--teal">Form {{ form15gh.kind }}</span>
} @else if (isHypothecation && hypothecation) {
<span class="cpd-stage-pill cpd-stage-pill--red">{{ hypothecation.status }}</span>
} @else if (isNoc && noc) {
<span class="cpd-stage-pill cpd-stage-pill--green">
{{ noc.status === 'READY_TO_ISSUE' ? 'Ready to Issue' : noc.status }}
</span>
}
</div>
</div>
</div>
<!-- ── LOAN INTEREST CERTIFICATE ── -->
@if (isInterest && interest) {
<div class="cpd-block">
<div class="cpd-block-l">To,</div>
<div class="cpd-block-v">
<div>
<strong>{{ interest.record.customerName }}</strong>
</div>
<div>
Customer ID: <span class="mono">{{ interest.record.customerId }}</span> · PAN:
<span class="mono">{{ interest.record.customerPan }}</span>
</div>
<div>{{ interest.record.customerAddress }}</div>
</div>
</div>
<p class="cpd-para">
This is to certify that <strong>{{ interest.record.customerName }}</strong> (Customer ID
<strong>{{ interest.record.customerId }}</strong
>) is availing the following loan facility from our bank, and the interest paid by the above customer during the
financial year <strong>{{ interest.fy.fy }}</strong> is as detailed below.
</p>
<table class="cpd-summary">
<tbody>
<tr>
<td>Loan Account No.</td>
<td class="mono">
<strong>{{ interest.record.accountNo }}</strong>
</td>
<td>Branch</td>
<td>{{ interest.record.branch }}</td>
</tr>
<tr>
<td>Loan Type</td>
<td>{{ interest.record.loanType }}</td>
<td>Disbursed On</td>
<td>{{ interest.record.disbursementDate | date: 'd MMM y' }}</td>
</tr>
<tr>
<td>Sanctioned Amount</td>
<td>₹{{ interest.record.sanctionedAmount | number: '1.2-2' }}</td>
<td>Interest Rate</td>
<td>{{ interest.record.interestRate }}% p.a.</td>
</tr>
<tr>
<td>Opening Balance ({{ interest.fy.fy | slice: 0 : 8 }})</td>
<td>₹{{ interest.fy.openingBalance | number: '1.2-2' }}</td>
<td>Closing Balance</td>
<td>₹{{ interest.fy.closingBalance | number: '1.2-2' }}</td>
</tr>
<tr>
<td>Principal Repaid</td>
<td>₹{{ interest.fy.principalPaid | number: '1.2-2' }}</td>
<td>Interest Paid</td>
<td>
<strong class="cpd-amount">₹{{ interest.fy.interestPaid | number: '1.2-2' }}</strong>
</td>
</tr>
<tr class="cpd-summary-total">
<td colspan="3">TOTAL INTEREST PAID DURING {{ interest.fy.fy }}</td>
<td>
<strong>₹{{ interest.fy.interestPaid | number: '1.2-2' }}</strong>
</td>
</tr>
</tbody>
</table>
<p class="cpd-para">
This certificate is issued at the request of the borrower for income-tax / financial record purposes and
reflects the interest credited to the captioned loan account during the said financial year as per our books.
</p>
}
<!-- ── SHARE CERTIFICATE ── -->
@if (isShare && share) {
<div class="cpd-block">
<div class="cpd-block-l">Issued To,</div>
<div class="cpd-block-v">
<div>
<strong>{{ share.memberName }}</strong>
</div>
<div>
Member ID: <span class="mono">{{ share.memberId }}</span> · PAN:
<span class="mono">{{ share.memberPan }}</span>
</div>
<div>{{ share.memberAddress }}</div>
</div>
</div>
<p class="cpd-para cpd-para--centered">
This is to certify that <strong>{{ share.memberName }}</strong> is the registered holder of
<strong>{{ share.shareCount | number }}</strong> ({{ classLabelText() }}) shares of
<strong>₹{{ share.faceValue | number: '1.0-0' }}</strong> each, fully paid up, in <strong>{{ bankName }}</strong
>, subject to the bye-laws of the bank.
</p>
<table class="cpd-summary">
<tbody>
<tr>
<td>Certificate No.</td>
<td class="mono">
<strong>{{ share.certificateNo }}</strong>
</td>
<td>Folio No.</td>
<td class="mono">{{ share.folioNo }}</td>
</tr>
<tr>
<td>Branch</td>
<td>{{ share.branch }}</td>
<td>Member Since</td>
<td>{{ share.joiningDate | date: 'd MMM y' }}</td>
</tr>
<tr>
<td>Share Class</td>
<td>{{ classLabelText() }}</td>
<td>Date of Issue</td>
<td>{{ share.issueDate | date: 'd MMM y' }}</td>
</tr>
<tr>
<td>Share Numbers</td>
<td class="mono">{{ share.shareNoFrom | number }} &ndash; {{ share.shareNoTo | number }}</td>
<td>Dividend Paid (Cumulative)</td>
<td>₹{{ share.dividendYtd | number: '1.0-0' }}</td>
</tr>
<tr class="cpd-summary-total">
<td colspan="3">TOTAL FACE VALUE</td>
<td>
<strong> ₹{{ share.shareCount * share.faceValue | number: '1.2-2' }} </strong>
</td>
</tr>
</tbody>
</table>
<p class="cpd-para">
Given under the common seal of the bank this {{ issuedOn | date: 'd' }} day of {{ issuedOn | date: 'MMMM y' }}.
</p>
}
<!-- ── BALANCE CERTIFICATE ── -->
@if (isBalance && balance) {
<div class="cpd-block">
<div class="cpd-block-l">To Whom It May Concern,</div>
<div class="cpd-block-v">
<div>
<strong>{{ balance.record.customerName }}</strong>
</div>
<div>
Customer ID: <span class="mono">{{ balance.record.customerId }}</span> · PAN:
<span class="mono">{{ balance.record.customerPan }}</span>
</div>
<div>{{ balance.record.customerAddress }}</div>
@if (balance.record.jointHolders) {
<div class="small">Jointly with: {{ balance.record.jointHolders }}</div>
}
</div>
</div>
<p class="cpd-para">
This is to certify that <strong>{{ balance.record.customerName }}</strong> (Customer ID
<strong>{{ balance.record.customerId }}</strong
>) maintains the following account with our bank, and the balance available in the said account as on
<strong>{{ balance.asOnDate | date: 'd MMMM y' }}</strong> is as detailed below.
</p>
<table class="cpd-summary">
<tbody>
<tr>
<td>Account No.</td>
<td class="mono">
<strong>{{ balance.record.accountNo }}</strong>
</td>
<td>Branch</td>
<td>{{ balance.record.branch }}</td>
</tr>
<tr>
<td>Account Type</td>
<td>{{ accountTypeLabelText() }}</td>
<td>Product</td>
<td>{{ balance.record.productName }}</td>
</tr>
<tr>
<td>Account Opened</td>
<td>{{ balance.record.openedOn | date: 'd MMM y' }}</td>
<td>Interest Rate</td>
<td>
@if (balance.record.interestRate) {
{{ balance.record.interestRate }}% p.a.
} @else {
—
}
</td>
</tr>
<tr>
<td>Available Balance</td>
<td>₹{{ balance.record.availableBalance | number: '1.2-2' }}</td>
<td>Accrued Interest</td>
<td>₹{{ balance.record.accruedInterest | number: '1.2-2' }}</td>
</tr>
<tr class="cpd-summary-total">
<td colspan="3">TOTAL LEDGER BALANCE AS ON {{ balance.asOnDate | date: 'd MMM y' }}</td>
<td>
<strong>₹{{ balance.record.ledgerBalance | number: '1.2-2' }}</strong>
</td>
</tr>
</tbody>
</table>
<p class="cpd-para">
This certificate is issued at the request of the customer for record purposes and reflects the balance available
in the said account as per our books as on the date mentioned above.
</p>
}
<!-- ── FORM 60 ── -->
@if (isForm60 && form60) {
<p class="cpd-para">
Declaration under <strong>Section 114B</strong> of the Income-tax Rules, 1962, to be made by a person who does
not have a Permanent Account Number (PAN) and who enters into any transaction specified in rule 114B.
</p>
<table class="cpd-summary">
<tbody>
<tr>
<td>1. Full Name (in Block Letters)</td>
<td colspan="3">
<strong>{{ form60.customerName }}</strong>
</td>
</tr>
<tr>
<td>2. Father's / Spouse's Name</td>
<td colspan="3">{{ form60.fatherName }}</td>
</tr>
<tr>
<td>3. Date of Birth</td>
<td>{{ form60.dob | date: 'd MMM y' }}</td>
<td>4. Customer ID</td>
<td class="mono">{{ form60.customerId }}</td>
</tr>
<tr>
<td>5. Address</td>
<td colspan="3">{{ form60.customerAddress }}</td>
</tr>
<tr>
<td>6. ID Proof Submitted</td>
<td>{{ idProofLabelText() }}</td>
<td>7. ID Document No.</td>
<td class="mono">{{ form60.idProofNumber }}</td>
</tr>
<tr>
<td>8. Nature of Transaction</td>
<td colspan="3">{{ form60.transactionType }}</td>
</tr>
<tr>
<td>9. Transaction Amount</td>
<td>₹{{ form60.transactionAmount | number: '1.2-2' }}</td>
<td>10. Transaction Date</td>
<td>{{ form60.transactionDate | date: 'd MMM y' }}</td>
</tr>
<tr>
<td>11. Estimated Total Income (Current FY)</td>
<td>₹{{ form60.estimatedIncome | number: '1.0-0' }}</td>
<td>12. Agricultural Income</td>
<td>₹{{ form60.agriIncome | number: '1.0-0' }}</td>
</tr>
<tr class="cpd-summary-total">
<td colspan="3">PAN STATUS</td>
<td>
<strong>NOT ALLOTTED — Form 60 declared</strong>
</td>
</tr>
</tbody>
</table>
<p class="cpd-para">
I, <strong>{{ form60.customerName }}</strong
>, do hereby declare that the particulars given above are true and correct to the best of my knowledge and
belief. I further declare that I have applied / not applied for a Permanent Account Number with the Income-tax
department, and that I have neither been assessed nor am liable to be assessed under the Income-tax Act, 1961.
</p>
}
<!-- ── FORM 15G / 15H ── -->
@if (isForm15gh && form15gh) {
<p class="cpd-para">
Declaration under <strong>Section {{ form15gh.kind === '15H' ? '197A(1C)' : '197A(1) / 197A(1A)' }}</strong>
of the Income-tax Act, 1961, for non-deduction of tax at source on certain incomes —
{{
form15gh.kind === '15H'
? 'applicable to resident individuals aged 60 years or above'
: 'applicable to resident individuals (other than companies / firms) below 60 years'
}}.
</p>
<table class="cpd-summary">
<tbody>
<tr>
<td>1. Name of Assessee</td>
<td colspan="3">
<strong>{{ form15gh.record.customerName }}</strong>
</td>
</tr>
<tr>
<td>2. PAN of Assessee</td>
<td class="mono">{{ form15gh.record.customerPan }}</td>
<td>3. Status</td>
<td>Individual</td>
</tr>
<tr>
<td>4. Previous Year</td>
<td>{{ form15gh.record.fy }}</td>
<td>5. Age (years)</td>
<td>{{ form15gh.age }}</td>
</tr>
<tr>
<td>6. Address</td>
<td colspan="3">{{ form15gh.record.customerAddress }}</td>
</tr>
<tr>
<td>7. Branch</td>
<td>{{ form15gh.record.branch }}</td>
<td>8. Customer ID</td>
<td class="mono">{{ form15gh.record.customerId }}</td>
</tr>
<tr>
<td>9. Deposit Principal</td>
<td>₹{{ form15gh.record.depositPrincipal | number: '1.0-0' }}</td>
<td>10. Estimated Interest ({{ form15gh.record.fy }})</td>
<td>
<strong>₹{{ form15gh.record.estimatedInterest | number: '1.0-0' }}</strong>
</td>
</tr>
<tr>
<td>11. Estimated Total Income</td>
<td>₹{{ form15gh.record.estimatedIncome | number: '1.0-0' }}</td>
<td>12. Previously Assessed?</td>
<td>{{ form15gh.record.assessed ? 'Yes' : 'No' }}</td>
</tr>
<tr class="cpd-summary-total">
<td colspan="3">TAX PAYABLE ON ESTIMATED INCOME</td>
<td><strong>NIL</strong></td>
</tr>
</tbody>
</table>
<p class="cpd-para">
I, <strong>{{ form15gh.record.customerName }}</strong
>, do hereby declare that the particulars given above are true and correct, and that the tax on my estimated
total income for the previous year {{ form15gh.record.fy }} will be NIL. I therefore request that no tax be
deducted at source on the interest paid to me by the bank on the captioned deposits during the said previous
year.
</p>
}
<!-- ── HYPOTHECATION CERTIFICATE ── -->
@if (isHypothecation && hypothecation) {
<div class="cpd-block">
<div class="cpd-block-l">Issued To,</div>
<div class="cpd-block-v">
<div>
<strong>{{ hypothecation.customerName }}</strong>
</div>
<div>
Customer ID: <span class="mono">{{ hypothecation.customerId }}</span> · PAN:
<span class="mono">{{ hypothecation.customerPan }}</span>
</div>
<div>{{ hypothecation.customerAddress }}</div>
</div>
</div>
<p class="cpd-para">
This is to certify that the asset described below stands hypothecated to
<strong>{{ bankName }}</strong> as security for the captioned loan facility extended to
<strong>{{ hypothecation.customerName }}</strong
>, and the bank holds the first and exclusive charge over the said asset until the loan is fully discharged.
</p>
<table class="cpd-summary">
<tbody>
<tr>
<td>Certificate No.</td>
<td class="mono">
<strong>{{ hypothecation.certificateNo }}</strong>
</td>
<td>Loan Account No.</td>
<td class="mono">{{ hypothecation.accountNo }}</td>
</tr>
<tr>
<td>Loan Type</td>
<td>{{ hypothecation.loanType }}</td>
<td>Branch</td>
<td>{{ hypothecation.branch }}</td>
</tr>
<tr>
<td>Sanctioned Amount</td>
<td>₹{{ hypothecation.sanctionedAmount | number: '1.0-0' }}</td>
<td>Disbursed On</td>
<td>{{ hypothecation.disbursementDate | date: 'd MMM y' }}</td>
</tr>
<tr>
<td>Asset Type</td>
<td>{{ assetLabelText() }}</td>
<td>Identifier</td>
<td class="mono">{{ hypothecation.assetIdentifier }}</td>
</tr>
<tr>
<td>Asset Description</td>
<td colspan="3">{{ hypothecation.assetDescription }}</td>
</tr>
@if (hypothecation.insuredValue > 0) {
<tr>
<td>Insured Value</td>
<td>₹{{ hypothecation.insuredValue | number: '1.0-0' }}</td>
<td>Insurance Valid Till</td>
<td>{{ hypothecation.insuranceValidTill | date: 'd MMM y' }}</td>
</tr>
}
<tr class="cpd-summary-total">
<td colspan="3">OUTSTANDING PRINCIPAL</td>
<td>
<strong>₹{{ hypothecation.outstandingPrincipal | number: '1.2-2' }}</strong>
</td>
</tr>
</tbody>
</table>
<p class="cpd-para">
The lien on the said asset shall stand released only upon the bank issuing a No-Objection Certificate (NOC)
after the entire outstanding under the captioned facility is paid in full, together with all interest, charges,
and incidental costs.
</p>
}
<!-- ── NO-OBJECTION CERTIFICATE ── -->
@if (isNoc && noc) {
<div class="cpd-block">
<div class="cpd-block-l">Issued To,</div>
<div class="cpd-block-v">
<div>
<strong>{{ noc.customerName }}</strong>
</div>
<div>
Customer ID: <span class="mono">{{ noc.customerId }}</span> · PAN:
<span class="mono">{{ noc.customerPan }}</span>
</div>
<div>{{ noc.customerAddress }}</div>
</div>
</div>
<p class="cpd-para">
This is to certify that the captioned loan account in the name of
<strong>{{ noc.customerName }}</strong>
has been
@if (noc.status === 'ISSUED') {
duly closed and the bank
} @else {
closed and the bank
}
has <strong>NO OBJECTION</strong> to the release of the lien hereinafter recorded over the asset described
below. All dues under the said facility, including principal, interest, and incidental charges, have been
received by the bank as on the closure date.
</p>
<table class="cpd-summary">
<tbody>
<tr>
<td>NOC No.</td>
<td class="mono">
<strong>{{ noc.nocNo }}</strong>
</td>
<td>Loan Account No.</td>
<td class="mono">{{ noc.accountNo }}</td>
</tr>
<tr>
<td>Loan Type</td>
<td>{{ nocLoanTypeLabel() }}</td>
<td>Branch</td>
<td>{{ noc.branch }}</td>
</tr>
<tr>
<td>Sanctioned Amount</td>
<td>₹{{ noc.sanctionedAmount | number: '1.0-0' }}</td>
<td>Disbursed On</td>
<td>{{ noc.disbursementDate | date: 'd MMM y' }}</td>
</tr>
@if (noc.closureDate) {
<tr>
<td>Account Closed On</td>
<td>
<strong>{{ noc.closureDate | date: 'd MMM y' }}</strong>
</td>
<td>Total Paid</td>
<td>₹{{ noc.totalPaid | number: '1.2-2' }}</td>
</tr>
}
@if (noc.assetType !== 'NONE') {
<tr>
<td>Asset Type</td>
<td>{{ nocAssetLabel() }}</td>
<td>Identifier</td>
<td class="mono">{{ noc.assetIdentifier }}</td>
</tr>
<tr>
<td>Asset Description</td>
<td colspan="3">{{ noc.assetDescription }}</td>
</tr>
}
@if (noc.forwardTo) {
<tr>
<td>Forward To</td>
<td colspan="3">{{ noc.forwardTo }}</td>
</tr>
}
<tr class="cpd-summary-total">
<td colspan="3">
@if (noc.assetType === 'NONE') {
ACCOUNT CLOSED — NO DUES OUTSTANDING
} @else {
LIEN RELEASED — NO OBJECTION TO TRANSFER
}
</td>
<td><strong>NIL</strong></td>
</tr>
</tbody>
</table>
<p class="cpd-para">
@if (noc.assetType === 'VEHICLE') {
The Regional Transport Authority is hereby requested to delete the hypothecation endorsement of the bank from
the registration certificate of the captioned vehicle.
} @else if (noc.assetType === 'PROPERTY') {
The Sub-Registrar's office is hereby requested to delete the equitable / registered mortgage of the bank from
the property records of the captioned property.
} @else if (noc.assetType === 'GOLD') {
The pledged gold ornaments described above may be released to the customer / authorised representative after
due verification at the branch counter.
} @else if (noc.assetType !== 'NONE') {
The bank's lien over the captioned asset stands released with effect from the closure date, and the customer
is free to deal with the said asset.
} @else {
This certificate is issued at the request of the customer for record purposes and confirms that no dues are
outstanding under the captioned facility.
}
</p>
@if (noc.status === 'ISSUED' && noc.issuedOn) {
<p class="cpd-para">
<strong>Issued on:</strong> {{ noc.issuedOn | date: 'd MMM y' }}
@if (noc.issuedBy) {
<strong>· By:</strong> {{ noc.issuedBy }}
}
</p>
}
}
<!-- Common sign-off -->
<div class="cpd-signoff">
<div class="cpd-sign-block">
<div class="cpd-sig-line"></div>
<div class="cpd-sig-l">Authorised Signatory</div>
@if (isInterest && interest) {
<div class="cpd-sig-sub">{{ interest.record.branch }}</div>
}
@if (isShare && share) {
<div class="cpd-sig-sub">{{ share.branch }}</div>
}
@if (isBalance && balance) {
<div class="cpd-sig-sub">{{ balance.record.branch }}</div>
}
@if (isForm60 && form60) {
<div class="cpd-sig-sub">{{ form60.branch }}</div>
}
@if (isForm15gh && form15gh) {
<div class="cpd-sig-sub">{{ form15gh.record.branch }}</div>
}
@if (isHypothecation && hypothecation) {
<div class="cpd-sig-sub">{{ hypothecation.branch }}</div>
}
@if (isNoc && noc) {
<div class="cpd-sig-sub">{{ noc.branch }}</div>
}
</div>
<div class="cpd-seal">
<div class="cpd-seal-ring">
<div class="cpd-seal-inner">
<div class="cpd-seal-bank">NEXORA</div>
<div class="cpd-seal-line"></div>
<div class="cpd-seal-sub">CO-OPERATIVE BANK</div>
</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="cpd-footer">
<span>
Issued by {{ bankName }} on {{ issuedOn | date: 'd MMM y' }}. Computer generated — does not require physical
signature for record purposes.
</span>
</div>
</div>
</div>
@@ -0,0 +1,607 @@
/**
* 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/.
*/
:host {
--cpd-blue: #1d4ed8;
--cpd-blue-d: #1e40af;
--cpd-blue-l: #eff6ff;
--cpd-blue-tint: #dbeafe;
--cpd-amber: #d97706;
--cpd-amber-l: #fef3c7;
--cpd-amber-tint: #fde68a;
--cpd-red: #b91c1c;
--cpd-red-l: #fee2e2;
--cpd-red-tint: #fecaca;
--cpd-green: #15803d;
--cpd-green-l: #dcfce7;
--cpd-purple: #6d28d9;
--cpd-g50: #f8fafc;
--cpd-g100: #f1f5f9;
--cpd-g200: #e2e8f0;
--cpd-g300: #cbd5e1;
--cpd-g500: #64748b;
--cpd-g700: #334155;
--cpd-g900: #0f172a;
display: block;
}
.cpd-root {
background: var(--cpd-g100);
width: 100%;
max-height: 90vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* ───────── Toolbar (hidden on print) ───────── */
.cpd-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 12px 16px;
background: #fff;
border-bottom: 1px solid var(--cpd-g200);
flex-shrink: 0;
}
.cpd-toolbar-l {
display: flex;
align-items: center;
gap: 12px;
}
.cpd-toolbar-ic {
width: 36px;
height: 36px;
border-radius: 10px;
background: var(--cpd-blue-d);
color: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 15px;
}
.cpd-toolbar-t {
font-size: 14px;
font-weight: 800;
color: var(--cpd-g900);
}
.cpd-toolbar-s {
font-size: 11.5px;
color: var(--cpd-g500);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.cpd-toolbar-r {
display: inline-flex;
gap: 8px;
}
.cpd-tb-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 7px 12px;
border: 1px solid var(--cpd-g200);
background: #fff;
color: var(--cpd-g700);
font-size: 12.5px;
font-weight: 700;
border-radius: 8px;
cursor: pointer;
transition: all 0.15s ease;
fa-icon {
font-size: 12px;
color: var(--cpd-blue);
}
&:hover {
background: var(--cpd-blue-l);
border-color: var(--cpd-blue-tint);
color: var(--cpd-blue-d);
}
&.cpd-tb-x {
width: 34px;
padding: 0;
justify-content: center;
&:hover {
background: var(--cpd-red-l);
color: var(--cpd-red);
border-color: var(--cpd-red-tint);
fa-icon {
color: var(--cpd-red);
}
}
}
}
/* ───────── Certificate page (A4 feel) ───────── */
.cpd-page {
background: #fff;
border: 1px solid var(--cpd-g200);
padding: 38px 48px 24px;
margin: 18px auto;
width: min(720px, 94%);
border-top: 6px solid var(--cpd-blue-d);
font-family: Georgia, 'Times New Roman', Times, serif;
color: var(--cpd-g900);
line-height: 1.6;
overflow: auto;
position: relative;
&.cpd-page--share,
&.cpd-page--amber {
border-top-color: var(--cpd-amber);
}
&.cpd-page--teal {
border-top-color: #0d9488;
}
&.cpd-page--red {
border-top-color: var(--cpd-red);
}
&.cpd-page--green {
border-top-color: var(--cpd-green);
}
/* Subtle decorative border inside the page edge to give the cert a "framed" feel. */
&::before {
content: '';
position: absolute;
inset: 18px;
border: 1px solid var(--cpd-g200);
pointer-events: none;
border-radius: 4px;
}
&.cpd-page--share::before,
&.cpd-page--amber::before {
border-color: var(--cpd-amber-tint);
}
&.cpd-page--teal::before {
border-color: #99f6e4;
}
&.cpd-page--red::before {
border-color: var(--cpd-red-tint);
}
&.cpd-page--green::before {
border-color: #bbf7d0;
}
}
/* ───────── Letterhead ───────── */
.cpd-letterhead {
text-align: center;
padding-bottom: 12px;
margin-bottom: 14px;
border-bottom: 1px solid var(--cpd-g200);
}
.cpd-bank-name {
font-size: 19px;
font-weight: 800;
letter-spacing: 0.02em;
color: var(--cpd-blue-d);
.cpd-page--share &,
.cpd-page--amber & {
color: var(--cpd-amber);
}
.cpd-page--teal & {
color: #0d9488;
}
.cpd-page--red & {
color: var(--cpd-red);
}
.cpd-page--green & {
color: var(--cpd-green);
}
}
.cpd-bank-sub {
font-size: 11.5px;
color: var(--cpd-g500);
margin-top: 2px;
font-family:
ui-sans-serif,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
}
/* ───────── Title strip ───────── */
.cpd-title-strip {
text-align: center;
margin: 6px 0 14px;
font-family:
ui-sans-serif,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
}
.cpd-title-l {
display: inline-block;
padding: 5px 18px;
border-radius: 999px;
font-size: 11.5px;
font-weight: 800;
letter-spacing: 0.15em;
text-transform: uppercase;
background: var(--cpd-blue-l);
color: var(--cpd-blue-d);
border: 1px solid var(--cpd-blue-tint);
.cpd-page--share &,
.cpd-page--amber & {
background: var(--cpd-amber-l);
color: var(--cpd-amber);
border-color: var(--cpd-amber-tint);
}
.cpd-page--teal & {
background: #ccfbf1;
color: #0d9488;
border-color: #99f6e4;
}
.cpd-page--red & {
background: var(--cpd-red-l);
color: var(--cpd-red);
border-color: var(--cpd-red-tint);
}
.cpd-page--green & {
background: var(--cpd-green-l);
color: var(--cpd-green);
border-color: #bbf7d0;
}
}
.cpd-stage-pill--teal {
background: #ccfbf1;
color: #0d9488;
}
.cpd-stage-pill--red {
background: var(--cpd-red-l);
color: var(--cpd-red);
}
.cpd-stage-pill--green {
background: var(--cpd-green-l);
color: var(--cpd-green);
}
/* ───────── Meta row ───────── */
.cpd-meta-row {
display: flex;
justify-content: space-between;
gap: 16px;
font-family:
ui-sans-serif,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
font-size: 12px;
margin-bottom: 16px;
padding-bottom: 14px;
border-bottom: 1px dashed var(--cpd-g200);
> div {
display: flex;
flex-direction: column;
gap: 2px;
}
}
.cpd-meta-l {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--cpd-g500);
}
.cpd-meta-v {
font-size: 12.5px;
color: var(--cpd-g900);
font-weight: 700;
}
.cpd-stage-pill {
display: inline-flex;
align-items: center;
padding: 3px 10px;
border-radius: 999px;
font-size: 10.5px;
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
background: var(--cpd-blue-l);
color: var(--cpd-blue-d);
&--amber {
background: var(--cpd-amber-l);
color: var(--cpd-amber);
}
}
/* ───────── Addressee + body ───────── */
.cpd-block {
display: flex;
gap: 8px;
font-family:
ui-sans-serif,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
font-size: 13px;
margin-bottom: 14px;
}
.cpd-block-l {
font-weight: 700;
color: var(--cpd-g700);
width: 70px;
flex-shrink: 0;
}
.cpd-block-v {
flex: 1;
color: var(--cpd-g900);
line-height: 1.5;
}
.cpd-para {
margin: 12px 0;
font-size: 13px;
text-align: justify;
color: var(--cpd-g900);
&--centered {
text-align: center;
font-size: 13.5px;
padding: 6px 12px;
background: var(--cpd-amber-l);
border-left: 4px solid var(--cpd-amber);
border-radius: 0 6px 6px 0;
}
}
.cpd-amount {
color: var(--cpd-blue-d);
font-variant-numeric: tabular-nums;
}
/* ───────── Summary table ───────── */
.cpd-summary {
width: 100%;
margin: 14px 0;
border-collapse: collapse;
font-family:
ui-sans-serif,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
font-size: 12px;
td {
padding: 8px 10px;
border: 1px solid var(--cpd-g200);
vertical-align: top;
color: var(--cpd-g900);
}
td:nth-child(odd) {
background: var(--cpd-g50);
color: var(--cpd-g500);
font-weight: 700;
font-size: 10.5px;
text-transform: uppercase;
letter-spacing: 0.04em;
width: 22%;
}
.mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.cpd-summary-total td {
background: var(--cpd-blue-l);
color: var(--cpd-blue-d);
font-weight: 800;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.04em;
&:last-child {
text-align: right;
font-variant-numeric: tabular-nums;
}
}
.cpd-page--share & .cpd-summary-total td {
background: var(--cpd-amber-l);
color: var(--cpd-amber);
}
}
/* ───────── Sign-off + bank seal ───────── */
.cpd-signoff {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 24px;
margin-top: 26px;
}
.cpd-sign-block {
font-family:
ui-sans-serif,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
}
.cpd-sig-line {
width: 220px;
border-top: 1px solid var(--cpd-g500);
margin-bottom: 5px;
}
.cpd-sig-l {
font-size: 12px;
font-weight: 700;
color: var(--cpd-g900);
}
.cpd-sig-sub {
font-size: 11px;
color: var(--cpd-g500);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
/* Decorative round "common seal" for the share certificate (also looks good on
the interest certificate). Pure CSS, prints fine. */
.cpd-seal-ring {
width: 96px;
height: 96px;
border-radius: 50%;
border: 2px solid var(--cpd-amber);
display: flex;
align-items: center;
justify-content: center;
position: relative;
background: #fff;
.cpd-page--interest & {
border-color: var(--cpd-blue);
}
&::after {
content: '';
position: absolute;
inset: 5px;
border-radius: 50%;
border: 1px dashed var(--cpd-amber);
.cpd-page--interest & {
border-color: var(--cpd-blue);
}
}
}
.cpd-seal-inner {
text-align: center;
position: relative;
z-index: 1;
}
.cpd-seal-bank {
font-size: 12px;
font-weight: 800;
letter-spacing: 0.18em;
color: var(--cpd-amber);
.cpd-page--interest & {
color: var(--cpd-blue);
}
}
.cpd-seal-line {
width: 36px;
border-top: 1px solid currentcolor;
margin: 3px auto;
color: var(--cpd-amber);
.cpd-page--interest & {
color: var(--cpd-blue);
}
}
.cpd-seal-sub {
font-size: 7.5px;
font-weight: 800;
letter-spacing: 0.12em;
color: var(--cpd-g500);
}
/* ───────── Footer ───────── */
.cpd-footer {
margin-top: 24px;
padding-top: 10px;
border-top: 1px dashed var(--cpd-g200);
font-family:
ui-sans-serif,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
font-size: 10.5px;
color: var(--cpd-g500);
text-align: center;
}
.mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
/* ───────── Print — only the page, no toolbar / dialog chrome ───────── */
@media print {
:host {
background: #fff;
}
.no-print {
display: none !important;
}
.cpd-root {
background: #fff;
max-height: none;
overflow: visible;
height: auto;
}
.cpd-page {
border: 0;
margin: 0;
padding: 24px 32px;
width: 100%;
box-shadow: none;
border-top: 4px solid var(--cpd-blue-d);
&.cpd-page--share {
border-top-color: var(--cpd-amber);
}
}
}
@@ -0,0 +1,322 @@
/**
* 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/.
*/
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatTooltip } from '@angular/material/tooltip';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { FyInterestBreakup, LoanInterestRecord } from '../loan-interest/loan-interest.component';
import { ShareCertificateRecord } from '../share-certificate/share-certificate.component';
import { BalanceCertificateRecord } from '../balance-certificate/balance-certificate.component';
import { Form60Record } from '../form-60/form-60.component';
import { Form15Kind, Form15ghRecord } from '../form-15gh/form-15gh.component';
import { HypothecationRecord } from '../hypothecation/hypothecation.component';
import { NocRecord } from '../noc/noc.component';
import {
downloadBalanceCertificatePdf,
downloadForm15ghPdf,
downloadForm60Pdf,
downloadHypothecationPdf,
downloadLoanInterestPdf,
downloadNocPdf,
downloadShareCertificatePdf
} from '../certificate-pdf';
export interface InterestPreviewPayload {
record: LoanInterestRecord;
fy: FyInterestBreakup;
}
export interface BalancePreviewPayload {
record: BalanceCertificateRecord;
asOnDate: string;
}
export interface Form15ghPreviewPayload {
record: Form15ghRecord;
kind: Form15Kind;
age: number;
}
export type CertificateKind = 'interest' | 'share' | 'balance' | 'form60' | 'form15gh' | 'hypothecation' | 'noc';
export interface CertificatePreviewDialogData {
kind: CertificateKind;
loanInterest?: InterestPreviewPayload;
shareCertificate?: ShareCertificateRecord;
balance?: BalancePreviewPayload;
form60?: Form60Record;
form15gh?: Form15ghPreviewPayload;
hypothecation?: HypothecationRecord;
noc?: NocRecord;
}
@Component({
selector: 'mifosx-certificate-preview-dialog',
templateUrl: './certificate-preview-dialog.component.html',
styleUrls: ['./certificate-preview-dialog.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
CommonModule,
FaIconComponent,
MatTooltip
]
})
export class CertificatePreviewDialogComponent {
private dialogRef = inject(MatDialogRef<CertificatePreviewDialogComponent>);
data: CertificatePreviewDialogData = inject(MAT_DIALOG_DATA);
readonly bankName = 'Nexora Co-operative Bank Ltd.';
readonly bankAddress = 'Head Office, 4th Floor, Nexora Tower, Mumbai 400051';
readonly bankRegNo = 'RBI Lic. No. NX-2018/MH/482';
readonly bankContact = 'info@nexorabank.in · 022 6789 0000';
get isInterest(): boolean {
return this.data.kind === 'interest';
}
get isShare(): boolean {
return this.data.kind === 'share';
}
get isBalance(): boolean {
return this.data.kind === 'balance';
}
get isForm60(): boolean {
return this.data.kind === 'form60';
}
get isForm15gh(): boolean {
return this.data.kind === 'form15gh';
}
get isHypothecation(): boolean {
return this.data.kind === 'hypothecation';
}
get isNoc(): boolean {
return this.data.kind === 'noc';
}
get interest(): InterestPreviewPayload | undefined {
return this.data.loanInterest;
}
get share(): ShareCertificateRecord | undefined {
return this.data.shareCertificate;
}
get balance(): BalancePreviewPayload | undefined {
return this.data.balance;
}
get form60(): Form60Record | undefined {
return this.data.form60;
}
get form15gh(): Form15ghPreviewPayload | undefined {
return this.data.form15gh;
}
get hypothecation(): HypothecationRecord | undefined {
return this.data.hypothecation;
}
get noc(): NocRecord | undefined {
return this.data.noc;
}
/** Tone class on `.cpd-page` — drives the accent stripe + seal + pill colour. */
get pageToneClass(): string {
switch (this.data.kind) {
case 'share':
case 'form60':
return 'cpd-page--amber';
case 'form15gh':
return 'cpd-page--teal';
case 'hypothecation':
return 'cpd-page--red';
case 'noc':
return 'cpd-page--green';
case 'balance':
case 'interest':
default:
return '';
}
}
get title(): string {
switch (this.data.kind) {
case 'interest':
return 'Interest-Paid Certificate';
case 'share':
return 'Share Certificate';
case 'balance':
return 'Balance Certificate';
case 'form60':
return 'Form 60 Declaration';
case 'form15gh':
return this.form15gh?.kind === '15H' ? 'Form 15H Declaration' : 'Form 15G Declaration';
case 'hypothecation':
return 'Hypothecation Certificate';
case 'noc':
return 'No-Objection Certificate';
}
}
get refLine(): string {
if (this.isInterest && this.interest) {
return `${this.interest.record.accountNo} · ${this.interest.fy.fy}`;
}
if (this.isShare && this.share) {
return `${this.share.certificateNo} · Folio ${this.share.folioNo}`;
}
if (this.isBalance && this.balance) {
return `${this.balance.record.accountNo} · as on ${this.balance.asOnDate}`;
}
if (this.isForm60 && this.form60) {
return `${this.form60.customerId} · ${this.form60.transactionType}`;
}
if (this.isForm15gh && this.form15gh) {
return `${this.form15gh.record.customerId} · ${this.form15gh.kind} · ${this.form15gh.record.fy}`;
}
if (this.isHypothecation && this.hypothecation) {
return `${this.hypothecation.certificateNo} · ${this.hypothecation.accountNo}`;
}
if (this.isNoc && this.noc) {
return `${this.noc.nocNo} · ${this.noc.accountNo}`;
}
return '';
}
get issuedOn(): string {
return new Date().toISOString().slice(0, 10);
}
close(): void {
this.dialogRef.close();
}
printCertificate(): void {
window.print();
}
downloadPdf(): void {
if (this.isInterest && this.interest) {
downloadLoanInterestPdf(this.interest.record, this.interest.fy);
} else if (this.isShare && this.share) {
downloadShareCertificatePdf(this.share);
} else if (this.isBalance && this.balance) {
downloadBalanceCertificatePdf(this.balance.record, this.balance.asOnDate);
} else if (this.isForm60 && this.form60) {
downloadForm60Pdf(this.form60);
} else if (this.isForm15gh && this.form15gh) {
downloadForm15ghPdf(this.form15gh.record, this.form15gh.kind);
} else if (this.isHypothecation && this.hypothecation) {
downloadHypothecationPdf(this.hypothecation);
} else if (this.isNoc && this.noc) {
downloadNocPdf(this.noc);
}
}
nocLoanTypeLabel(): string {
const t = this.noc?.loanType;
switch (t) {
case 'AUTO':
return 'Auto Loan';
case 'HOUSING':
return 'Housing Loan';
case 'PERSONAL':
return 'Personal Loan';
case 'EDUCATION':
return 'Education Loan';
case 'GOLD':
return 'Gold Loan';
case 'BUSINESS':
return 'Business Loan';
case 'CC':
return 'Cash Credit';
case 'OD':
return 'Overdraft';
default:
return '';
}
}
nocAssetLabel(): string {
const t = this.noc?.assetType;
switch (t) {
case 'VEHICLE':
return 'Vehicle';
case 'PROPERTY':
return 'Property';
case 'GOLD':
return 'Gold / Bullion';
case 'EQUIPMENT':
return 'Equipment';
case 'STOCK_DEBTORS':
return 'Stock / Debtors';
case 'NONE':
return 'Unsecured';
default:
return '';
}
}
classLabelText(): string {
return this.share?.shareClass === 'PREFERENCE' ? 'Preference' : 'Ordinary';
}
accountTypeLabelText(): string {
const t = this.balance?.record.accountType;
switch (t) {
case 'SAVINGS':
return 'Savings';
case 'CURRENT':
return 'Current';
case 'FIXED_DEPOSIT':
return 'Fixed Deposit';
case 'RD':
return 'Recurring Deposit';
case 'NRE':
return 'NRE Savings';
case 'NRO':
return 'NRO Savings';
default:
return '';
}
}
idProofLabelText(): string {
const t = this.form60?.idProofType;
switch (t) {
case 'AADHAAR':
return 'Aadhaar Card';
case 'VOTER_ID':
return 'Voter Identity Card';
case 'DRIVING_LICENSE':
return 'Driving License';
case 'PASSPORT':
return 'Passport';
case 'RATION':
return 'Ration Card';
default:
return '';
}
}
assetLabelText(): string {
const t = this.hypothecation?.assetType;
switch (t) {
case 'VEHICLE':
return 'Vehicle';
case 'PLANT_MACHINERY':
return 'Plant & Machinery';
case 'STOCK_DEBTORS':
return 'Stock / Debtors';
case 'GOLD':
return 'Gold / Bullion';
case 'EQUIPMENT':
return 'Equipment';
default:
return '';
}
}
}