Files
cbsnexora/src/app/investment/fd-investment/fd-investment.component.ts
T
nitesh 0832017508 feat(investment,reconciliation): add Export to Excel across Investment, fix export popup
Adds a shared Export to Excel utility (investment-export.ts) and wires it
into every listing/report screen in the Investment module — Dashboard
(break-up + allocation), Reports (all ~20 report shapes), Interest (per
active tab), the FD/Bond/Share Holdings registers, and Masters (per active
master tab). Entry/transaction forms intentionally have no export button.
No sign-off is included here, matching the request to keep that feature
reconciliation-only.

Also fixes the Reconciliation export flow: the Prepared/Checked/Approved
sign-off is now an optional one-off popup shown when "Export to Excel" is
clicked (fields are blank by default and downloading works whether or not
they're filled), replacing the earlier always-visible sign-off card. Also
fixes three reconciliation reports (Sundry Dr/Cr, NEFT/RTGS, UDIR) whose
labels contain "/" — an Excel-forbidden worksheet-name character — which
was silently aborting their export.
2026-08-01 19:20:53 +05:30

526 lines
15 KiB
TypeScript

/**
* 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 } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import {
FD_HOLDINGS,
ALL_TXNS,
PARTY_MASTER,
SUBTYPE_MASTER,
FD_TYPE_OPTIONS,
INT_TYPE_OPTIONS,
LIEN_REASON_MASTER,
InvestmentHolding,
InvestmentTxn,
InterestFreq,
accruedInterest,
daysToMaturity,
maturityAmount,
addPeriod,
todayDDMMYYYY
} from '../investment-data';
import { exportWorkbook } from '../investment-export';
type FdTab = 'holdings' | 'purchase' | 'interest' | 'sell' | 'writeoff' | 'partpayment';
interface FdPurchaseForm {
tranType: 'FRESH' | 'RENEWAL';
oldCertNo: string;
fdType: string;
/** GENERAL uses the period (Y/M/D) fields; DATEWISE lets the user type the maturity date directly. */
matureAmtCalcType: 'GENERAL' | 'DATEWISE';
certNo: string;
certDate: string;
party: string;
subParty: string;
glAcNo: string;
glAcName: string;
subType: string;
receiptAdvice: 'RECEIPT' | 'ADVICE';
transactionDate: string;
depositDate: string;
depositAmount: number | null;
interestFreq: InterestFreq;
interestDate: string;
interestRate: number | null;
periodYear: number | null;
periodMonth: number | null;
periodDays: number | null;
/** Only used when matureAmtCalcType === 'DATEWISE'. */
manualMaturityDate: string;
resolutionNo: string;
crBranchCode: string;
crToAc: string;
crToAcName: string;
intReceivableCode: string;
lien: 'YES' | 'NO';
lienAmt: number | null;
lienReason: string;
classification: string;
collable: 'YES' | 'NO';
}
/**
* FD (Fixed Deposit) treasury investment screen — Holdings register + Purchase / Interest /
* Sell-Premature / Write-off / Part-Payment entry tabs, modelled on the legacy "Investment - FD
* Purchase Entry" form. Dummy in-memory data only; Save appends to the local holdings/txns
* arrays so the demo feels interactive without any backend.
*/
@Component({
selector: 'mifosx-fd-investment',
templateUrl: './fd-investment.component.html',
styleUrls: [
'../../cashier/cashier-shared.scss',
'../investment-shared.scss'
],
standalone: true,
imports: [
CommonModule,
FormsModule
]
})
export class FdInvestmentComponent {
activeTab: FdTab = 'holdings';
asOnDate = todayDDMMYYYY();
holdings: InvestmentHolding[] = [...FD_HOLDINGS];
txns: InvestmentTxn[] = ALL_TXNS.filter((t) => t.instrumentType === 'FD');
readonly partyOptions = PARTY_MASTER;
readonly subTypeOptions = SUBTYPE_MASTER;
readonly fdTypeOptions = FD_TYPE_OPTIONS;
readonly intTypeOptions = INT_TYPE_OPTIONS;
readonly lienReasonOptions = LIEN_REASON_MASTER;
readonly branchOptions = [
'HEAD OFFICE',
'BRANCH 1',
'BRANCH 2'
];
generating = false;
toastMsg = '';
toastVisible = false;
private toastTimer: ReturnType<typeof setTimeout> | undefined;
purchaseForm: FdPurchaseForm = this.blankPurchaseForm();
/** Certificate No typed into the Purchase Entry's Search box (legacy "Search" button). */
searchCertNo = '';
interestCertNo = '';
sellCertNo = '';
sellDate = this.asOnDate;
sellRemark = '';
writeoffCertNo = '';
writeoffReason = '';
partPayCertNo = '';
partPayAmount: number | null = null;
setTab(tab: FdTab): void {
this.activeTab = tab;
}
get activeHoldings(): InvestmentHolding[] {
return this.holdings.filter((h) => h.status === 'ACTIVE');
}
private nextCertNo(): string {
const nums = this.holdings.map((h) => parseInt(h.certNo, 10)).filter((n) => !isNaN(n));
return String((nums.length ? Math.max(...nums) : 15619) + 1);
}
private blankPurchaseForm(): FdPurchaseForm {
return {
tranType: 'FRESH',
oldCertNo: '',
fdType: 'CUMMULATIVE',
matureAmtCalcType: 'GENERAL',
certNo: this.nextCertNo(),
certDate: this.asOnDate,
party: '',
subParty: '',
glAcNo: '320020',
glAcName: 'FIXED DEPO. CO-OPER. BANK MAIN',
subType: 'NON SLR',
receiptAdvice: 'RECEIPT',
transactionDate: this.asOnDate,
depositDate: this.asOnDate,
depositAmount: null,
interestFreq: 'CUMULATIVE',
interestDate: this.asOnDate,
interestRate: null,
periodYear: 2,
periodMonth: 0,
periodDays: 0,
manualMaturityDate: '',
resolutionNo: '',
crBranchCode: 'HEAD OFFICE',
crToAc: '310070',
crToAcName: 'CURRENT A/C CO-OP. BANK MAIN BR.41',
intReceivableCode: '320020',
lien: 'NO',
lienAmt: null,
lienReason: '',
classification: '',
collable: 'NO'
};
}
get purchaseMaturityDate(): string {
const f = this.purchaseForm;
if (f.matureAmtCalcType === 'DATEWISE') {
return f.manualMaturityDate;
}
if (!f.depositDate) {
return '';
}
const base = addPeriod(f.depositDate, f.periodYear || 0, f.periodMonth || 0);
return f.periodDays ? addPeriod(base, 0, 0, f.periodDays) : base;
}
get purchaseMaturityAmount(): number {
const f = this.purchaseForm;
if (!f.depositAmount || !f.interestRate || !this.purchaseMaturityDate) {
return 0;
}
return maturityAmount({
certNo: f.certNo,
instrumentType: 'FD',
scriptCode: '',
scriptName: '',
party: f.party,
subType: f.subType,
faceValue: f.depositAmount,
purchaseRate: 100,
units: 1,
purchaseDate: f.depositDate,
maturityDate: this.purchaseMaturityDate,
interestRate: f.interestRate,
interestFreq: f.interestFreq,
glCode: f.glAcNo,
glName: '',
crToAc: f.crToAc,
brokerCode: '',
status: 'ACTIVE'
});
}
savePurchase(): void {
const f = this.purchaseForm;
if (!f.party || !f.depositAmount || !f.interestRate || !this.purchaseMaturityDate) {
this.toast('Please fill Party, Deposit Amount, Interest Rate and Period.');
return;
}
const holding: InvestmentHolding = {
certNo: f.certNo,
instrumentType: 'FD',
scriptCode: f.interestFreq === 'CUMULATIVE' ? 'FD-CUM' : 'FD-MIP',
scriptName: f.fdType ? `${f.fdType} Fixed Deposit` : 'Fixed Deposit',
party: f.party,
subType: f.subType,
faceValue: f.depositAmount,
purchaseRate: 100,
units: 1,
purchaseDate: f.depositDate,
maturityDate: this.purchaseMaturityDate,
interestRate: f.interestRate,
interestFreq: f.interestFreq,
glCode: f.glAcNo,
glName: f.glAcName,
crToAc: f.crToAc,
brokerCode: '',
status: 'ACTIVE'
};
this.holdings = [
holding,
...this.holdings
];
this.txns = [
{
certNo: holding.certNo,
instrumentType: 'FD',
txnType: 'PURCHASE',
voucherNo: `INV/PUR/FD-${holding.certNo}`,
txnDate: f.transactionDate,
amount: holding.faceValue,
particulars: `${f.tranType === 'RENEWAL' ? 'Renewal' : 'Fresh'} FD Purchase - ${holding.party}`
},
...this.txns
];
this.toast(`FD Certificate No. ${holding.certNo} saved successfully.`);
this.purchaseForm = this.blankPurchaseForm();
this.activeTab = 'holdings';
}
cancelPurchase(): void {
this.purchaseForm = this.blankPurchaseForm();
this.toast('Entry cancelled — form reset.');
}
/** Legacy "Search" button — loads an existing certificate's values back into the form. */
searchPurchase(): void {
const h = this.holdings.find((x) => x.certNo === this.searchCertNo.trim());
if (!h) {
this.toast(`Certificate No. ${this.searchCertNo} not found.`);
return;
}
this.purchaseForm = {
...this.blankPurchaseForm(),
tranType: 'FRESH',
certNo: h.certNo,
certDate: h.purchaseDate,
party: h.party,
subType: h.subType,
glAcNo: h.glCode,
glAcName: h.glName,
transactionDate: h.purchaseDate,
depositDate: h.purchaseDate,
depositAmount: h.faceValue,
interestFreq: h.interestFreq,
interestDate: h.purchaseDate,
interestRate: h.interestRate,
matureAmtCalcType: 'DATEWISE',
manualMaturityDate: h.maturityDate,
crToAc: h.crToAc
};
this.toast(`Certificate ${h.certNo} loaded.`);
}
/** Legacy "Drop" button — removes the loaded certificate from the register. */
dropPurchase(): void {
const certNo = this.purchaseForm.certNo;
const exists = this.holdings.some((h) => h.certNo === certNo);
if (!exists) {
this.toast('Nothing to drop — this certificate is not yet saved.');
return;
}
this.holdings = this.holdings.filter((h) => h.certNo !== certNo);
this.txns = this.txns.filter((t) => t.certNo !== certNo);
this.purchaseForm = this.blankPurchaseForm();
this.toast(`Certificate ${certNo} dropped.`);
}
/** Legacy "Exit" button — closes the entry form and returns to the register. */
exitPurchase(): void {
this.purchaseForm = this.blankPurchaseForm();
this.activeTab = 'holdings';
}
// -------------------- Interest --------------------
get interestHolding(): InvestmentHolding | undefined {
return this.holdings.find((h) => h.certNo === this.interestCertNo);
}
interestAccrued(h: InvestmentHolding): number {
return accruedInterest(h, this.asOnDate);
}
postInterest(): void {
const h = this.interestHolding;
if (!h) {
this.toast('Select a certificate first.');
return;
}
const amt = this.interestAccrued(h);
this.txns = [
{
certNo: h.certNo,
instrumentType: 'FD',
txnType: 'INTEREST',
voucherNo: `INV/INT/FD-${h.certNo}-${this.txns.length + 1}`,
txnDate: this.asOnDate,
amount: amt,
particulars: `Interest posted - ${h.party} (${h.interestFreq})`
},
...this.txns
];
this.toast(`Interest of ₹${amt.toLocaleString('en-IN')} posted for Certificate ${h.certNo}.`);
this.interestCertNo = '';
}
// -------------------- Sell / Premature --------------------
get sellHolding(): InvestmentHolding | undefined {
return this.holdings.find((h) => h.certNo === this.sellCertNo);
}
saveSell(): void {
const h = this.sellHolding;
if (!h || !this.sellDate) {
this.toast('Select a certificate and sell/premature date.');
return;
}
const payout = h.faceValue + accruedInterest(h, this.sellDate);
h.status = 'SOLD';
this.txns = [
{
certNo: h.certNo,
instrumentType: 'FD',
txnType: 'SELL',
voucherNo: `INV/SEL/FD-${h.certNo}`,
txnDate: this.sellDate,
amount: payout,
particulars: `Premature closure - ${h.party}${this.sellRemark ? ' - ' + this.sellRemark : ''}`
},
...this.txns
];
this.toast(`Certificate ${h.certNo} closed. Payout ₹${payout.toLocaleString('en-IN')}.`);
this.sellCertNo = '';
this.sellRemark = '';
this.activeTab = 'holdings';
}
// -------------------- Write-off --------------------
get writeoffHolding(): InvestmentHolding | undefined {
return this.holdings.find((h) => h.certNo === this.writeoffCertNo);
}
saveWriteoff(): void {
const h = this.writeoffHolding;
if (!h || !this.writeoffReason) {
this.toast('Select a certificate and enter a write-off reason.');
return;
}
h.status = 'WRITTEN_OFF';
this.txns = [
{
certNo: h.certNo,
instrumentType: 'FD',
txnType: 'WRITE_OFF',
voucherNo: `INV/WOF/FD-${h.certNo}`,
txnDate: this.asOnDate,
amount: h.faceValue,
particulars: `Written off - ${this.writeoffReason}`
},
...this.txns
];
this.toast(`Certificate ${h.certNo} written off.`);
this.writeoffCertNo = '';
this.writeoffReason = '';
this.activeTab = 'holdings';
}
// -------------------- Part Payment --------------------
get partPayHolding(): InvestmentHolding | undefined {
return this.holdings.find((h) => h.certNo === this.partPayCertNo);
}
savePartPayment(): void {
const h = this.partPayHolding;
if (!h || !this.partPayAmount || this.partPayAmount <= 0 || this.partPayAmount >= h.faceValue) {
this.toast('Select a certificate and enter a valid part-payment amount (less than face value).');
return;
}
h.faceValue -= this.partPayAmount;
this.txns = [
{
certNo: h.certNo,
instrumentType: 'FD',
txnType: 'PART_PAYMENT',
voucherNo: `INV/PP/FD-${h.certNo}-${this.txns.length + 1}`,
txnDate: this.asOnDate,
amount: this.partPayAmount,
particulars: `Part payment withdrawn - ${h.party}, balance ₹${h.faceValue.toLocaleString('en-IN')}`
},
...this.txns
];
this.toast(`Part payment of ₹${this.partPayAmount.toLocaleString('en-IN')} recorded for Certificate ${h.certNo}.`);
this.partPayCertNo = '';
this.partPayAmount = null;
this.activeTab = 'holdings';
}
// -------------------- Helpers --------------------
accrued(h: InvestmentHolding): number {
return accruedInterest(h, this.asOnDate);
}
maturity(h: InvestmentHolding): number {
return maturityAmount(h);
}
daysLeft(h: InvestmentHolding): number {
return daysToMaturity(h, this.asOnDate);
}
statusClass(h: InvestmentHolding): string {
return `inv-badge inv-badge--${h.status.toLowerCase()}`;
}
// -------------------- Export to Excel (Holdings register) --------------------
async exportExcel(): Promise<void> {
this.generating = true;
try {
await exportWorkbook(
[
{
sheetName: 'FD Holdings',
title: 'FD Holdings Register',
subtitle: `As on ${this.asOnDate}`,
headers: [
'Cert No',
'Party',
'Type',
'Face Value',
'Rate',
'Frequency',
'Purchase Date',
'Maturity Date',
'Accrued Interest',
'Maturity Value',
'Status'
],
widths: [
14,
28,
18,
16,
10,
14,
14,
14,
16,
16,
12
],
rows: this.holdings.map((h) => [
h.certNo,
h.party,
h.interestFreq === 'CUMULATIVE' ? 'Cumulative' : 'Payout',
h.faceValue,
h.interestRate,
h.interestFreq,
h.purchaseDate,
h.maturityDate,
this.accrued(h),
this.maturity(h),
h.status
])
}
],
`FD-Holdings-${this.asOnDate}`
);
this.toast('FD Holdings exported.');
} catch (err) {
console.error('FD Holdings export failed', err);
this.toast('Could not generate the workbook — please retry.');
} finally {
this.generating = false;
}
}
private toast(msg: string): void {
this.toastMsg = msg;
this.toastVisible = true;
if (this.toastTimer) {
clearTimeout(this.toastTimer);
}
this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200);
}
}