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:
co-authored by
Claude Opus 4.7
parent
4322f37c6d
commit
325228c514
+152
@@ -0,0 +1,152 @@
|
||||
<!--
|
||||
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="cert-root">
|
||||
<div class="cert-head">
|
||||
<div class="cert-head-l">
|
||||
<fa-icon icon="balance-scale" class="cert-head-ic"></fa-icon>
|
||||
<div>
|
||||
<h2 class="cert-head-t">Balance Certificate</h2>
|
||||
<div class="cert-head-s">Generate an "as-on-date" balance certificate for any deposit / current account.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI strip -->
|
||||
<div class="cert-kpis">
|
||||
<div class="cert-kpi cert-kpi--blue">
|
||||
<fa-icon icon="balance-scale" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">{{ filteredRecords.length | number }}</div>
|
||||
<div class="cert-kpi-l">Accounts</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cert-kpi cert-kpi--green">
|
||||
<fa-icon icon="coins" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">₹{{ totalBalance() | number: '1.0-0' }}</div>
|
||||
<div class="cert-kpi-l">Balance (as on {{ asOnDate | date: 'd MMM y' }})</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cert-kpi cert-kpi--teal">
|
||||
<fa-icon icon="calendar-alt" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">{{ asOnDate | date: 'd MMM y' }}</div>
|
||||
<div class="cert-kpi-l">Snapshot Date</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="cert-filters">
|
||||
<div class="cert-filter">
|
||||
<label>Account / Customer / PAN</label>
|
||||
<div class="cert-input-ic">
|
||||
<fa-icon icon="search"></fa-icon>
|
||||
<input type="text" [(ngModel)]="search" placeholder="e.g. SB10029384 or customer name" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cert-filter">
|
||||
<label>Account type</label>
|
||||
<select [(ngModel)]="accountTypeFilter">
|
||||
<option value="">All types</option>
|
||||
@for (o of accountTypeOptions; track o.value) {
|
||||
<option [value]="o.value">{{ o.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="cert-filter">
|
||||
<label>Balance as on</label>
|
||||
<input type="date" [(ngModel)]="asOnDate" />
|
||||
</div>
|
||||
|
||||
@if (search || accountTypeFilter) {
|
||||
<button type="button" class="cert-clear" (click)="clearFilters()"><fa-icon icon="times"></fa-icon> Clear</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="cert-card">
|
||||
<div class="cert-table-wrap">
|
||||
<table class="cert-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Customer · A/C</th>
|
||||
<th>Type</th>
|
||||
<th>Branch</th>
|
||||
<th>Opened</th>
|
||||
<th class="num">Available Bal.</th>
|
||||
<th class="num">Accrued Int.</th>
|
||||
<th class="num">Ledger Bal.</th>
|
||||
<th class="col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of filteredRecords; track r.accountNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<div class="cert-cust-name">{{ r.customerName }}</div>
|
||||
<div class="mono small muted">{{ r.accountNo }} · {{ r.customerId }} · PAN {{ r.customerPan }}</div>
|
||||
@if (r.jointHolders) {
|
||||
<div class="small muted">Jointly with {{ r.jointHolders }}</div>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<span class="cert-pill cert-pill--type">{{ accountTypeLabel(r.accountType) }}</span>
|
||||
<div class="small muted">{{ r.productName }}</div>
|
||||
</td>
|
||||
<td class="small">{{ r.branch }}</td>
|
||||
<td class="small">{{ r.openedOn | date: 'd MMM y' }}</td>
|
||||
<td class="num">
|
||||
<strong>₹{{ r.availableBalance | number: '1.2-2' }}</strong>
|
||||
</td>
|
||||
<td class="num">
|
||||
@if (r.accruedInterest > 0) {
|
||||
₹{{ r.accruedInterest | number: '1.2-2' }}
|
||||
} @else {
|
||||
<span class="muted">—</span>
|
||||
}
|
||||
</td>
|
||||
<td class="num">₹{{ r.ledgerBalance | number: '1.2-2' }}</td>
|
||||
<td class="col-actions">
|
||||
<div class="cert-row-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="cert-icon-btn primary"
|
||||
(click)="viewCertificate(r)"
|
||||
matTooltip="View / Print certificate"
|
||||
>
|
||||
<fa-icon icon="eye"></fa-icon>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="cert-icon-btn"
|
||||
(click)="downloadCertificate(r)"
|
||||
matTooltip="Download PDF"
|
||||
>
|
||||
<fa-icon icon="download"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (filteredRecords.length === 0) {
|
||||
<tr>
|
||||
<td colspan="8" class="cert-empty">
|
||||
<fa-icon icon="info-circle"></fa-icon>
|
||||
No accounts match the current filters.
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* 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, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
import {
|
||||
CertificatePreviewDialogComponent,
|
||||
CertificatePreviewDialogData
|
||||
} from '../certificate-preview-dialog/certificate-preview-dialog.component';
|
||||
import { downloadBalanceCertificatePdf } from '../certificate-pdf';
|
||||
|
||||
export type BalanceAccountType = 'SAVINGS' | 'CURRENT' | 'FIXED_DEPOSIT' | 'RD' | 'NRE' | 'NRO';
|
||||
|
||||
export interface BalanceCertificateRecord {
|
||||
accountNo: string;
|
||||
customerId: string;
|
||||
customerName: string;
|
||||
customerPan: string;
|
||||
customerAddress: string;
|
||||
branch: string;
|
||||
accountType: BalanceAccountType;
|
||||
productName: string;
|
||||
openedOn: string;
|
||||
/** Balance available as on the snapshot date. */
|
||||
availableBalance: number;
|
||||
/** Total ledger balance (incl. accrued interest, hold amounts). */
|
||||
ledgerBalance: number;
|
||||
/** Accrued but uncredited interest. */
|
||||
accruedInterest: number;
|
||||
/** Interest rate p.a. (for FDs / RD / interest-bearing accounts). */
|
||||
interestRate?: number;
|
||||
/** Joint-holder names if any. */
|
||||
jointHolders?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-balance-certificate',
|
||||
templateUrl: './balance-certificate.component.html',
|
||||
styleUrls: ['../certificates.shared.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
FaIconComponent,
|
||||
MatTooltip
|
||||
]
|
||||
})
|
||||
export class BalanceCertificateComponent implements OnInit {
|
||||
private dialog = inject(MatDialog);
|
||||
private snack = inject(MatSnackBar);
|
||||
|
||||
// ── Filters ──
|
||||
search = '';
|
||||
accountTypeFilter: '' | BalanceAccountType = '';
|
||||
/** Snapshot date for the balance "as on". */
|
||||
asOnDate = todayIso();
|
||||
|
||||
records: BalanceCertificateRecord[] = [];
|
||||
|
||||
readonly accountTypeOptions: { value: BalanceAccountType; label: string }[] = [
|
||||
{ value: 'SAVINGS', label: 'Savings' },
|
||||
{ value: 'CURRENT', label: 'Current' },
|
||||
{ value: 'FIXED_DEPOSIT', label: 'Fixed Deposit' },
|
||||
{ value: 'RD', label: 'Recurring Deposit' },
|
||||
{ value: 'NRE', label: 'NRE Savings' },
|
||||
{ value: 'NRO', label: 'NRO Savings' }
|
||||
];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.records = seedBalanceCertificates();
|
||||
}
|
||||
|
||||
get filteredRecords(): BalanceCertificateRecord[] {
|
||||
const q = this.search.trim().toLowerCase();
|
||||
return this.records.filter((r) => {
|
||||
if (this.accountTypeFilter && r.accountType !== this.accountTypeFilter) return false;
|
||||
if (!q) return true;
|
||||
return (
|
||||
r.accountNo.toLowerCase().includes(q) ||
|
||||
r.customerName.toLowerCase().includes(q) ||
|
||||
r.customerId.toLowerCase().includes(q) ||
|
||||
r.customerPan.toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
accountTypeLabel(t: BalanceAccountType): string {
|
||||
return this.accountTypeOptions.find((o) => o.value === t)?.label || t;
|
||||
}
|
||||
|
||||
totalBalance(): number {
|
||||
return this.filteredRecords.reduce((s, r) => s + r.availableBalance, 0);
|
||||
}
|
||||
|
||||
clearFilters(): void {
|
||||
this.search = '';
|
||||
this.accountTypeFilter = '';
|
||||
this.asOnDate = todayIso();
|
||||
}
|
||||
|
||||
viewCertificate(r: BalanceCertificateRecord): void {
|
||||
const data: CertificatePreviewDialogData = {
|
||||
kind: 'balance',
|
||||
balance: { record: r, asOnDate: this.asOnDate }
|
||||
};
|
||||
this.dialog.open(CertificatePreviewDialogComponent, {
|
||||
width: '880px',
|
||||
maxWidth: '96vw',
|
||||
maxHeight: '92vh',
|
||||
autoFocus: false,
|
||||
panelClass: 'cert-preview-dialog',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
downloadCertificate(r: BalanceCertificateRecord): void {
|
||||
downloadBalanceCertificatePdf(r, this.asOnDate);
|
||||
this.snack.open(`Balance Certificate (${r.accountNo}) downloaded.`, 'OK', { duration: 2400 });
|
||||
}
|
||||
}
|
||||
|
||||
function todayIso(): string {
|
||||
const d = new Date();
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||
}
|
||||
|
||||
// ───────────────────── Mock seed (top-level export) ─────────────────────
|
||||
export function seedBalanceCertificates(): BalanceCertificateRecord[] {
|
||||
return [
|
||||
{
|
||||
accountNo: 'SB10029384',
|
||||
customerId: 'CUST112233',
|
||||
customerName: 'Rohan S. Mehta',
|
||||
customerPan: 'ABCPM1234E',
|
||||
customerAddress: 'Flat 902, Skyline Apt, Andheri (E), Mumbai 400069',
|
||||
branch: 'Mumbai — Andheri (E)',
|
||||
accountType: 'SAVINGS',
|
||||
productName: 'Savings — Premium',
|
||||
openedOn: '2018-04-22',
|
||||
availableBalance: 482500,
|
||||
ledgerBalance: 482500,
|
||||
accruedInterest: 1820,
|
||||
interestRate: 3.5,
|
||||
jointHolders: 'Priya Mehta'
|
||||
},
|
||||
{
|
||||
accountNo: 'CA20034112',
|
||||
customerId: 'CUST334455',
|
||||
customerName: 'Anita Verma',
|
||||
customerPan: 'EFGHV5678A',
|
||||
customerAddress: 'B-12, Connaught Pl, New Delhi 110001',
|
||||
branch: 'New Delhi — Connaught Place',
|
||||
accountType: 'CURRENT',
|
||||
productName: 'Current — Business',
|
||||
openedOn: '2019-08-11',
|
||||
availableBalance: 1245300,
|
||||
ledgerBalance: 1245300,
|
||||
accruedInterest: 0
|
||||
},
|
||||
{
|
||||
accountNo: 'FD50200077',
|
||||
customerId: 'CUST556677',
|
||||
customerName: 'Karthik Rao',
|
||||
customerPan: 'PQRSR2233Z',
|
||||
customerAddress: '24, MG Road, Bengaluru 560001',
|
||||
branch: 'Bengaluru — MG Road',
|
||||
accountType: 'FIXED_DEPOSIT',
|
||||
productName: 'FD — Senior 18 months',
|
||||
openedOn: '2024-02-15',
|
||||
availableBalance: 500000,
|
||||
ledgerBalance: 526500,
|
||||
accruedInterest: 26500,
|
||||
interestRate: 7.25
|
||||
},
|
||||
{
|
||||
accountNo: 'RD60012345',
|
||||
customerId: 'CUST667788',
|
||||
customerName: 'Lakshmi Naidu',
|
||||
customerPan: 'XYZAN7788Q',
|
||||
customerAddress: 'Plot 14, Road No. 3, Banjara Hills, Hyderabad 500034',
|
||||
branch: 'Hyderabad — Banjara Hills',
|
||||
accountType: 'RD',
|
||||
productName: 'Recurring Deposit — 24m',
|
||||
openedOn: '2023-08-18',
|
||||
availableBalance: 180000,
|
||||
ledgerBalance: 188750,
|
||||
accruedInterest: 8750,
|
||||
interestRate: 6.85
|
||||
},
|
||||
{
|
||||
accountNo: 'NRE70010023',
|
||||
customerId: 'CUST889900',
|
||||
customerName: 'Vikram Joshi',
|
||||
customerPan: 'LMNOJ9988X',
|
||||
customerAddress: 'Office 3B, Koregaon Park Road, Pune 411001',
|
||||
branch: 'Pune — Koregaon Park',
|
||||
accountType: 'NRE',
|
||||
productName: 'NRE Savings — Premium',
|
||||
openedOn: '2020-11-05',
|
||||
availableBalance: 3850000,
|
||||
ledgerBalance: 3850000,
|
||||
accruedInterest: 12400,
|
||||
interestRate: 3.5
|
||||
},
|
||||
{
|
||||
accountNo: 'SB88990022',
|
||||
customerId: 'CUST990011',
|
||||
customerName: 'Arjun Banerjee',
|
||||
customerPan: 'TUVWB4455F',
|
||||
customerAddress: '42, Park Street, Kolkata 700016',
|
||||
branch: 'Kolkata — Park Street',
|
||||
accountType: 'SAVINGS',
|
||||
productName: 'Savings — Regular',
|
||||
openedOn: '2021-09-08',
|
||||
availableBalance: 92300,
|
||||
ledgerBalance: 92300,
|
||||
accruedInterest: 450,
|
||||
interestRate: 3.0
|
||||
}
|
||||
];
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+703
@@ -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 }} – {{ 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>
|
||||
+607
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+322
@@ -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 '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Shared list-page styling for the Letter-Mgmt certificate pages
|
||||
* (Loan Interest, Share Certificate, etc.). Mirrors the visual language of
|
||||
* the notice list (header + filters + table) so the module feels cohesive.
|
||||
*/
|
||||
|
||||
:host {
|
||||
--cert-blue: #1d4ed8;
|
||||
--cert-blue-d: #1e40af;
|
||||
--cert-blue-l: #eff6ff;
|
||||
--cert-blue-tint: #dbeafe;
|
||||
--cert-teal: #0d9488;
|
||||
--cert-teal-l: #ccfbf1;
|
||||
--cert-green: #15803d;
|
||||
--cert-green-l: #dcfce7;
|
||||
--cert-amber: #d97706;
|
||||
--cert-amber-l: #fef3c7;
|
||||
--cert-red: #b91c1c;
|
||||
--cert-red-l: #fee2e2;
|
||||
--cert-red-tint: #fecaca;
|
||||
--cert-purple: #6d28d9;
|
||||
--cert-purple-l: #ede9fe;
|
||||
--cert-g50: #f8fafc;
|
||||
--cert-g100: #f1f5f9;
|
||||
--cert-g200: #e2e8f0;
|
||||
--cert-g300: #cbd5e1;
|
||||
--cert-g500: #64748b;
|
||||
--cert-g700: #334155;
|
||||
--cert-g900: #0f172a;
|
||||
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cert-root {
|
||||
padding: 20px 24px 32px;
|
||||
max-width: 1340px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ───────── Header banner ───────── */
|
||||
.cert-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 18px 22px;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, var(--cert-blue-l) 0%, #fff 65%);
|
||||
border: 1px solid var(--cert-blue-tint);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.cert-head-l {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.cert-head-ic {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 14px;
|
||||
background: var(--cert-blue-d);
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.cert-head-t {
|
||||
margin: 0 0 3px;
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
color: var(--cert-g900);
|
||||
}
|
||||
|
||||
.cert-head-s {
|
||||
font-size: 12.5px;
|
||||
color: var(--cert-g500);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ───────── KPI strip ───────── */
|
||||
.cert-kpis {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
|
||||
@media (width <= 900px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.cert-kpi {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--cert-g200);
|
||||
|
||||
&--blue {
|
||||
background: var(--cert-blue-l);
|
||||
border-color: var(--cert-blue-tint);
|
||||
}
|
||||
|
||||
&--green {
|
||||
background: var(--cert-green-l);
|
||||
border-color: #bbf7d0;
|
||||
}
|
||||
|
||||
&--teal {
|
||||
background: var(--cert-teal-l);
|
||||
border-color: #99f6e4;
|
||||
}
|
||||
|
||||
&--amber {
|
||||
background: var(--cert-amber-l);
|
||||
border-color: #fde68a;
|
||||
}
|
||||
}
|
||||
|
||||
.cert-kpi-ic {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
background: rgb(255 255 255 / 70%);
|
||||
font-size: 16px;
|
||||
|
||||
.cert-kpi--blue & {
|
||||
color: var(--cert-blue);
|
||||
}
|
||||
|
||||
.cert-kpi--green & {
|
||||
color: var(--cert-green);
|
||||
}
|
||||
|
||||
.cert-kpi--teal & {
|
||||
color: var(--cert-teal);
|
||||
}
|
||||
|
||||
.cert-kpi--amber & {
|
||||
color: var(--cert-amber);
|
||||
}
|
||||
}
|
||||
|
||||
.cert-kpi-n {
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
color: var(--cert-g900);
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.cert-kpi-l {
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--cert-g500);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* ───────── Filters ───────── */
|
||||
.cert-filters {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--cert-g200);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cert-filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1 1 0;
|
||||
min-width: 220px;
|
||||
position: relative;
|
||||
|
||||
label {
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
color: var(--cert-g500);
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--cert-g200);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
font-size: 13px;
|
||||
color: var(--cert-g900);
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease;
|
||||
appearance: none;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--cert-blue);
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
padding-right: 34px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8' fill='none' stroke='%2364748b' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='1 2 6 7 11 2'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
background-size: 12px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.cert-input-ic {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
fa-icon {
|
||||
position: absolute;
|
||||
left: 11px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--cert-g500);
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
input {
|
||||
padding-left: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.cert-clear {
|
||||
height: 36px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--cert-g200);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: var(--cert-g700);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
&:hover {
|
||||
background: var(--cert-red-l);
|
||||
color: var(--cert-red);
|
||||
border-color: var(--cert-red-tint);
|
||||
}
|
||||
}
|
||||
|
||||
/* ───────── Table ───────── */
|
||||
.cert-card {
|
||||
background: #fff;
|
||||
border: 1px solid var(--cert-g200);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cert-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.cert-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12.5px;
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
padding: 12px;
|
||||
background: var(--cert-g50);
|
||||
color: var(--cert-g500);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
font-size: 10.5px;
|
||||
border-bottom: 1px solid var(--cert-g200);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--cert-g100);
|
||||
vertical-align: top;
|
||||
color: var(--cert-g900);
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.num {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--cert-g500);
|
||||
}
|
||||
|
||||
.col-actions {
|
||||
width: 88px;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.cert-cust-name {
|
||||
font-weight: 700;
|
||||
color: var(--cert-g900);
|
||||
}
|
||||
|
||||
.cert-interest {
|
||||
color: var(--cert-green);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
/* Pills */
|
||||
.cert-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
background: var(--cert-g100);
|
||||
color: var(--cert-g700);
|
||||
white-space: nowrap;
|
||||
|
||||
&--type {
|
||||
background: var(--cert-purple-l);
|
||||
color: var(--cert-purple);
|
||||
}
|
||||
|
||||
&--green {
|
||||
background: var(--cert-green-l);
|
||||
color: var(--cert-green);
|
||||
}
|
||||
|
||||
&--blue {
|
||||
background: var(--cert-blue-l);
|
||||
color: var(--cert-blue-d);
|
||||
}
|
||||
|
||||
&--amber {
|
||||
background: var(--cert-amber-l);
|
||||
color: var(--cert-amber);
|
||||
}
|
||||
}
|
||||
|
||||
.cert-rate-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
background: var(--cert-blue-l);
|
||||
color: var(--cert-blue-d);
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Row action icon buttons */
|
||||
.cert-row-actions {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.cert-icon-btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--cert-g200);
|
||||
background: #fff;
|
||||
color: var(--cert-g500);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 11.5px;
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s,
|
||||
border-color 0.15s;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--cert-blue-l);
|
||||
color: var(--cert-blue-d);
|
||||
border-color: var(--cert-blue-tint);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background: var(--cert-blue-d);
|
||||
color: #fff;
|
||||
border-color: var(--cert-blue-d);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--cert-blue);
|
||||
border-color: var(--cert-blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cert-empty {
|
||||
text-align: center;
|
||||
padding: 28px 16px;
|
||||
color: var(--cert-g500);
|
||||
font-size: 13px;
|
||||
|
||||
fa-icon {
|
||||
margin-right: 6px;
|
||||
color: var(--cert-blue);
|
||||
}
|
||||
}
|
||||
|
||||
/* Share-certificate specific cells (used by share-certificate.component.html) */
|
||||
.cert-shares {
|
||||
font-weight: 800;
|
||||
color: var(--cert-blue-d);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.cert-folio {
|
||||
font-weight: 700;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
color: var(--cert-g900);
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
<!--
|
||||
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="cert-root">
|
||||
<div class="cert-head">
|
||||
<div class="cert-head-l">
|
||||
<fa-icon icon="file-signature" class="cert-head-ic"></fa-icon>
|
||||
<div>
|
||||
<h2 class="cert-head-t">Form 15G & 15H</h2>
|
||||
<div class="cert-head-s">
|
||||
TDS-waiver declarations for FD / RD interest income —
|
||||
<strong>15G</strong> for individuals below 60 years, <strong>15H</strong> for senior citizens (60+).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI strip -->
|
||||
<div class="cert-kpis">
|
||||
<div class="cert-kpi cert-kpi--blue">
|
||||
<fa-icon icon="user" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">{{ count15G() | number }}</div>
|
||||
<div class="cert-kpi-l">Form 15G (under 60)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cert-kpi cert-kpi--amber">
|
||||
<fa-icon icon="user-tie" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">{{ count15H() | number }}</div>
|
||||
<div class="cert-kpi-l">Form 15H (60+)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cert-kpi cert-kpi--green">
|
||||
<fa-icon icon="check-circle" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">{{ countFiled() | number }}</div>
|
||||
<div class="cert-kpi-l">Filed this FY</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cert-kpi cert-kpi--teal">
|
||||
<fa-icon icon="coins" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">₹{{ totalInterest() | number: '1.0-0' }}</div>
|
||||
<div class="cert-kpi-l">Projected Interest</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="cert-filters">
|
||||
<div class="cert-filter">
|
||||
<label>Customer / Customer ID / PAN</label>
|
||||
<div class="cert-input-ic">
|
||||
<fa-icon icon="search"></fa-icon>
|
||||
<input type="text" [(ngModel)]="search" placeholder="e.g. CUST556677 or customer name" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cert-filter">
|
||||
<label>Form</label>
|
||||
<select [(ngModel)]="kindFilter">
|
||||
<option value="">Both forms</option>
|
||||
<option value="15G">15G — Under 60</option>
|
||||
<option value="15H">15H — Senior citizens (60+)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="cert-filter">
|
||||
<label>Status</label>
|
||||
<select [(ngModel)]="statusFilter">
|
||||
<option value="">All statuses</option>
|
||||
<option value="FILED">Filed</option>
|
||||
<option value="PENDING">Pending</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@if (search || kindFilter || statusFilter) {
|
||||
<button type="button" class="cert-clear" (click)="clearFilters()"><fa-icon icon="times"></fa-icon> Clear</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="cert-card">
|
||||
<div class="cert-table-wrap">
|
||||
<table class="cert-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Customer · PAN</th>
|
||||
<th>Form</th>
|
||||
<th>Age</th>
|
||||
<th>Branch</th>
|
||||
<th class="num">Deposit Principal</th>
|
||||
<th class="num">Est. Interest ({{ records[0]?.fy }})</th>
|
||||
<th>Status</th>
|
||||
<th class="col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of filteredRecords; track r.customerId) {
|
||||
<tr>
|
||||
<td>
|
||||
<div class="cert-cust-name">{{ r.customerName }}</div>
|
||||
<div class="mono small muted">{{ r.customerId }} · PAN {{ r.customerPan }}</div>
|
||||
<div class="small muted">DOB {{ r.dob | date: 'd MMM y' }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
class="cert-pill"
|
||||
[class.cert-pill--blue]="formKind(r) === '15G'"
|
||||
[class.cert-pill--amber]="formKind(r) === '15H'"
|
||||
>
|
||||
Form {{ formKind(r) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="small">{{ age(r.dob) }} yrs</td>
|
||||
<td class="small">{{ r.branch }}</td>
|
||||
<td class="num">₹{{ r.depositPrincipal | number: '1.0-0' }}</td>
|
||||
<td class="num">
|
||||
<strong class="cert-interest">₹{{ r.estimatedInterest | number: '1.0-0' }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
@if (r.status === 'FILED') {
|
||||
<span class="cert-pill cert-pill--green"> <fa-icon icon="check-circle"></fa-icon> Filed </span>
|
||||
@if (r.filedOn) {
|
||||
<div class="mono small muted">{{ r.filedOn | date: 'd MMM y' }}</div>
|
||||
}
|
||||
} @else {
|
||||
<span class="cert-pill cert-pill--amber"> <fa-icon icon="hourglass-half"></fa-icon> Pending </span>
|
||||
}
|
||||
</td>
|
||||
<td class="col-actions">
|
||||
<div class="cert-row-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="cert-icon-btn primary"
|
||||
(click)="viewCertificate(r)"
|
||||
matTooltip="View / Print declaration"
|
||||
>
|
||||
<fa-icon icon="eye"></fa-icon>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="cert-icon-btn"
|
||||
(click)="downloadCertificate(r)"
|
||||
matTooltip="Download PDF"
|
||||
>
|
||||
<fa-icon icon="download"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (filteredRecords.length === 0) {
|
||||
<tr>
|
||||
<td colspan="8" class="cert-empty">
|
||||
<fa-icon icon="info-circle"></fa-icon>
|
||||
No declarations match the current filters.
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* 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, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
import {
|
||||
CertificatePreviewDialogComponent,
|
||||
CertificatePreviewDialogData
|
||||
} from '../certificate-preview-dialog/certificate-preview-dialog.component';
|
||||
import { downloadForm15ghPdf } from '../certificate-pdf';
|
||||
|
||||
export type Form15Kind = '15G' | '15H';
|
||||
export type Form15Status = 'FILED' | 'PENDING';
|
||||
|
||||
export interface Form15ghRecord {
|
||||
customerId: string;
|
||||
customerName: string;
|
||||
customerPan: string;
|
||||
customerAddress: string;
|
||||
dob: string;
|
||||
branch: string;
|
||||
/** Aggregate interest the bank will pay this FY. */
|
||||
estimatedInterest: number;
|
||||
/** Customer's total estimated income for the FY. */
|
||||
estimatedIncome: number;
|
||||
/** Sum of FD / RD principals tied to the declaration. */
|
||||
depositPrincipal: number;
|
||||
fy: string;
|
||||
status: Form15Status;
|
||||
filedOn?: string;
|
||||
/** "Yes / No" for: has any income tax been assessed for the customer
|
||||
under the IT Act in the past six years? */
|
||||
assessed: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-form-15gh',
|
||||
templateUrl: './form-15gh.component.html',
|
||||
styleUrls: ['../certificates.shared.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
FaIconComponent,
|
||||
MatTooltip
|
||||
]
|
||||
})
|
||||
export class Form15ghComponent implements OnInit {
|
||||
private dialog = inject(MatDialog);
|
||||
private snack = inject(MatSnackBar);
|
||||
|
||||
// ── Filters ──
|
||||
search = '';
|
||||
/** Which form. Empty = both. */
|
||||
kindFilter: '' | Form15Kind = '';
|
||||
statusFilter: '' | Form15Status = '';
|
||||
|
||||
records: Form15ghRecord[] = [];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.records = seedForm15gh();
|
||||
}
|
||||
|
||||
/** Compute age in years from DoB. */
|
||||
age(dob: string): number {
|
||||
if (!dob) return 0;
|
||||
const today = new Date();
|
||||
const birth = new Date(dob);
|
||||
let age = today.getFullYear() - birth.getFullYear();
|
||||
const m = today.getMonth() - birth.getMonth();
|
||||
if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) age--;
|
||||
return age;
|
||||
}
|
||||
|
||||
/** 15H once 60+, 15G otherwise. */
|
||||
formKind(r: Form15ghRecord): Form15Kind {
|
||||
return this.age(r.dob) >= 60 ? '15H' : '15G';
|
||||
}
|
||||
|
||||
get filteredRecords(): Form15ghRecord[] {
|
||||
const q = this.search.trim().toLowerCase();
|
||||
return this.records.filter((r) => {
|
||||
if (this.kindFilter && this.formKind(r) !== this.kindFilter) return false;
|
||||
if (this.statusFilter && r.status !== this.statusFilter) return false;
|
||||
if (!q) return true;
|
||||
return (
|
||||
r.customerName.toLowerCase().includes(q) ||
|
||||
r.customerId.toLowerCase().includes(q) ||
|
||||
r.customerPan.toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
count15G(): number {
|
||||
return this.records.filter((r) => this.formKind(r) === '15G').length;
|
||||
}
|
||||
|
||||
count15H(): number {
|
||||
return this.records.filter((r) => this.formKind(r) === '15H').length;
|
||||
}
|
||||
|
||||
countFiled(): number {
|
||||
return this.records.filter((r) => r.status === 'FILED').length;
|
||||
}
|
||||
|
||||
totalInterest(): number {
|
||||
return this.filteredRecords.reduce((s, r) => s + r.estimatedInterest, 0);
|
||||
}
|
||||
|
||||
clearFilters(): void {
|
||||
this.search = '';
|
||||
this.kindFilter = '';
|
||||
this.statusFilter = '';
|
||||
}
|
||||
|
||||
viewCertificate(r: Form15ghRecord): void {
|
||||
const data: CertificatePreviewDialogData = {
|
||||
kind: 'form15gh',
|
||||
form15gh: { record: r, kind: this.formKind(r), age: this.age(r.dob) }
|
||||
};
|
||||
this.dialog.open(CertificatePreviewDialogComponent, {
|
||||
width: '880px',
|
||||
maxWidth: '96vw',
|
||||
maxHeight: '92vh',
|
||||
autoFocus: false,
|
||||
panelClass: 'cert-preview-dialog',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
downloadCertificate(r: Form15ghRecord): void {
|
||||
downloadForm15ghPdf(r, this.formKind(r));
|
||||
this.snack.open(`Form ${this.formKind(r)} for ${r.customerName} downloaded.`, 'OK', { duration: 2400 });
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────── Mock seed (top-level export) ─────────────────────
|
||||
export function seedForm15gh(): Form15ghRecord[] {
|
||||
return [
|
||||
{
|
||||
customerId: 'CUST112233',
|
||||
customerName: 'Rohan S. Mehta',
|
||||
customerPan: 'ABCPM1234E',
|
||||
customerAddress: 'Flat 902, Skyline Apt, Andheri (E), Mumbai 400069',
|
||||
dob: '1985-02-14',
|
||||
branch: 'Mumbai — Andheri (E)',
|
||||
estimatedInterest: 38500,
|
||||
estimatedIncome: 850000,
|
||||
depositPrincipal: 500000,
|
||||
fy: 'FY 2025-26',
|
||||
status: 'FILED',
|
||||
filedOn: '2025-04-10',
|
||||
assessed: true
|
||||
},
|
||||
{
|
||||
customerId: 'CUST556677',
|
||||
customerName: 'Karthik Rao',
|
||||
customerPan: 'PQRSR2233Z',
|
||||
customerAddress: '24, MG Road, Bengaluru 560001',
|
||||
dob: '1972-09-04',
|
||||
branch: 'Bengaluru — MG Road',
|
||||
estimatedInterest: 52800,
|
||||
estimatedIncome: 320000,
|
||||
depositPrincipal: 700000,
|
||||
fy: 'FY 2025-26',
|
||||
status: 'PENDING',
|
||||
assessed: false
|
||||
},
|
||||
{
|
||||
customerId: 'CUST667788',
|
||||
customerName: 'Lakshmi Naidu',
|
||||
customerPan: 'XYZAN7788Q',
|
||||
customerAddress: 'Plot 14, Road No. 3, Banjara Hills, Hyderabad 500034',
|
||||
dob: '1958-06-22',
|
||||
branch: 'Hyderabad — Banjara Hills',
|
||||
estimatedInterest: 88200,
|
||||
estimatedIncome: 410000,
|
||||
depositPrincipal: 1200000,
|
||||
fy: 'FY 2025-26',
|
||||
status: 'FILED',
|
||||
filedOn: '2025-04-15',
|
||||
assessed: true
|
||||
},
|
||||
{
|
||||
customerId: 'CUST334455',
|
||||
customerName: 'Anita Verma',
|
||||
customerPan: 'EFGHV5678A',
|
||||
customerAddress: 'B-12, Connaught Pl, New Delhi 110001',
|
||||
dob: '1962-11-30',
|
||||
branch: 'New Delhi — Connaught Place',
|
||||
estimatedInterest: 64500,
|
||||
estimatedIncome: 280000,
|
||||
depositPrincipal: 900000,
|
||||
fy: 'FY 2025-26',
|
||||
status: 'FILED',
|
||||
filedOn: '2025-04-05',
|
||||
assessed: false
|
||||
},
|
||||
{
|
||||
customerId: 'CUST990011',
|
||||
customerName: 'Arjun Banerjee',
|
||||
customerPan: 'TUVWB4455F',
|
||||
customerAddress: '42, Park Street, Kolkata 700016',
|
||||
dob: '1991-07-19',
|
||||
branch: 'Kolkata — Park Street',
|
||||
estimatedInterest: 14200,
|
||||
estimatedIncome: 620000,
|
||||
depositPrincipal: 200000,
|
||||
fy: 'FY 2025-26',
|
||||
status: 'PENDING',
|
||||
assessed: true
|
||||
},
|
||||
{
|
||||
customerId: 'CUST330044',
|
||||
customerName: 'Madhavi Singh',
|
||||
customerPan: 'TUVWS3344K',
|
||||
customerAddress: 'D-5, Greater Kailash, New Delhi 110048',
|
||||
dob: '1955-03-08',
|
||||
branch: 'New Delhi — Connaught Place',
|
||||
estimatedInterest: 145000,
|
||||
estimatedIncome: 320000,
|
||||
depositPrincipal: 2000000,
|
||||
fy: 'FY 2025-26',
|
||||
status: 'FILED',
|
||||
filedOn: '2025-04-02',
|
||||
assessed: false
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<!--
|
||||
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="cert-root">
|
||||
<div class="cert-head">
|
||||
<div class="cert-head-l">
|
||||
<fa-icon icon="file-signature" class="cert-head-ic"></fa-icon>
|
||||
<div>
|
||||
<h2 class="cert-head-t">Form 60 Declaration</h2>
|
||||
<div class="cert-head-s">
|
||||
Income Tax Rule 114B — required for any customer transacting above the threshold who does
|
||||
<strong>not</strong> have a PAN on file.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI strip -->
|
||||
<div class="cert-kpis">
|
||||
<div class="cert-kpi cert-kpi--blue">
|
||||
<fa-icon icon="users" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">{{ totalNoPan() | number }}</div>
|
||||
<div class="cert-kpi-l">Customers without PAN</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cert-kpi cert-kpi--green">
|
||||
<fa-icon icon="check-circle" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">{{ totalFiled() | number }}</div>
|
||||
<div class="cert-kpi-l">Form 60 Filed</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cert-kpi cert-kpi--amber">
|
||||
<fa-icon icon="hourglass-half" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">{{ totalPending() | number }}</div>
|
||||
<div class="cert-kpi-l">Filing Pending</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="cert-filters">
|
||||
<div class="cert-filter">
|
||||
<label>Customer / Customer ID / ID No.</label>
|
||||
<div class="cert-input-ic">
|
||||
<fa-icon icon="search"></fa-icon>
|
||||
<input type="text" [(ngModel)]="search" placeholder="e.g. CUST220033 or customer name" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cert-filter">
|
||||
<label>PAN status</label>
|
||||
<select [(ngModel)]="panFilter">
|
||||
<option value="MISSING">Without PAN (Form 60 applicable)</option>
|
||||
<option value="ON_FILE">With PAN on file</option>
|
||||
<option value="">All customers</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="cert-filter">
|
||||
<label>ID proof used</label>
|
||||
<select [(ngModel)]="idProofFilter">
|
||||
<option value="">All ID proofs</option>
|
||||
@for (o of idProofOptions; track o.value) {
|
||||
<option [value]="o.value">{{ o.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@if (search || idProofFilter || panFilter !== 'MISSING') {
|
||||
<button type="button" class="cert-clear" (click)="clearFilters()"><fa-icon icon="times"></fa-icon> Clear</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="cert-card">
|
||||
<div class="cert-table-wrap">
|
||||
<table class="cert-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Customer · ID</th>
|
||||
<th>ID Proof</th>
|
||||
<th>Branch</th>
|
||||
<th>Trigger Transaction</th>
|
||||
<th class="num">Amount</th>
|
||||
<th>PAN Status</th>
|
||||
<th>Form 60</th>
|
||||
<th class="col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of filteredRecords; track r.customerId) {
|
||||
<tr>
|
||||
<td>
|
||||
<div class="cert-cust-name">{{ r.customerName }}</div>
|
||||
<div class="mono small muted">{{ r.customerId }} · DOB {{ r.dob | date: 'd MMM y' }}</div>
|
||||
<div class="small muted">S/o, D/o, W/o: {{ r.fatherName }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="cert-pill cert-pill--type">{{ idProofLabel(r.idProofType) }}</span>
|
||||
<div class="mono small muted">{{ r.idProofNumber }}</div>
|
||||
</td>
|
||||
<td class="small">{{ r.branch }}</td>
|
||||
<td class="small">
|
||||
{{ r.transactionType }}
|
||||
@if (r.transactionDate) {
|
||||
<div class="mono small muted">{{ r.transactionDate | date: 'd MMM y' }}</div>
|
||||
}
|
||||
</td>
|
||||
<td class="num">
|
||||
@if (r.transactionAmount > 0) {
|
||||
₹{{ r.transactionAmount | number: '1.0-0' }}
|
||||
} @else {
|
||||
<span class="muted">—</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (r.hasPan) {
|
||||
<span class="cert-pill cert-pill--green"> <fa-icon icon="check-circle"></fa-icon> PAN on file </span>
|
||||
} @else {
|
||||
<span class="cert-pill cert-pill--amber">
|
||||
<fa-icon icon="exclamation-triangle"></fa-icon> No PAN
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (r.hasPan) {
|
||||
<span class="muted small">N/A</span>
|
||||
} @else if (r.filedOn) {
|
||||
<span class="cert-pill cert-pill--green">Filed · {{ r.filedOn | date: 'd MMM' }}</span>
|
||||
} @else {
|
||||
<span class="cert-pill cert-pill--amber">Pending</span>
|
||||
}
|
||||
</td>
|
||||
<td class="col-actions">
|
||||
<div class="cert-row-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="cert-icon-btn primary"
|
||||
[disabled]="r.hasPan"
|
||||
(click)="viewCertificate(r)"
|
||||
matTooltip="View / Print Form 60"
|
||||
>
|
||||
<fa-icon icon="eye"></fa-icon>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="cert-icon-btn"
|
||||
[disabled]="r.hasPan"
|
||||
(click)="downloadCertificate(r)"
|
||||
matTooltip="Download PDF"
|
||||
>
|
||||
<fa-icon icon="download"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (filteredRecords.length === 0) {
|
||||
<tr>
|
||||
<td colspan="8" class="cert-empty">
|
||||
<fa-icon icon="info-circle"></fa-icon>
|
||||
No customers match the current filters.
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* 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, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
import {
|
||||
CertificatePreviewDialogComponent,
|
||||
CertificatePreviewDialogData
|
||||
} from '../certificate-preview-dialog/certificate-preview-dialog.component';
|
||||
import { downloadForm60Pdf } from '../certificate-pdf';
|
||||
|
||||
export type Form60IdProofType = 'AADHAAR' | 'VOTER_ID' | 'DRIVING_LICENSE' | 'PASSPORT' | 'RATION';
|
||||
|
||||
export interface Form60Record {
|
||||
customerId: string;
|
||||
customerName: string;
|
||||
customerAddress: string;
|
||||
fatherName: string;
|
||||
dob: string;
|
||||
branch: string;
|
||||
/** True only when there's no PAN on file — Form 60 is required. */
|
||||
hasPan: boolean;
|
||||
/** ID document used in lieu of PAN. */
|
||||
idProofType: Form60IdProofType;
|
||||
idProofNumber: string;
|
||||
/** Transaction reason that triggered Form 60 (cash >50k etc.). */
|
||||
transactionType: string;
|
||||
transactionAmount: number;
|
||||
transactionDate: string;
|
||||
estimatedIncome: number;
|
||||
agriIncome: number;
|
||||
filedOn?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-form-60',
|
||||
templateUrl: './form-60.component.html',
|
||||
styleUrls: ['../certificates.shared.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
FaIconComponent,
|
||||
MatTooltip
|
||||
]
|
||||
})
|
||||
export class Form60Component implements OnInit {
|
||||
private dialog = inject(MatDialog);
|
||||
private snack = inject(MatSnackBar);
|
||||
|
||||
// ── Filters ──
|
||||
search = '';
|
||||
idProofFilter: '' | Form60IdProofType = '';
|
||||
/** Default: only show customers without PAN (since Form 60 only applies to them). */
|
||||
panFilter: '' | 'MISSING' | 'ON_FILE' = 'MISSING';
|
||||
|
||||
records: Form60Record[] = [];
|
||||
|
||||
readonly idProofOptions: { value: Form60IdProofType; label: string }[] = [
|
||||
{ value: 'AADHAAR', label: 'Aadhaar Card' },
|
||||
{ value: 'VOTER_ID', label: 'Voter ID' },
|
||||
{ value: 'DRIVING_LICENSE', label: 'Driving License' },
|
||||
{ value: 'PASSPORT', label: 'Passport' },
|
||||
{ value: 'RATION', label: 'Ration Card' }
|
||||
];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.records = seedForm60();
|
||||
}
|
||||
|
||||
get filteredRecords(): Form60Record[] {
|
||||
const q = this.search.trim().toLowerCase();
|
||||
return this.records.filter((r) => {
|
||||
if (this.panFilter === 'MISSING' && r.hasPan) return false;
|
||||
if (this.panFilter === 'ON_FILE' && !r.hasPan) return false;
|
||||
if (this.idProofFilter && r.idProofType !== this.idProofFilter) return false;
|
||||
if (!q) return true;
|
||||
return (
|
||||
r.customerName.toLowerCase().includes(q) ||
|
||||
r.customerId.toLowerCase().includes(q) ||
|
||||
r.idProofNumber.toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
idProofLabel(t: Form60IdProofType): string {
|
||||
return this.idProofOptions.find((o) => o.value === t)?.label || t;
|
||||
}
|
||||
|
||||
totalNoPan(): number {
|
||||
return this.records.filter((r) => !r.hasPan).length;
|
||||
}
|
||||
|
||||
totalFiled(): number {
|
||||
return this.records.filter((r) => !r.hasPan && !!r.filedOn).length;
|
||||
}
|
||||
|
||||
totalPending(): number {
|
||||
return this.records.filter((r) => !r.hasPan && !r.filedOn).length;
|
||||
}
|
||||
|
||||
clearFilters(): void {
|
||||
this.search = '';
|
||||
this.idProofFilter = '';
|
||||
this.panFilter = 'MISSING';
|
||||
}
|
||||
|
||||
viewCertificate(r: Form60Record): void {
|
||||
if (r.hasPan) {
|
||||
this.snack.open(`${r.customerName} already has a PAN on file — Form 60 is not applicable.`, 'OK', {
|
||||
duration: 3200
|
||||
});
|
||||
return;
|
||||
}
|
||||
const data: CertificatePreviewDialogData = { kind: 'form60', form60: r };
|
||||
this.dialog.open(CertificatePreviewDialogComponent, {
|
||||
width: '880px',
|
||||
maxWidth: '96vw',
|
||||
maxHeight: '92vh',
|
||||
autoFocus: false,
|
||||
panelClass: 'cert-preview-dialog',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
downloadCertificate(r: Form60Record): void {
|
||||
if (r.hasPan) {
|
||||
this.snack.open(`${r.customerName} already has a PAN on file — Form 60 is not applicable.`, 'OK', {
|
||||
duration: 3200
|
||||
});
|
||||
return;
|
||||
}
|
||||
downloadForm60Pdf(r);
|
||||
this.snack.open(`Form 60 for ${r.customerName} downloaded.`, 'OK', { duration: 2400 });
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────── Mock seed (top-level export) ─────────────────────
|
||||
export function seedForm60(): Form60Record[] {
|
||||
return [
|
||||
{
|
||||
customerId: 'CUST220033',
|
||||
customerName: 'Bluestone Trading Co. (Prop. Harish Iyer)',
|
||||
customerAddress: '12, Brigade Road, Bengaluru 560025',
|
||||
fatherName: 'K. R. Iyer',
|
||||
dob: '1972-03-18',
|
||||
branch: 'Bengaluru — MG Road',
|
||||
hasPan: false,
|
||||
idProofType: 'AADHAAR',
|
||||
idProofNumber: 'XXXX-XXXX-7891',
|
||||
transactionType: 'Cash deposit > ₹50,000',
|
||||
transactionAmount: 75000,
|
||||
transactionDate: '2025-05-10',
|
||||
estimatedIncome: 850000,
|
||||
agriIncome: 0,
|
||||
filedOn: '2025-05-10'
|
||||
},
|
||||
{
|
||||
customerId: 'CUST120544',
|
||||
customerName: 'Sundar Pillai',
|
||||
customerAddress: '14, Pondy Bazaar, T. Nagar, Chennai 600017',
|
||||
fatherName: 'M. Pillai',
|
||||
dob: '1968-11-04',
|
||||
branch: 'Chennai — T. Nagar',
|
||||
hasPan: false,
|
||||
idProofType: 'VOTER_ID',
|
||||
idProofNumber: 'TNV1234567',
|
||||
transactionType: 'NEFT credit',
|
||||
transactionAmount: 250000,
|
||||
transactionDate: '2025-04-22',
|
||||
estimatedIncome: 320000,
|
||||
agriIncome: 180000
|
||||
},
|
||||
{
|
||||
customerId: 'CUST998812',
|
||||
customerName: 'Kamla Devi',
|
||||
customerAddress: 'Plot 22, Sec 14, Rohini, New Delhi 110085',
|
||||
fatherName: 'Late Hari Lal',
|
||||
dob: '1955-06-12',
|
||||
branch: 'New Delhi — Connaught Place',
|
||||
hasPan: false,
|
||||
idProofType: 'AADHAAR',
|
||||
idProofNumber: 'XXXX-XXXX-4421',
|
||||
transactionType: 'FD booking > ₹50,000',
|
||||
transactionAmount: 200000,
|
||||
transactionDate: '2025-05-02',
|
||||
estimatedIncome: 145000,
|
||||
agriIncome: 0,
|
||||
filedOn: '2025-05-02'
|
||||
},
|
||||
{
|
||||
customerId: 'CUST551177',
|
||||
customerName: 'Ramesh Yadav',
|
||||
customerAddress: 'Hno 122, Saket Nagar, Indore 452018',
|
||||
fatherName: 'Banwari Yadav',
|
||||
dob: '1980-08-25',
|
||||
branch: 'Pune — Koregaon Park',
|
||||
hasPan: false,
|
||||
idProofType: 'DRIVING_LICENSE',
|
||||
idProofNumber: 'MP09 20180012345',
|
||||
transactionType: 'Cash deposit > ₹50,000',
|
||||
transactionAmount: 85000,
|
||||
transactionDate: '2025-05-15',
|
||||
estimatedIncome: 410000,
|
||||
agriIncome: 0
|
||||
},
|
||||
{
|
||||
customerId: 'CUST112233',
|
||||
customerName: 'Rohan S. Mehta',
|
||||
customerAddress: 'Flat 902, Skyline Apt, Andheri (E), Mumbai 400069',
|
||||
fatherName: 'Suresh Mehta',
|
||||
dob: '1985-02-14',
|
||||
branch: 'Mumbai — Andheri (E)',
|
||||
hasPan: true,
|
||||
idProofType: 'AADHAAR',
|
||||
idProofNumber: 'XXXX-XXXX-9090',
|
||||
transactionType: 'PAN on file — Form 60 not applicable',
|
||||
transactionAmount: 0,
|
||||
transactionDate: '',
|
||||
estimatedIncome: 0,
|
||||
agriIncome: 0
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
<!--
|
||||
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="cert-root">
|
||||
<div class="cert-head">
|
||||
<div class="cert-head-l">
|
||||
<fa-icon icon="file-alt" class="cert-head-ic"></fa-icon>
|
||||
<div>
|
||||
<h2 class="cert-head-t">Hypothecation Certificate</h2>
|
||||
<div class="cert-head-s">
|
||||
Lien certificate for any loan where movable assets are hypothecated to the bank — vehicles, plant &
|
||||
machinery, stock, gold, equipment.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI strip -->
|
||||
<div class="cert-kpis">
|
||||
<div class="cert-kpi cert-kpi--blue">
|
||||
<fa-icon icon="file-alt" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">{{ countActive() | number }}</div>
|
||||
<div class="cert-kpi-l">Active Hypothecations</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cert-kpi cert-kpi--green">
|
||||
<fa-icon icon="check-circle" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">{{ countNoc() | number }}</div>
|
||||
<div class="cert-kpi-l">NOCs Issued</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cert-kpi cert-kpi--amber">
|
||||
<fa-icon icon="coins" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">₹{{ totalOutstanding() | number: '1.0-0' }}</div>
|
||||
<div class="cert-kpi-l">Outstanding Principal</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="cert-filters">
|
||||
<div class="cert-filter">
|
||||
<label>Certificate / A/C / Customer / Asset</label>
|
||||
<div class="cert-input-ic">
|
||||
<fa-icon icon="search"></fa-icon>
|
||||
<input type="text" [(ngModel)]="search" placeholder="e.g. HYP-2024-001023, KA-01-MZ-4421 or customer name" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cert-filter">
|
||||
<label>Asset type</label>
|
||||
<select [(ngModel)]="assetFilter">
|
||||
<option value="">All assets</option>
|
||||
@for (o of assetOptions; track o.value) {
|
||||
<option [value]="o.value">{{ o.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="cert-filter">
|
||||
<label>Status</label>
|
||||
<select [(ngModel)]="statusFilter">
|
||||
<option value="ACTIVE">Active</option>
|
||||
<option value="CLOSED">Closed</option>
|
||||
<option value="NOC_ISSUED">NOC Issued</option>
|
||||
<option value="">All statuses</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@if (search || assetFilter || statusFilter !== 'ACTIVE') {
|
||||
<button type="button" class="cert-clear" (click)="clearFilters()"><fa-icon icon="times"></fa-icon> Clear</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="cert-card">
|
||||
<div class="cert-table-wrap">
|
||||
<table class="cert-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Customer · Loan A/C</th>
|
||||
<th>Certificate No.</th>
|
||||
<th>Asset</th>
|
||||
<th>Identifier</th>
|
||||
<th class="num">Sanctioned</th>
|
||||
<th class="num">Outstanding</th>
|
||||
<th>Insurance</th>
|
||||
<th>Status</th>
|
||||
<th class="col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of filteredRecords; track r.certificateNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<div class="cert-cust-name">{{ r.customerName }}</div>
|
||||
<div class="mono small muted">{{ r.accountNo }} · {{ r.customerId }} · PAN {{ r.customerPan }}</div>
|
||||
<div class="small muted">{{ r.loanType }}</div>
|
||||
</td>
|
||||
<td class="mono">{{ r.certificateNo }}</td>
|
||||
<td>
|
||||
<span class="cert-pill cert-pill--type">{{ assetLabel(r.assetType) }}</span>
|
||||
<div class="small muted">{{ r.assetDescription }}</div>
|
||||
</td>
|
||||
<td class="mono small">{{ r.assetIdentifier }}</td>
|
||||
<td class="num">₹{{ r.sanctionedAmount | number: '1.0-0' }}</td>
|
||||
<td class="num">
|
||||
<strong>₹{{ r.outstandingPrincipal | number: '1.0-0' }}</strong>
|
||||
</td>
|
||||
<td class="small">
|
||||
@if (r.insuredValue > 0) {
|
||||
₹{{ r.insuredValue | number: '1.0-0' }}
|
||||
<div class="mono small muted">valid till {{ r.insuranceValidTill | date: 'd MMM y' }}</div>
|
||||
} @else {
|
||||
<span class="muted">—</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (r.status === 'ACTIVE') {
|
||||
<span class="cert-pill cert-pill--blue">Active</span>
|
||||
} @else if (r.status === 'NOC_ISSUED') {
|
||||
<span class="cert-pill cert-pill--green">NOC Issued</span>
|
||||
} @else {
|
||||
<span class="cert-pill">Closed</span>
|
||||
}
|
||||
</td>
|
||||
<td class="col-actions">
|
||||
<div class="cert-row-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="cert-icon-btn primary"
|
||||
(click)="viewCertificate(r)"
|
||||
matTooltip="View / Print certificate"
|
||||
>
|
||||
<fa-icon icon="eye"></fa-icon>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="cert-icon-btn"
|
||||
(click)="downloadCertificate(r)"
|
||||
matTooltip="Download PDF"
|
||||
>
|
||||
<fa-icon icon="download"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (filteredRecords.length === 0) {
|
||||
<tr>
|
||||
<td colspan="9" class="cert-empty">
|
||||
<fa-icon icon="info-circle"></fa-icon>
|
||||
No hypothecation records match the current filters.
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* 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, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
import {
|
||||
CertificatePreviewDialogComponent,
|
||||
CertificatePreviewDialogData
|
||||
} from '../certificate-preview-dialog/certificate-preview-dialog.component';
|
||||
import { downloadHypothecationPdf } from '../certificate-pdf';
|
||||
|
||||
export type HypoAssetType = 'VEHICLE' | 'PLANT_MACHINERY' | 'STOCK_DEBTORS' | 'GOLD' | 'EQUIPMENT';
|
||||
export type HypoStatus = 'ACTIVE' | 'CLOSED' | 'NOC_ISSUED';
|
||||
|
||||
export interface HypothecationRecord {
|
||||
certificateNo: string;
|
||||
accountNo: string;
|
||||
customerId: string;
|
||||
customerName: string;
|
||||
customerPan: string;
|
||||
customerAddress: string;
|
||||
branch: string;
|
||||
loanType: string;
|
||||
sanctionedAmount: number;
|
||||
outstandingPrincipal: number;
|
||||
disbursementDate: string;
|
||||
/** Asset hypothecated to the bank. */
|
||||
assetType: HypoAssetType;
|
||||
/** Free-text asset description (Maruti Swift VDi · 2022 · etc.). */
|
||||
assetDescription: string;
|
||||
/** Vehicle registration, machine serial, etc. */
|
||||
assetIdentifier: string;
|
||||
/** Insured up to ₹ (asset insurance for replacement). */
|
||||
insuredValue: number;
|
||||
insuranceValidTill: string;
|
||||
status: HypoStatus;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-hypothecation',
|
||||
templateUrl: './hypothecation.component.html',
|
||||
styleUrls: ['../certificates.shared.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
FaIconComponent,
|
||||
MatTooltip
|
||||
]
|
||||
})
|
||||
export class HypothecationCertificateComponent implements OnInit {
|
||||
private dialog = inject(MatDialog);
|
||||
private snack = inject(MatSnackBar);
|
||||
|
||||
search = '';
|
||||
assetFilter: '' | HypoAssetType = '';
|
||||
statusFilter: '' | HypoStatus = 'ACTIVE';
|
||||
|
||||
records: HypothecationRecord[] = [];
|
||||
|
||||
readonly assetOptions: { value: HypoAssetType; label: string }[] = [
|
||||
{ value: 'VEHICLE', label: 'Vehicle' },
|
||||
{ value: 'PLANT_MACHINERY', label: 'Plant & Machinery' },
|
||||
{ value: 'STOCK_DEBTORS', label: 'Stock / Debtors' },
|
||||
{ value: 'GOLD', label: 'Gold / Bullion' },
|
||||
{ value: 'EQUIPMENT', label: 'Equipment' }
|
||||
];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.records = seedHypothecations();
|
||||
}
|
||||
|
||||
get filteredRecords(): HypothecationRecord[] {
|
||||
const q = this.search.trim().toLowerCase();
|
||||
return this.records.filter((r) => {
|
||||
if (this.assetFilter && r.assetType !== this.assetFilter) return false;
|
||||
if (this.statusFilter && r.status !== this.statusFilter) return false;
|
||||
if (!q) return true;
|
||||
return (
|
||||
r.certificateNo.toLowerCase().includes(q) ||
|
||||
r.accountNo.toLowerCase().includes(q) ||
|
||||
r.customerName.toLowerCase().includes(q) ||
|
||||
r.assetIdentifier.toLowerCase().includes(q) ||
|
||||
r.assetDescription.toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
assetLabel(t: HypoAssetType): string {
|
||||
return this.assetOptions.find((o) => o.value === t)?.label || t;
|
||||
}
|
||||
|
||||
countActive(): number {
|
||||
return this.records.filter((r) => r.status === 'ACTIVE').length;
|
||||
}
|
||||
|
||||
countNoc(): number {
|
||||
return this.records.filter((r) => r.status === 'NOC_ISSUED').length;
|
||||
}
|
||||
|
||||
totalOutstanding(): number {
|
||||
return this.filteredRecords.reduce((s, r) => s + r.outstandingPrincipal, 0);
|
||||
}
|
||||
|
||||
clearFilters(): void {
|
||||
this.search = '';
|
||||
this.assetFilter = '';
|
||||
this.statusFilter = 'ACTIVE';
|
||||
}
|
||||
|
||||
viewCertificate(r: HypothecationRecord): void {
|
||||
const data: CertificatePreviewDialogData = { kind: 'hypothecation', hypothecation: r };
|
||||
this.dialog.open(CertificatePreviewDialogComponent, {
|
||||
width: '880px',
|
||||
maxWidth: '96vw',
|
||||
maxHeight: '92vh',
|
||||
autoFocus: false,
|
||||
panelClass: 'cert-preview-dialog',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
downloadCertificate(r: HypothecationRecord): void {
|
||||
downloadHypothecationPdf(r);
|
||||
this.snack.open(`Hypothecation Certificate ${r.certificateNo} downloaded.`, 'OK', {
|
||||
duration: 2400
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────── Mock seed (top-level export) ─────────────────────
|
||||
export function seedHypothecations(): HypothecationRecord[] {
|
||||
return [
|
||||
{
|
||||
certificateNo: 'HYP-2024-001023',
|
||||
accountNo: 'LN50200077',
|
||||
customerId: 'CUST556677',
|
||||
customerName: 'Karthik Rao',
|
||||
customerPan: 'PQRSR2233Z',
|
||||
customerAddress: '24, MG Road, Bengaluru 560001',
|
||||
branch: 'Bengaluru — MG Road',
|
||||
loanType: 'Auto Loan',
|
||||
sanctionedAmount: 850000,
|
||||
outstandingPrincipal: 480000,
|
||||
disbursementDate: '2023-01-20',
|
||||
assetType: 'VEHICLE',
|
||||
assetDescription: 'Maruti Suzuki Swift VXi · Manual · Petrol',
|
||||
assetIdentifier: 'KA-01-MZ-4421',
|
||||
insuredValue: 950000,
|
||||
insuranceValidTill: '2026-01-19',
|
||||
status: 'ACTIVE'
|
||||
},
|
||||
{
|
||||
certificateNo: 'HYP-2024-001024',
|
||||
accountNo: 'LN70010023',
|
||||
customerId: 'CUST889900',
|
||||
customerName: 'Vikram Joshi',
|
||||
customerPan: 'LMNOJ9988X',
|
||||
customerAddress: 'Office 3B, Koregaon Park Road, Pune 411001',
|
||||
branch: 'Pune — Koregaon Park',
|
||||
loanType: 'Business Loan',
|
||||
sanctionedAmount: 5500000,
|
||||
outstandingPrincipal: 3950000,
|
||||
disbursementDate: '2020-11-05',
|
||||
assetType: 'PLANT_MACHINERY',
|
||||
assetDescription: 'CNC milling machine · Haas VF-2SS · 2020',
|
||||
assetIdentifier: 'SN-VF2SS-2020-114782',
|
||||
insuredValue: 7200000,
|
||||
insuranceValidTill: '2026-03-30',
|
||||
status: 'ACTIVE'
|
||||
},
|
||||
{
|
||||
certificateNo: 'HYP-2024-001025',
|
||||
accountNo: 'LN50200099',
|
||||
customerId: 'CUST667788',
|
||||
customerName: 'Lakshmi Naidu',
|
||||
customerPan: 'XYZAN7788Q',
|
||||
customerAddress: 'Plot 14, Road No. 3, Banjara Hills, Hyderabad 500034',
|
||||
branch: 'Hyderabad — Banjara Hills',
|
||||
loanType: 'Gold Loan',
|
||||
sanctionedAmount: 450000,
|
||||
outstandingPrincipal: 180000,
|
||||
disbursementDate: '2023-08-18',
|
||||
assetType: 'GOLD',
|
||||
assetDescription: '22 KT Gold Jewellery · Total 92.5 grams',
|
||||
assetIdentifier: 'GL-PKT-7891 · Locker S-22',
|
||||
insuredValue: 580000,
|
||||
insuranceValidTill: '2026-08-17',
|
||||
status: 'ACTIVE'
|
||||
},
|
||||
{
|
||||
certificateNo: 'HYP-2024-001026',
|
||||
accountNo: 'LN20034112',
|
||||
customerId: 'CUST334455',
|
||||
customerName: 'Anita Verma',
|
||||
customerPan: 'EFGHV5678A',
|
||||
customerAddress: 'B-12, Connaught Pl, New Delhi 110001',
|
||||
branch: 'New Delhi — Connaught Place',
|
||||
loanType: 'Personal Loan',
|
||||
sanctionedAmount: 600000,
|
||||
outstandingPrincipal: 0,
|
||||
disbursementDate: '2022-09-10',
|
||||
assetType: 'STOCK_DEBTORS',
|
||||
assetDescription: 'Trade receivables — invoices > 60 days',
|
||||
assetIdentifier: 'POOL-AV-2022-001',
|
||||
insuredValue: 0,
|
||||
insuranceValidTill: '',
|
||||
status: 'NOC_ISSUED'
|
||||
},
|
||||
{
|
||||
certificateNo: 'HYP-2024-001027',
|
||||
accountNo: 'LN50100445',
|
||||
customerId: 'CUST110022',
|
||||
customerName: 'Spectra Components Pvt Ltd',
|
||||
customerPan: 'AABCS1122F',
|
||||
customerAddress: 'Plot 9, MIDC, Andheri (E), Mumbai 400093',
|
||||
branch: 'Mumbai — Andheri (E)',
|
||||
loanType: 'Cash Credit',
|
||||
sanctionedAmount: 2500000,
|
||||
outstandingPrincipal: 1820000,
|
||||
disbursementDate: '2021-07-12',
|
||||
assetType: 'STOCK_DEBTORS',
|
||||
assetDescription: 'Stock-in-trade + book debts (paripassu)',
|
||||
assetIdentifier: 'POOL-SCPL-2021-A',
|
||||
insuredValue: 4500000,
|
||||
insuranceValidTill: '2026-06-30',
|
||||
status: 'ACTIVE'
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<!--
|
||||
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="cert-root">
|
||||
<!-- HEADER -->
|
||||
<div class="cert-head">
|
||||
<div class="cert-head-l">
|
||||
<fa-icon icon="file-invoice" class="cert-head-ic"></fa-icon>
|
||||
<div>
|
||||
<h2 class="cert-head-t">Loan Interest Certificate</h2>
|
||||
<div class="cert-head-s">
|
||||
Generate and download interest-paid certificates per loan account, per financial year.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI STRIP -->
|
||||
<div class="cert-kpis">
|
||||
<div class="cert-kpi cert-kpi--blue">
|
||||
<fa-icon icon="file-invoice" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">{{ filteredLoans.length | number }}</div>
|
||||
<div class="cert-kpi-l">Loan Accounts</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cert-kpi cert-kpi--green">
|
||||
<fa-icon icon="coins" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">₹{{ totalInterestForFy() | number: '1.0-0' }}</div>
|
||||
<div class="cert-kpi-l">Interest Paid ({{ fyFilter }})</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cert-kpi cert-kpi--teal">
|
||||
<fa-icon icon="check-circle" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">₹{{ totalPrincipalForFy() | number: '1.0-0' }}</div>
|
||||
<div class="cert-kpi-l">Principal Paid ({{ fyFilter }})</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FILTERS -->
|
||||
<div class="cert-filters">
|
||||
<div class="cert-filter">
|
||||
<label>Account number / Customer / PAN</label>
|
||||
<div class="cert-input-ic">
|
||||
<fa-icon icon="search"></fa-icon>
|
||||
<input type="text" [(ngModel)]="search" placeholder="e.g. LN10029384, ABCPM1234E or customer name" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cert-filter">
|
||||
<label>Financial year</label>
|
||||
<select [(ngModel)]="fyFilter">
|
||||
@for (f of fyOptions; track f) {
|
||||
<option [value]="f">{{ f }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="cert-filter">
|
||||
<label>Loan type</label>
|
||||
<select [(ngModel)]="loanTypeFilter">
|
||||
<option value="">All types</option>
|
||||
@for (o of loanTypeOptions; track o.value) {
|
||||
<option [value]="o.value">{{ o.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@if (search || loanTypeFilter) {
|
||||
<button type="button" class="cert-clear" (click)="clearFilters()"><fa-icon icon="times"></fa-icon> Clear</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- TABLE -->
|
||||
<div class="cert-card">
|
||||
<div class="cert-table-wrap">
|
||||
<table class="cert-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Customer · Loan A/C</th>
|
||||
<th>Type</th>
|
||||
<th>Branch</th>
|
||||
<th class="num">Sanctioned</th>
|
||||
<th class="num">Outstanding</th>
|
||||
<th class="num">Interest ({{ fyFilter }})</th>
|
||||
<th class="num">Principal ({{ fyFilter }})</th>
|
||||
<th>Rate</th>
|
||||
<th class="col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (l of filteredLoans; track l.accountNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<div class="cert-cust-name">{{ l.customerName }}</div>
|
||||
<div class="mono small muted">{{ l.accountNo }} · {{ l.customerId }} · PAN {{ l.customerPan }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="cert-pill cert-pill--type">{{ loanTypeLabel(l.loanType) }}</span>
|
||||
</td>
|
||||
<td class="small">{{ l.branch }}</td>
|
||||
<td class="num">₹{{ l.sanctionedAmount | number: '1.0-0' }}</td>
|
||||
<td class="num">
|
||||
<strong>₹{{ outstandingPrincipal(l) | number: '1.0-0' }}</strong>
|
||||
</td>
|
||||
<td class="num">
|
||||
<strong class="cert-interest">₹{{ selectedFyData(l)?.interestPaid | number: '1.0-0' }}</strong>
|
||||
</td>
|
||||
<td class="num">₹{{ selectedFyData(l)?.principalPaid | number: '1.0-0' }}</td>
|
||||
<td>
|
||||
<span class="cert-rate-pill">{{ l.interestRate }}% p.a.</span>
|
||||
</td>
|
||||
<td class="col-actions">
|
||||
<div class="cert-row-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="cert-icon-btn primary"
|
||||
(click)="viewCertificate(l)"
|
||||
matTooltip="View / Print certificate"
|
||||
>
|
||||
<fa-icon icon="eye"></fa-icon>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="cert-icon-btn"
|
||||
(click)="downloadCertificate(l)"
|
||||
matTooltip="Download PDF"
|
||||
>
|
||||
<fa-icon icon="download"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (filteredLoans.length === 0) {
|
||||
<tr>
|
||||
<td colspan="9" class="cert-empty">
|
||||
<fa-icon icon="info-circle"></fa-icon>
|
||||
No loan accounts match the current filters.
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,336 @@
|
||||
/**
|
||||
* 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, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
import {
|
||||
CertificatePreviewDialogComponent,
|
||||
CertificatePreviewDialogData
|
||||
} from '../certificate-preview-dialog/certificate-preview-dialog.component';
|
||||
import { downloadLoanInterestPdf } from '../certificate-pdf';
|
||||
|
||||
export type LoanProductType = 'HOUSING' | 'PERSONAL' | 'AUTO' | 'EDUCATION' | 'GOLD' | 'BUSINESS';
|
||||
|
||||
export interface FyInterestBreakup {
|
||||
/** Financial year label e.g. "FY 2024-25". */
|
||||
fy: string;
|
||||
openingBalance: number;
|
||||
closingBalance: number;
|
||||
principalPaid: number;
|
||||
interestPaid: number;
|
||||
}
|
||||
|
||||
export interface LoanInterestRecord {
|
||||
accountNo: string;
|
||||
customerId: string;
|
||||
customerName: string;
|
||||
customerPan: string;
|
||||
customerAddress: string;
|
||||
branch: string;
|
||||
loanType: LoanProductType;
|
||||
sanctionedAmount: number;
|
||||
disbursementDate: string;
|
||||
/** Interest rate p.a. (e.g. 8.65) */
|
||||
interestRate: number;
|
||||
fy: FyInterestBreakup[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-loan-interest-certificate',
|
||||
templateUrl: './loan-interest.component.html',
|
||||
styleUrls: ['../certificates.shared.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
FaIconComponent,
|
||||
MatTooltip
|
||||
]
|
||||
})
|
||||
export class LoanInterestCertificateComponent implements OnInit {
|
||||
private dialog = inject(MatDialog);
|
||||
private snack = inject(MatSnackBar);
|
||||
|
||||
// ── Filters ──
|
||||
search = '';
|
||||
fyFilter = '';
|
||||
loanTypeFilter: '' | LoanProductType = '';
|
||||
|
||||
loans: LoanInterestRecord[] = [];
|
||||
|
||||
/** All financial years present across the dataset (newest first). */
|
||||
fyOptions: string[] = [];
|
||||
|
||||
readonly loanTypeOptions: { value: LoanProductType; label: string }[] = [
|
||||
{ value: 'HOUSING', label: 'Housing Loan' },
|
||||
{ value: 'PERSONAL', label: 'Personal Loan' },
|
||||
{ value: 'AUTO', label: 'Auto Loan' },
|
||||
{ value: 'EDUCATION', label: 'Education Loan' },
|
||||
{ value: 'GOLD', label: 'Gold Loan' },
|
||||
{ value: 'BUSINESS', label: 'Business Loan' }
|
||||
];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loans = seedLoanInterestRecords();
|
||||
this.fyOptions = Array.from(new Set(this.loans.flatMap((l) => l.fy.map((f) => f.fy))))
|
||||
.sort()
|
||||
.reverse();
|
||||
// Default to most-recent FY
|
||||
if (this.fyOptions.length) {
|
||||
this.fyFilter = this.fyOptions[0];
|
||||
}
|
||||
}
|
||||
|
||||
// ── Derived list ──
|
||||
get filteredLoans(): LoanInterestRecord[] {
|
||||
const q = this.search.trim().toLowerCase();
|
||||
return this.loans.filter((l) => {
|
||||
if (this.loanTypeFilter && l.loanType !== this.loanTypeFilter) return false;
|
||||
if (!q) return true;
|
||||
return (
|
||||
l.accountNo.toLowerCase().includes(q) ||
|
||||
l.customerName.toLowerCase().includes(q) ||
|
||||
l.customerId.toLowerCase().includes(q) ||
|
||||
l.customerPan.toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
loanTypeLabel(t: LoanProductType): string {
|
||||
return this.loanTypeOptions.find((o) => o.value === t)?.label || t;
|
||||
}
|
||||
|
||||
selectedFyData(l: LoanInterestRecord): FyInterestBreakup | undefined {
|
||||
return l.fy.find((f) => f.fy === this.fyFilter) || l.fy[0];
|
||||
}
|
||||
|
||||
totalInterestForFy(): number {
|
||||
return this.filteredLoans.reduce((s, l) => s + (this.selectedFyData(l)?.interestPaid || 0), 0);
|
||||
}
|
||||
|
||||
totalPrincipalForFy(): number {
|
||||
return this.filteredLoans.reduce((s, l) => s + (this.selectedFyData(l)?.principalPaid || 0), 0);
|
||||
}
|
||||
|
||||
outstandingPrincipal(l: LoanInterestRecord): number {
|
||||
return this.selectedFyData(l)?.closingBalance ?? 0;
|
||||
}
|
||||
|
||||
// ── Actions ──
|
||||
clearFilters(): void {
|
||||
this.search = '';
|
||||
this.loanTypeFilter = '';
|
||||
if (this.fyOptions.length) this.fyFilter = this.fyOptions[0];
|
||||
}
|
||||
|
||||
viewCertificate(l: LoanInterestRecord): void {
|
||||
const fy = this.selectedFyData(l);
|
||||
if (!fy) {
|
||||
this.snack.open('No interest data on file for the selected financial year.', 'OK', { duration: 2500 });
|
||||
return;
|
||||
}
|
||||
const data: CertificatePreviewDialogData = {
|
||||
kind: 'interest',
|
||||
loanInterest: { record: l, fy }
|
||||
};
|
||||
this.dialog.open(CertificatePreviewDialogComponent, {
|
||||
width: '880px',
|
||||
maxWidth: '96vw',
|
||||
maxHeight: '92vh',
|
||||
autoFocus: false,
|
||||
panelClass: 'cert-preview-dialog',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
downloadCertificate(l: LoanInterestRecord): void {
|
||||
const fy = this.selectedFyData(l);
|
||||
if (!fy) {
|
||||
this.snack.open('No interest data on file for the selected financial year.', 'OK', { duration: 2500 });
|
||||
return;
|
||||
}
|
||||
downloadLoanInterestPdf(l, fy);
|
||||
this.snack.open(`Interest Certificate (${fy.fy}) downloaded.`, 'OK', { duration: 2400 });
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────── Mock seed (top-level export) ─────────────────────
|
||||
export function seedLoanInterestRecords(): LoanInterestRecord[] {
|
||||
return [
|
||||
{
|
||||
accountNo: 'LN10029384',
|
||||
customerId: 'CUST112233',
|
||||
customerName: 'Rohan S. Mehta',
|
||||
customerPan: 'ABCPM1234E',
|
||||
customerAddress: 'Flat 902, Skyline Apt, Andheri (E), Mumbai 400069',
|
||||
branch: 'Mumbai — Andheri (E)',
|
||||
loanType: 'HOUSING',
|
||||
sanctionedAmount: 4500000,
|
||||
disbursementDate: '2021-06-15',
|
||||
interestRate: 8.65,
|
||||
fy: [
|
||||
{
|
||||
fy: 'FY 2024-25',
|
||||
openingBalance: 3620000,
|
||||
closingBalance: 3380000,
|
||||
principalPaid: 240000,
|
||||
interestPaid: 312500
|
||||
},
|
||||
{
|
||||
fy: 'FY 2023-24',
|
||||
openingBalance: 3840000,
|
||||
closingBalance: 3620000,
|
||||
principalPaid: 220000,
|
||||
interestPaid: 328200
|
||||
},
|
||||
{
|
||||
fy: 'FY 2022-23',
|
||||
openingBalance: 4050000,
|
||||
closingBalance: 3840000,
|
||||
principalPaid: 210000,
|
||||
interestPaid: 342800
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
accountNo: 'LN20034112',
|
||||
customerId: 'CUST334455',
|
||||
customerName: 'Anita Verma',
|
||||
customerPan: 'EFGHV5678A',
|
||||
customerAddress: 'B-12, Connaught Pl, New Delhi 110001',
|
||||
branch: 'New Delhi — Connaught Place',
|
||||
loanType: 'PERSONAL',
|
||||
sanctionedAmount: 600000,
|
||||
disbursementDate: '2022-09-10',
|
||||
interestRate: 12.5,
|
||||
fy: [
|
||||
{
|
||||
fy: 'FY 2024-25',
|
||||
openingBalance: 380000,
|
||||
closingBalance: 220000,
|
||||
principalPaid: 160000,
|
||||
interestPaid: 41200
|
||||
},
|
||||
{ fy: 'FY 2023-24', openingBalance: 520000, closingBalance: 380000, principalPaid: 140000, interestPaid: 56800 }
|
||||
]
|
||||
},
|
||||
{
|
||||
accountNo: 'LN50200077',
|
||||
customerId: 'CUST556677',
|
||||
customerName: 'Karthik Rao',
|
||||
customerPan: 'PQRSR2233Z',
|
||||
customerAddress: '24, MG Road, Bengaluru 560001',
|
||||
branch: 'Bengaluru — MG Road',
|
||||
loanType: 'AUTO',
|
||||
sanctionedAmount: 850000,
|
||||
disbursementDate: '2023-01-20',
|
||||
interestRate: 9.4,
|
||||
fy: [
|
||||
{
|
||||
fy: 'FY 2024-25',
|
||||
openingBalance: 620000,
|
||||
closingBalance: 480000,
|
||||
principalPaid: 140000,
|
||||
interestPaid: 58800
|
||||
},
|
||||
{ fy: 'FY 2023-24', openingBalance: 780000, closingBalance: 620000, principalPaid: 160000, interestPaid: 71200 }
|
||||
]
|
||||
},
|
||||
{
|
||||
accountNo: 'LN70010023',
|
||||
customerId: 'CUST889900',
|
||||
customerName: 'Vikram Joshi',
|
||||
customerPan: 'LMNOJ9988X',
|
||||
customerAddress: 'Office 3B, Koregaon Park Road, Pune 411001',
|
||||
branch: 'Pune — Koregaon Park',
|
||||
loanType: 'BUSINESS',
|
||||
sanctionedAmount: 5500000,
|
||||
disbursementDate: '2020-11-05',
|
||||
interestRate: 11.0,
|
||||
fy: [
|
||||
{
|
||||
fy: 'FY 2024-25',
|
||||
openingBalance: 4200000,
|
||||
closingBalance: 3950000,
|
||||
principalPaid: 250000,
|
||||
interestPaid: 464800
|
||||
},
|
||||
{
|
||||
fy: 'FY 2023-24',
|
||||
openingBalance: 4480000,
|
||||
closingBalance: 4200000,
|
||||
principalPaid: 280000,
|
||||
interestPaid: 488600
|
||||
},
|
||||
{
|
||||
fy: 'FY 2022-23',
|
||||
openingBalance: 4760000,
|
||||
closingBalance: 4480000,
|
||||
principalPaid: 280000,
|
||||
interestPaid: 522300
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
accountNo: 'LN88990022',
|
||||
customerId: 'CUST990011',
|
||||
customerName: 'Arjun Banerjee',
|
||||
customerPan: 'TUVWB4455F',
|
||||
customerAddress: '42, Park Street, Kolkata 700016',
|
||||
branch: 'Kolkata — Park Street',
|
||||
loanType: 'EDUCATION',
|
||||
sanctionedAmount: 1200000,
|
||||
disbursementDate: '2022-04-22',
|
||||
interestRate: 9.85,
|
||||
fy: [
|
||||
{
|
||||
fy: 'FY 2024-25',
|
||||
openingBalance: 920000,
|
||||
closingBalance: 790000,
|
||||
principalPaid: 130000,
|
||||
interestPaid: 93500
|
||||
},
|
||||
{
|
||||
fy: 'FY 2023-24',
|
||||
openingBalance: 1080000,
|
||||
closingBalance: 920000,
|
||||
principalPaid: 160000,
|
||||
interestPaid: 102200
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
accountNo: 'LN50200099',
|
||||
customerId: 'CUST667788',
|
||||
customerName: 'Lakshmi Naidu',
|
||||
customerPan: 'XYZAN7788Q',
|
||||
customerAddress: 'Plot 14, Road No. 3, Banjara Hills, Hyderabad 500034',
|
||||
branch: 'Hyderabad — Banjara Hills',
|
||||
loanType: 'GOLD',
|
||||
sanctionedAmount: 450000,
|
||||
disbursementDate: '2023-08-18',
|
||||
interestRate: 10.25,
|
||||
fy: [
|
||||
{
|
||||
fy: 'FY 2024-25',
|
||||
openingBalance: 320000,
|
||||
closingBalance: 180000,
|
||||
principalPaid: 140000,
|
||||
interestPaid: 28400
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
<!--
|
||||
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="cert-root">
|
||||
<div class="cert-head">
|
||||
<div class="cert-head-l">
|
||||
<fa-icon icon="id-card" class="cert-head-ic"></fa-icon>
|
||||
<div>
|
||||
<h2 class="cert-head-t">No-Objection Certificate (NOC)</h2>
|
||||
<div class="cert-head-s">
|
||||
Issue NOCs for closed loan accounts — releases the bank's lien on hypothecated assets (vehicles, property,
|
||||
gold) or confirms unsecured-loan closure.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI strip -->
|
||||
<div class="cert-kpis">
|
||||
<div class="cert-kpi cert-kpi--green">
|
||||
<fa-icon icon="check-circle" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">{{ countIssued() | number }}</div>
|
||||
<div class="cert-kpi-l">NOCs Issued</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cert-kpi cert-kpi--blue">
|
||||
<fa-icon icon="paper-plane" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">{{ countReady() | number }}</div>
|
||||
<div class="cert-kpi-l">Ready to Issue</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cert-kpi cert-kpi--amber">
|
||||
<fa-icon icon="hourglass-half" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">{{ countPending() | number }}</div>
|
||||
<div class="cert-kpi-l">Pending Closure</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="cert-filters">
|
||||
<div class="cert-filter">
|
||||
<label>NOC / A/C / Customer / Asset</label>
|
||||
<div class="cert-input-ic">
|
||||
<fa-icon icon="search"></fa-icon>
|
||||
<input type="text" [(ngModel)]="search" placeholder="e.g. NOC-2024-005012, KA-01-MZ-4421 or customer name" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cert-filter">
|
||||
<label>Loan type</label>
|
||||
<select [(ngModel)]="loanTypeFilter">
|
||||
<option value="">All types</option>
|
||||
@for (o of loanTypeOptions; track o.value) {
|
||||
<option [value]="o.value">{{ o.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="cert-filter">
|
||||
<label>Status</label>
|
||||
<select [(ngModel)]="statusFilter">
|
||||
<option value="">All statuses</option>
|
||||
<option value="ISSUED">Issued</option>
|
||||
<option value="READY_TO_ISSUE">Ready to issue</option>
|
||||
<option value="PENDING">Pending closure</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@if (search || loanTypeFilter || statusFilter) {
|
||||
<button type="button" class="cert-clear" (click)="clearFilters()"><fa-icon icon="times"></fa-icon> Clear</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="cert-card">
|
||||
<div class="cert-table-wrap">
|
||||
<table class="cert-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Customer · Loan A/C</th>
|
||||
<th>NOC No.</th>
|
||||
<th>Loan Type</th>
|
||||
<th>Asset / Identifier</th>
|
||||
<th class="num">Total Paid</th>
|
||||
<th>Closure</th>
|
||||
<th>Status</th>
|
||||
<th class="col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of filteredRecords; track r.nocNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<div class="cert-cust-name">{{ r.customerName }}</div>
|
||||
<div class="mono small muted">{{ r.accountNo }} · {{ r.customerId }} · PAN {{ r.customerPan }}</div>
|
||||
</td>
|
||||
<td class="mono">{{ r.nocNo }}</td>
|
||||
<td>
|
||||
<span class="cert-pill cert-pill--type">{{ loanTypeLabel(r.loanType) }}</span>
|
||||
<div class="small muted">Sanctioned ₹{{ r.sanctionedAmount | number: '1.0-0' }}</div>
|
||||
</td>
|
||||
<td>
|
||||
@if (r.assetType !== 'NONE') {
|
||||
<div>{{ r.assetDescription }}</div>
|
||||
<div class="mono small muted">{{ r.assetIdentifier }}</div>
|
||||
} @else {
|
||||
<span class="muted small">Unsecured · no asset</span>
|
||||
}
|
||||
</td>
|
||||
<td class="num">
|
||||
<strong>₹{{ r.totalPaid | number: '1.0-0' }}</strong>
|
||||
</td>
|
||||
<td class="small">
|
||||
@if (r.closureDate) {
|
||||
{{ r.closureDate | date: 'd MMM y' }}
|
||||
} @else {
|
||||
<span class="muted">—</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (r.status === 'ISSUED') {
|
||||
<span class="cert-pill cert-pill--green"> <fa-icon icon="check-circle"></fa-icon> Issued </span>
|
||||
@if (r.issuedOn) {
|
||||
<div class="mono small muted">{{ r.issuedOn | date: 'd MMM y' }}</div>
|
||||
}
|
||||
} @else if (r.status === 'READY_TO_ISSUE') {
|
||||
<span class="cert-pill cert-pill--blue"> <fa-icon icon="paper-plane"></fa-icon> Ready to issue </span>
|
||||
} @else {
|
||||
<span class="cert-pill cert-pill--amber">
|
||||
<fa-icon icon="hourglass-half"></fa-icon> Pending closure
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td class="col-actions">
|
||||
<div class="cert-row-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="cert-icon-btn primary"
|
||||
[disabled]="r.status === 'PENDING'"
|
||||
(click)="viewCertificate(r)"
|
||||
matTooltip="View / Print NOC"
|
||||
>
|
||||
<fa-icon icon="eye"></fa-icon>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="cert-icon-btn"
|
||||
[disabled]="r.status === 'PENDING'"
|
||||
(click)="downloadCertificate(r)"
|
||||
matTooltip="Download PDF"
|
||||
>
|
||||
<fa-icon icon="download"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (filteredRecords.length === 0) {
|
||||
<tr>
|
||||
<td colspan="8" class="cert-empty">
|
||||
<fa-icon icon="info-circle"></fa-icon>
|
||||
No NOCs match the current filters.
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* 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, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
import {
|
||||
CertificatePreviewDialogComponent,
|
||||
CertificatePreviewDialogData
|
||||
} from '../certificate-preview-dialog/certificate-preview-dialog.component';
|
||||
import { downloadNocPdf } from '../certificate-pdf';
|
||||
|
||||
export type NocLoanType = 'AUTO' | 'HOUSING' | 'PERSONAL' | 'EDUCATION' | 'GOLD' | 'BUSINESS' | 'CC' | 'OD';
|
||||
|
||||
export type NocStatus = 'ISSUED' | 'PENDING' | 'READY_TO_ISSUE';
|
||||
|
||||
export type NocAssetType = 'VEHICLE' | 'PROPERTY' | 'GOLD' | 'EQUIPMENT' | 'STOCK_DEBTORS' | 'NONE';
|
||||
|
||||
export interface NocRecord {
|
||||
nocNo: string;
|
||||
accountNo: string;
|
||||
customerId: string;
|
||||
customerName: string;
|
||||
customerPan: string;
|
||||
customerAddress: string;
|
||||
branch: string;
|
||||
loanType: NocLoanType;
|
||||
sanctionedAmount: number;
|
||||
disbursementDate: string;
|
||||
/** Date the loan was fully closed / settled. */
|
||||
closureDate: string;
|
||||
totalPaid: number;
|
||||
/** Asset whose lien is being released. */
|
||||
assetType: NocAssetType;
|
||||
assetDescription: string;
|
||||
assetIdentifier: string;
|
||||
/** Where to forward the NOC (RTO Mumbai, Sub-Registrar etc.) */
|
||||
forwardTo?: string;
|
||||
status: NocStatus;
|
||||
issuedOn?: string;
|
||||
issuedBy?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-noc',
|
||||
templateUrl: './noc.component.html',
|
||||
styleUrls: ['../certificates.shared.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
FaIconComponent,
|
||||
MatTooltip
|
||||
]
|
||||
})
|
||||
export class NocCertificateComponent implements OnInit {
|
||||
private dialog = inject(MatDialog);
|
||||
private snack = inject(MatSnackBar);
|
||||
|
||||
// ── Filters ──
|
||||
search = '';
|
||||
loanTypeFilter: '' | NocLoanType = '';
|
||||
statusFilter: '' | NocStatus = '';
|
||||
|
||||
records: NocRecord[] = [];
|
||||
|
||||
readonly loanTypeOptions: { value: NocLoanType; label: string }[] = [
|
||||
{ value: 'AUTO', label: 'Auto Loan' },
|
||||
{ value: 'HOUSING', label: 'Housing Loan' },
|
||||
{ value: 'PERSONAL', label: 'Personal Loan' },
|
||||
{ value: 'EDUCATION', label: 'Education Loan' },
|
||||
{ value: 'GOLD', label: 'Gold Loan' },
|
||||
{ value: 'BUSINESS', label: 'Business Loan' },
|
||||
{ value: 'CC', label: 'Cash Credit (CC)' },
|
||||
{ value: 'OD', label: 'Overdraft (OD)' }
|
||||
];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.records = seedNocRecords();
|
||||
}
|
||||
|
||||
// ── Derived list ──
|
||||
get filteredRecords(): NocRecord[] {
|
||||
const q = this.search.trim().toLowerCase();
|
||||
return this.records.filter((r) => {
|
||||
if (this.loanTypeFilter && r.loanType !== this.loanTypeFilter) return false;
|
||||
if (this.statusFilter && r.status !== this.statusFilter) return false;
|
||||
if (!q) return true;
|
||||
return (
|
||||
r.nocNo.toLowerCase().includes(q) ||
|
||||
r.accountNo.toLowerCase().includes(q) ||
|
||||
r.customerName.toLowerCase().includes(q) ||
|
||||
r.customerId.toLowerCase().includes(q) ||
|
||||
r.assetIdentifier.toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
loanTypeLabel(t: NocLoanType): string {
|
||||
return this.loanTypeOptions.find((o) => o.value === t)?.label || t;
|
||||
}
|
||||
|
||||
countIssued(): number {
|
||||
return this.records.filter((r) => r.status === 'ISSUED').length;
|
||||
}
|
||||
|
||||
countReady(): number {
|
||||
return this.records.filter((r) => r.status === 'READY_TO_ISSUE').length;
|
||||
}
|
||||
|
||||
countPending(): number {
|
||||
return this.records.filter((r) => r.status === 'PENDING').length;
|
||||
}
|
||||
|
||||
clearFilters(): void {
|
||||
this.search = '';
|
||||
this.loanTypeFilter = '';
|
||||
this.statusFilter = '';
|
||||
}
|
||||
|
||||
viewCertificate(r: NocRecord): void {
|
||||
if (r.status === 'PENDING') {
|
||||
this.snack.open(
|
||||
`${r.customerName}'s loan ${r.accountNo} is still pending closure — NOC cannot be previewed yet.`,
|
||||
'OK',
|
||||
{ duration: 3500 }
|
||||
);
|
||||
return;
|
||||
}
|
||||
const data: CertificatePreviewDialogData = { kind: 'noc', noc: r };
|
||||
this.dialog.open(CertificatePreviewDialogComponent, {
|
||||
width: '880px',
|
||||
maxWidth: '96vw',
|
||||
maxHeight: '92vh',
|
||||
autoFocus: false,
|
||||
panelClass: 'cert-preview-dialog',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
downloadCertificate(r: NocRecord): void {
|
||||
if (r.status === 'PENDING') {
|
||||
this.snack.open(
|
||||
`${r.customerName}'s loan ${r.accountNo} is still pending closure — NOC cannot be downloaded yet.`,
|
||||
'OK',
|
||||
{ duration: 3500 }
|
||||
);
|
||||
return;
|
||||
}
|
||||
downloadNocPdf(r);
|
||||
this.snack.open(`NOC ${r.nocNo} downloaded.`, 'OK', { duration: 2400 });
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────── Mock seed (top-level export) ─────────────────────
|
||||
export function seedNocRecords(): NocRecord[] {
|
||||
return [
|
||||
{
|
||||
nocNo: 'NOC-2024-005012',
|
||||
accountNo: 'LN50200077',
|
||||
customerId: 'CUST556677',
|
||||
customerName: 'Karthik Rao',
|
||||
customerPan: 'PQRSR2233Z',
|
||||
customerAddress: '24, MG Road, Bengaluru 560001',
|
||||
branch: 'Bengaluru — MG Road',
|
||||
loanType: 'AUTO',
|
||||
sanctionedAmount: 850000,
|
||||
disbursementDate: '2023-01-20',
|
||||
closureDate: '2026-01-15',
|
||||
totalPaid: 988600,
|
||||
assetType: 'VEHICLE',
|
||||
assetDescription: 'Maruti Suzuki Swift VXi · Manual · Petrol',
|
||||
assetIdentifier: 'KA-01-MZ-4421',
|
||||
forwardTo: 'RTO Bengaluru, Koramangala',
|
||||
status: 'ISSUED',
|
||||
issuedOn: '2026-01-18',
|
||||
issuedBy: 'Anita Desai'
|
||||
},
|
||||
{
|
||||
nocNo: 'NOC-2024-005028',
|
||||
accountNo: 'LN20034112',
|
||||
customerId: 'CUST334455',
|
||||
customerName: 'Anita Verma',
|
||||
customerPan: 'EFGHV5678A',
|
||||
customerAddress: 'B-12, Connaught Pl, New Delhi 110001',
|
||||
branch: 'New Delhi — Connaught Place',
|
||||
loanType: 'PERSONAL',
|
||||
sanctionedAmount: 600000,
|
||||
disbursementDate: '2022-09-10',
|
||||
closureDate: '2025-09-09',
|
||||
totalPaid: 698000,
|
||||
assetType: 'NONE',
|
||||
assetDescription: 'Unsecured personal loan',
|
||||
assetIdentifier: '—',
|
||||
status: 'ISSUED',
|
||||
issuedOn: '2025-09-12',
|
||||
issuedBy: 'Sushma Iyer'
|
||||
},
|
||||
{
|
||||
nocNo: 'NOC-2024-005034',
|
||||
accountNo: 'LN50200099',
|
||||
customerId: 'CUST667788',
|
||||
customerName: 'Lakshmi Naidu',
|
||||
customerPan: 'XYZAN7788Q',
|
||||
customerAddress: 'Plot 14, Road No. 3, Banjara Hills, Hyderabad 500034',
|
||||
branch: 'Hyderabad — Banjara Hills',
|
||||
loanType: 'GOLD',
|
||||
sanctionedAmount: 450000,
|
||||
disbursementDate: '2023-08-18',
|
||||
closureDate: '2026-02-28',
|
||||
totalPaid: 502800,
|
||||
assetType: 'GOLD',
|
||||
assetDescription: '22 KT Gold Jewellery · Total 92.5 grams',
|
||||
assetIdentifier: 'GL-PKT-7891 · Locker S-22',
|
||||
forwardTo: 'Branch Locker — return of pledged ornaments',
|
||||
status: 'READY_TO_ISSUE'
|
||||
},
|
||||
{
|
||||
nocNo: 'NOC-2024-005041',
|
||||
accountNo: 'LN88990022',
|
||||
customerId: 'CUST990011',
|
||||
customerName: 'Arjun Banerjee',
|
||||
customerPan: 'TUVWB4455F',
|
||||
customerAddress: '42, Park Street, Kolkata 700016',
|
||||
branch: 'Kolkata — Park Street',
|
||||
loanType: 'EDUCATION',
|
||||
sanctionedAmount: 1200000,
|
||||
disbursementDate: '2022-04-22',
|
||||
closureDate: '2026-03-10',
|
||||
totalPaid: 1389500,
|
||||
assetType: 'NONE',
|
||||
assetDescription: 'Education loan (no asset hypothecated)',
|
||||
assetIdentifier: '—',
|
||||
status: 'READY_TO_ISSUE'
|
||||
},
|
||||
{
|
||||
nocNo: 'NOC-2024-005055',
|
||||
accountNo: 'LN10029384',
|
||||
customerId: 'CUST112233',
|
||||
customerName: 'Rohan S. Mehta',
|
||||
customerPan: 'ABCPM1234E',
|
||||
customerAddress: 'Flat 902, Skyline Apt, Andheri (E), Mumbai 400069',
|
||||
branch: 'Mumbai — Andheri (E)',
|
||||
loanType: 'HOUSING',
|
||||
sanctionedAmount: 4500000,
|
||||
disbursementDate: '2021-06-15',
|
||||
closureDate: '',
|
||||
totalPaid: 1310000,
|
||||
assetType: 'PROPERTY',
|
||||
assetDescription: 'Flat 902, Skyline Apt, Andheri (E), Mumbai 400069',
|
||||
assetIdentifier: 'Reg No. AND/2021/00482',
|
||||
forwardTo: 'Sub-Registrar, Andheri',
|
||||
status: 'PENDING'
|
||||
},
|
||||
{
|
||||
nocNo: 'NOC-2024-005062',
|
||||
accountNo: 'LN70010023',
|
||||
customerId: 'CUST889900',
|
||||
customerName: 'Vikram Joshi',
|
||||
customerPan: 'LMNOJ9988X',
|
||||
customerAddress: 'Office 3B, Koregaon Park Road, Pune 411001',
|
||||
branch: 'Pune — Koregaon Park',
|
||||
loanType: 'BUSINESS',
|
||||
sanctionedAmount: 5500000,
|
||||
disbursementDate: '2020-11-05',
|
||||
closureDate: '',
|
||||
totalPaid: 1820000,
|
||||
assetType: 'EQUIPMENT',
|
||||
assetDescription: 'CNC milling machine · Haas VF-2SS · 2020',
|
||||
assetIdentifier: 'SN-VF2SS-2020-114782',
|
||||
status: 'PENDING'
|
||||
},
|
||||
{
|
||||
nocNo: 'NOC-2024-005078',
|
||||
accountNo: 'LN30049827',
|
||||
customerId: 'CUST771199',
|
||||
customerName: 'Suresh Pillai',
|
||||
customerPan: 'RSTUP4400J',
|
||||
customerAddress: '17, Pondy Bazaar, T. Nagar, Chennai 600017',
|
||||
branch: 'Chennai — T. Nagar',
|
||||
loanType: 'AUTO',
|
||||
sanctionedAmount: 1250000,
|
||||
disbursementDate: '2022-06-30',
|
||||
closureDate: '2025-12-20',
|
||||
totalPaid: 1480200,
|
||||
assetType: 'VEHICLE',
|
||||
assetDescription: 'Honda City ZX · CVT · Petrol',
|
||||
assetIdentifier: 'TN-09-CB-8821',
|
||||
forwardTo: 'RTO Chennai Central',
|
||||
status: 'ISSUED',
|
||||
issuedOn: '2025-12-24',
|
||||
issuedBy: 'Ramesh Krishnan'
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<!--
|
||||
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="cert-root">
|
||||
<!-- HEADER -->
|
||||
<div class="cert-head">
|
||||
<div class="cert-head-l">
|
||||
<fa-icon icon="certificate" class="cert-head-ic"></fa-icon>
|
||||
<div>
|
||||
<h2 class="cert-head-t">Share Certificate</h2>
|
||||
<div class="cert-head-s">
|
||||
Issue, preview and download member share certificates with folio + share-number range.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI STRIP -->
|
||||
<div class="cert-kpis">
|
||||
<div class="cert-kpi cert-kpi--blue">
|
||||
<fa-icon icon="certificate" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">{{ filteredRecords.length | number }}</div>
|
||||
<div class="cert-kpi-l">Certificates</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cert-kpi cert-kpi--teal">
|
||||
<fa-icon icon="cube" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">{{ totalShares() | number }}</div>
|
||||
<div class="cert-kpi-l">Shares Held</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cert-kpi cert-kpi--green">
|
||||
<fa-icon icon="coins" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">₹{{ totalShareValue() | number: '1.0-0' }}</div>
|
||||
<div class="cert-kpi-l">Total Face Value</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FILTERS -->
|
||||
<div class="cert-filters">
|
||||
<div class="cert-filter">
|
||||
<label>Certificate / Folio / Member / PAN</label>
|
||||
<div class="cert-input-ic">
|
||||
<fa-icon icon="search"></fa-icon>
|
||||
<input type="text" [(ngModel)]="search" placeholder="e.g. SC-2024-000123 or member name" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cert-filter">
|
||||
<label>Share class</label>
|
||||
<select [(ngModel)]="classFilter">
|
||||
<option value="">All classes</option>
|
||||
@for (o of classOptions; track o.value) {
|
||||
<option [value]="o.value">{{ o.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="cert-filter">
|
||||
<label>Branch</label>
|
||||
<select [(ngModel)]="branchFilter">
|
||||
<option value="">All branches</option>
|
||||
@for (b of branchOptions; track b) {
|
||||
<option [value]="b">{{ b }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@if (search || classFilter || branchFilter) {
|
||||
<button type="button" class="cert-clear" (click)="clearFilters()"><fa-icon icon="times"></fa-icon> Clear</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- TABLE -->
|
||||
<div class="cert-card">
|
||||
<div class="cert-table-wrap">
|
||||
<table class="cert-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Member · Folio</th>
|
||||
<th>Certificate No.</th>
|
||||
<th>Class</th>
|
||||
<th>Branch</th>
|
||||
<th class="num">Shares</th>
|
||||
<th class="num">Face Value</th>
|
||||
<th class="num">Total</th>
|
||||
<th>Share Nos.</th>
|
||||
<th>Issued</th>
|
||||
<th class="col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of filteredRecords; track r.certificateNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<div class="cert-cust-name">{{ r.memberName }}</div>
|
||||
<div class="mono small muted">
|
||||
{{ r.memberId }} · Folio <span class="cert-folio">{{ r.folioNo }}</span> · PAN {{ r.memberPan }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="mono">{{ r.certificateNo }}</td>
|
||||
<td>
|
||||
<span
|
||||
class="cert-pill"
|
||||
[class.cert-pill--blue]="r.shareClass === 'ORDINARY'"
|
||||
[class.cert-pill--amber]="r.shareClass === 'PREFERENCE'"
|
||||
>
|
||||
{{ classLabel(r.shareClass) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="small">{{ r.branch }}</td>
|
||||
<td class="num">
|
||||
<strong class="cert-shares">{{ r.shareCount | number }}</strong>
|
||||
</td>
|
||||
<td class="num">₹{{ r.faceValue | number: '1.0-0' }}</td>
|
||||
<td class="num">
|
||||
<strong>₹{{ r.shareCount * r.faceValue | number: '1.0-0' }}</strong>
|
||||
</td>
|
||||
<td class="mono small">{{ r.shareNoFrom }}–{{ r.shareNoTo }}</td>
|
||||
<td class="small">{{ r.issueDate | date: 'd MMM y' }}</td>
|
||||
<td class="col-actions">
|
||||
<div class="cert-row-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="cert-icon-btn primary"
|
||||
(click)="viewCertificate(r)"
|
||||
matTooltip="View / Print certificate"
|
||||
>
|
||||
<fa-icon icon="eye"></fa-icon>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="cert-icon-btn"
|
||||
(click)="downloadCertificate(r)"
|
||||
matTooltip="Download PDF"
|
||||
>
|
||||
<fa-icon icon="download"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (filteredRecords.length === 0) {
|
||||
<tr>
|
||||
<td colspan="10" class="cert-empty">
|
||||
<fa-icon icon="info-circle"></fa-icon>
|
||||
No share certificates match the current filters.
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* 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, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
import {
|
||||
CertificatePreviewDialogComponent,
|
||||
CertificatePreviewDialogData
|
||||
} from '../certificate-preview-dialog/certificate-preview-dialog.component';
|
||||
import { downloadShareCertificatePdf } from '../certificate-pdf';
|
||||
|
||||
export type ShareClass = 'ORDINARY' | 'PREFERENCE';
|
||||
|
||||
export interface ShareCertificateRecord {
|
||||
certificateNo: string;
|
||||
folioNo: string;
|
||||
memberId: string;
|
||||
memberName: string;
|
||||
memberPan: string;
|
||||
memberAddress: string;
|
||||
branch: string;
|
||||
joiningDate: string;
|
||||
shareClass: ShareClass;
|
||||
shareCount: number;
|
||||
faceValue: number;
|
||||
issueDate: string;
|
||||
/** Distinct numeric range of share numbers held (e.g. 1001–1100). */
|
||||
shareNoFrom: number;
|
||||
shareNoTo: number;
|
||||
/** Total dividend received since joining. */
|
||||
dividendYtd: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-share-certificate',
|
||||
templateUrl: './share-certificate.component.html',
|
||||
styleUrls: ['../certificates.shared.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
FaIconComponent,
|
||||
MatTooltip
|
||||
]
|
||||
})
|
||||
export class ShareCertificateComponent implements OnInit {
|
||||
private dialog = inject(MatDialog);
|
||||
private snack = inject(MatSnackBar);
|
||||
|
||||
// ── Filters ──
|
||||
search = '';
|
||||
classFilter: '' | ShareClass = '';
|
||||
branchFilter = '';
|
||||
|
||||
records: ShareCertificateRecord[] = [];
|
||||
|
||||
readonly classOptions: { value: ShareClass; label: string }[] = [
|
||||
{ value: 'ORDINARY', label: 'Ordinary' },
|
||||
{ value: 'PREFERENCE', label: 'Preference' }
|
||||
];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.records = seedShareCertificates();
|
||||
}
|
||||
|
||||
// ── Derived list ──
|
||||
get filteredRecords(): ShareCertificateRecord[] {
|
||||
const q = this.search.trim().toLowerCase();
|
||||
const branch = this.branchFilter.trim().toLowerCase();
|
||||
return this.records.filter((r) => {
|
||||
if (this.classFilter && r.shareClass !== this.classFilter) return false;
|
||||
if (branch && !r.branch.toLowerCase().includes(branch)) return false;
|
||||
if (!q) return true;
|
||||
return (
|
||||
r.certificateNo.toLowerCase().includes(q) ||
|
||||
r.folioNo.toLowerCase().includes(q) ||
|
||||
r.memberId.toLowerCase().includes(q) ||
|
||||
r.memberName.toLowerCase().includes(q) ||
|
||||
r.memberPan.toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
get branchOptions(): string[] {
|
||||
return Array.from(new Set(this.records.map((r) => r.branch))).sort();
|
||||
}
|
||||
|
||||
classLabel(c: ShareClass): string {
|
||||
return this.classOptions.find((o) => o.value === c)?.label || c;
|
||||
}
|
||||
|
||||
totalShares(): number {
|
||||
return this.filteredRecords.reduce((s, r) => s + r.shareCount, 0);
|
||||
}
|
||||
|
||||
totalShareValue(): number {
|
||||
return this.filteredRecords.reduce((s, r) => s + r.shareCount * r.faceValue, 0);
|
||||
}
|
||||
|
||||
// ── Actions ──
|
||||
clearFilters(): void {
|
||||
this.search = '';
|
||||
this.classFilter = '';
|
||||
this.branchFilter = '';
|
||||
}
|
||||
|
||||
viewCertificate(r: ShareCertificateRecord): void {
|
||||
const data: CertificatePreviewDialogData = {
|
||||
kind: 'share',
|
||||
shareCertificate: r
|
||||
};
|
||||
this.dialog.open(CertificatePreviewDialogComponent, {
|
||||
width: '880px',
|
||||
maxWidth: '96vw',
|
||||
maxHeight: '92vh',
|
||||
autoFocus: false,
|
||||
panelClass: 'cert-preview-dialog',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
downloadCertificate(r: ShareCertificateRecord): void {
|
||||
downloadShareCertificatePdf(r);
|
||||
this.snack.open(`Share Certificate ${r.certificateNo} downloaded.`, 'OK', { duration: 2400 });
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────── Mock seed (top-level export) ─────────────────────
|
||||
export function seedShareCertificates(): ShareCertificateRecord[] {
|
||||
return [
|
||||
{
|
||||
certificateNo: 'SC-2024-000123',
|
||||
folioNo: 'F-00482',
|
||||
memberId: 'MBR000482',
|
||||
memberName: 'Rohan S. Mehta',
|
||||
memberPan: 'ABCPM1234E',
|
||||
memberAddress: 'Flat 902, Skyline Apt, Andheri (E), Mumbai 400069',
|
||||
branch: 'Mumbai — Andheri (E)',
|
||||
joiningDate: '2018-04-22',
|
||||
shareClass: 'ORDINARY',
|
||||
shareCount: 100,
|
||||
faceValue: 100,
|
||||
issueDate: '2018-04-22',
|
||||
shareNoFrom: 48201,
|
||||
shareNoTo: 48300,
|
||||
dividendYtd: 7200
|
||||
},
|
||||
{
|
||||
certificateNo: 'SC-2024-000124',
|
||||
folioNo: 'F-00713',
|
||||
memberId: 'MBR000713',
|
||||
memberName: 'Anita Verma',
|
||||
memberPan: 'EFGHV5678A',
|
||||
memberAddress: 'B-12, Connaught Pl, New Delhi 110001',
|
||||
branch: 'New Delhi — Connaught Place',
|
||||
joiningDate: '2019-08-11',
|
||||
shareClass: 'ORDINARY',
|
||||
shareCount: 50,
|
||||
faceValue: 100,
|
||||
issueDate: '2019-08-11',
|
||||
shareNoFrom: 71301,
|
||||
shareNoTo: 71350,
|
||||
dividendYtd: 3000
|
||||
},
|
||||
{
|
||||
certificateNo: 'SC-2024-000125',
|
||||
folioNo: 'F-01102',
|
||||
memberId: 'MBR001102',
|
||||
memberName: 'Karthik Rao',
|
||||
memberPan: 'PQRSR2233Z',
|
||||
memberAddress: '24, MG Road, Bengaluru 560001',
|
||||
branch: 'Bengaluru — MG Road',
|
||||
joiningDate: '2020-02-04',
|
||||
shareClass: 'ORDINARY',
|
||||
shareCount: 200,
|
||||
faceValue: 100,
|
||||
issueDate: '2020-02-04',
|
||||
shareNoFrom: 110201,
|
||||
shareNoTo: 110400,
|
||||
dividendYtd: 14400
|
||||
},
|
||||
{
|
||||
certificateNo: 'SC-2024-000126',
|
||||
folioNo: 'F-01277',
|
||||
memberId: 'MBR001277',
|
||||
memberName: 'Madhavi Singh',
|
||||
memberPan: 'TUVWS3344K',
|
||||
memberAddress: 'D-5, Greater Kailash, New Delhi 110048',
|
||||
branch: 'New Delhi — Connaught Place',
|
||||
joiningDate: '2017-11-19',
|
||||
shareClass: 'PREFERENCE',
|
||||
shareCount: 75,
|
||||
faceValue: 1000,
|
||||
issueDate: '2017-11-19',
|
||||
shareNoFrom: 12721,
|
||||
shareNoTo: 12795,
|
||||
dividendYtd: 67500
|
||||
},
|
||||
{
|
||||
certificateNo: 'SC-2024-000127',
|
||||
folioNo: 'F-01498',
|
||||
memberId: 'MBR001498',
|
||||
memberName: 'Vikram Joshi',
|
||||
memberPan: 'LMNOJ9988X',
|
||||
memberAddress: 'Office 3B, Koregaon Park Road, Pune 411001',
|
||||
branch: 'Pune — Koregaon Park',
|
||||
joiningDate: '2016-06-30',
|
||||
shareClass: 'PREFERENCE',
|
||||
shareCount: 250,
|
||||
faceValue: 1000,
|
||||
issueDate: '2016-06-30',
|
||||
shareNoFrom: 14981,
|
||||
shareNoTo: 15230,
|
||||
dividendYtd: 225000
|
||||
},
|
||||
{
|
||||
certificateNo: 'SC-2024-000128',
|
||||
folioNo: 'F-01902',
|
||||
memberId: 'MBR001902',
|
||||
memberName: 'Lakshmi Naidu',
|
||||
memberPan: 'XYZAN7788Q',
|
||||
memberAddress: 'Plot 14, Road No. 3, Banjara Hills, Hyderabad 500034',
|
||||
branch: 'Hyderabad — Banjara Hills',
|
||||
joiningDate: '2021-03-15',
|
||||
shareClass: 'ORDINARY',
|
||||
shareCount: 30,
|
||||
faceValue: 100,
|
||||
issueDate: '2021-03-15',
|
||||
shareNoFrom: 190201,
|
||||
shareNoTo: 190230,
|
||||
dividendYtd: 1800
|
||||
},
|
||||
{
|
||||
certificateNo: 'SC-2024-000129',
|
||||
folioNo: 'F-02011',
|
||||
memberId: 'MBR002011',
|
||||
memberName: 'Arjun Banerjee',
|
||||
memberPan: 'TUVWB4455F',
|
||||
memberAddress: '42, Park Street, Kolkata 700016',
|
||||
branch: 'Kolkata — Park Street',
|
||||
joiningDate: '2021-09-08',
|
||||
shareClass: 'ORDINARY',
|
||||
shareCount: 40,
|
||||
faceValue: 100,
|
||||
issueDate: '2021-09-08',
|
||||
shareNoFrom: 201101,
|
||||
shareNoTo: 201140,
|
||||
dividendYtd: 2400
|
||||
}
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user