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.
This commit is contained in:
2026-08-01 19:20:53 +05:30
parent 8ac4010967
commit 0832017508
18 changed files with 1610 additions and 0 deletions
@@ -16,6 +16,11 @@
same holdings/transactions used by the Entry screens.
</p>
</div>
<div class="pg-toolbar">
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="generating" (click)="exportExcel()">
{{ generating ? 'Exporting…' : 'Export to Excel' }}
</button>
</div>
</header>
<div class="cctr-tabs">
@@ -692,3 +697,5 @@
</div>
}
</div>
<div class="inv-toast" [class.show]="toastVisible">{{ toastMsg }}</div>
@@ -27,6 +27,7 @@ import {
daysToMaturity,
todayDDMMYYYY
} from '../investment-data';
import { exportWorkbook, ExportSheetSpec } from '../investment-export';
type ReportGroup = 'balance' | 'ledger' | 'maturity' | 'portfolio';
@@ -50,6 +51,11 @@ export class InvestmentReportsComponent {
activeReport = 'govtSecBalance';
asOnDate = todayDDMMYYYY();
generating = false;
toastMsg = '';
toastVisible = false;
private toastTimer: ReturnType<typeof setTimeout> | undefined;
setGroup(group: ReportGroup, defaultReport: string): void {
this.activeGroup = group;
this.activeReport = defaultReport;
@@ -244,4 +250,364 @@ export class InvestmentReportsComponent {
get portfolioTotalMaturity(): number {
return this.portfolioSummary.reduce((s, r) => s + r.maturityValue, 0);
}
// -------------------- Export to Excel --------------------
private readonly holdingsExportHeaders = [
'Cert No',
'Script / Party',
'Sub Type',
'Units',
'Face Value',
'Purchase Rate',
'Interest Rate',
'Purchase Date',
'Maturity Date',
'Accrued Interest',
'Maturity Value',
'Status'
];
private readonly holdingsExportWidths = [
14,
30,
16,
12,
16,
14,
12,
14,
14,
16,
16,
12
];
private holdingsExportRow(h: InvestmentHolding): (string | number | null)[] {
return [
h.certNo,
h.scriptName || h.party,
h.subType,
h.units,
h.faceValue,
h.purchaseRate,
h.interestRate,
h.purchaseDate,
h.maturityDate,
this.accrued(h),
this.maturity(h),
h.status
];
}
private readonly txnExportHeaders = [
'Date',
'Voucher No',
'Cert No',
'Type',
'Particulars',
'Amount'
];
private readonly txnExportWidths = [
14,
22,
14,
14,
42,
16
];
private txnExportRow(t: InvestmentTxn): (string | number | null)[] {
return [
t.txnDate,
t.voucherNo,
t.certNo,
t.txnType,
t.particulars,
t.amount
];
}
private exportSheets(): ExportSheetSpec[] {
const subtitle = `As on ${this.asOnDate}`;
const one = (
sheetName: string,
title: string,
headers: string[],
widths: number[],
rows: (string | number | null)[][]
): ExportSheetSpec[] => [
{ sheetName, title, subtitle, headers, widths, rows }];
switch (this.activeReport) {
case 'govtSecBalance':
return one(
'Govt Sec Balance',
'Govt Security Balance Report',
this.holdingsExportHeaders,
this.holdingsExportWidths,
this.govtSecHoldings.map((h) => this.holdingsExportRow(h))
);
case 'shareBalance':
return one(
'Share Balance',
'Share Balance Report',
this.holdingsExportHeaders,
this.holdingsExportWidths,
this.shareHoldings.map((h) => this.holdingsExportRow(h))
);
case 'fdrBalance':
return one(
'FDR Balance',
'FDR Balance Report',
this.holdingsExportHeaders,
this.holdingsExportWidths,
this.fdHoldings.map((h) => this.holdingsExportRow(h))
);
case 'bankInvList':
return one(
'Bank Investment List',
'Bank Investment List',
[
'Party',
'Holdings',
'Total Face Value'
],
[
30,
14,
18
],
this.bankInvestmentList.map((r) => [
r.party,
r.count,
r.totalFaceValue
])
);
case 'fdLedger':
return one(
'FD Ledger',
'FD Ledger',
this.txnExportHeaders,
this.txnExportWidths,
this.fdTxns.map((t) => this.txnExportRow(t))
);
case 'bondLedger':
return one(
'Govt-Bond Ledger',
'Govt/Bond Ledger',
this.txnExportHeaders,
this.txnExportWidths,
this.bondTxns.map((t) => this.txnExportRow(t))
);
case 'shareLedger':
return one(
'Share Ledger',
'Share Ledger',
this.txnExportHeaders,
this.txnExportWidths,
this.shareTxns.map((t) => this.txnExportRow(t))
);
case 'amortizeLedger':
return one(
'Amortize Ledger',
'Amortize Ledger',
this.txnExportHeaders,
this.txnExportWidths,
this.amortizeTxns.map((t) => this.txnExportRow(t))
);
case 'dealSlip':
return one(
'Deal Slip',
'Deal Slip Register',
this.txnExportHeaders,
this.txnExportWidths,
this.dealSlips.map((t) => this.txnExportRow(t))
);
case 'intDueRegister':
return one(
'Interest Due Register',
'Interest Due Register (Non-Cumulative Holdings)',
[
...this.holdingsExportHeaders,
'Due Amount'
],
[
...this.holdingsExportWidths,
16
],
this.interestDueRegister.map((h) => [
...this.holdingsExportRow(h),
h.dueAmount
])
);
case 'slrRegister':
return one(
'SLR Bond Register',
'SLR Bond Register',
this.holdingsExportHeaders,
this.holdingsExportWidths,
this.slrBondRegister.map((h) => this.holdingsExportRow(h))
);
case 'pendingReceived':
return [
{
sheetName: 'Received Interest',
title: 'Received Interest / Dividend',
subtitle,
headers: this.txnExportHeaders,
widths: this.txnExportWidths,
rows: this.receivedInterestTxns.map((t) => this.txnExportRow(t))
},
{
sheetName: 'Pending Interest',
title: 'Pending Interest (Not Yet Posted)',
subtitle,
headers: this.holdingsExportHeaders,
widths: this.holdingsExportWidths,
rows: this.pendingInterestHoldings.map((h) => this.holdingsExportRow(h))
}
];
case 'maturedFd':
return one(
'Matured FD',
'List of Matured FD',
this.holdingsExportHeaders,
this.holdingsExportWidths,
this.maturedFdList.map((h) => this.holdingsExportRow(h))
);
case 'maturingList':
return one(
'Maturity List',
'Govt Security / FD Maturity List',
this.holdingsExportHeaders,
this.holdingsExportWidths,
this.maturingList.map((h) => this.holdingsExportRow(h))
);
case 'maturityPattern':
return one(
'Maturity Pattern',
'Maturity Pattern Register',
[
'Bucket',
'Holdings',
'Face Value'
],
[
22,
14,
18
],
this.maturityBuckets.map((b) => [
b.label,
b.count,
b.faceValue
])
);
case 'govtSecList':
return one(
'Govt Security List',
'Govt Security List',
this.holdingsExportHeaders,
this.holdingsExportWidths,
this.govtSecList.map((h) => this.holdingsExportRow(h))
);
case 'fdSecList':
return one(
'FD Security List',
'FD Security List',
this.holdingsExportHeaders,
this.holdingsExportWidths,
this.fdSecList.map((h) => this.holdingsExportRow(h))
);
case 'shareSecList':
return one(
'Share Security List',
'Share Security List',
this.holdingsExportHeaders,
this.holdingsExportWidths,
this.shareSecList.map((h) => this.holdingsExportRow(h))
);
case 'mfList':
return one(
'Mutual Fund List',
'Mutual Fund List',
this.holdingsExportHeaders,
this.holdingsExportWidths,
this.mutualFundList.map((h) => this.holdingsExportRow(h))
);
case 'shiftingReport':
return one(
'Shifting Report',
'Shifting Report — SLR Market Deviation ≥ 1%',
[
'Cert No',
'Script',
'Purchase Rate',
'Market Price',
'Deviation %'
],
[
14,
30,
16,
16,
14
],
this.shiftingCandidates.map((x) => [
x.certNo,
x.scriptName,
x.purchaseRate,
x.marketPrice,
x.deviationPct
])
);
case 'portfolio':
return one(
'Portfolio',
'Investment Portfolio — Consolidated Summary',
[
'Instrument Type',
'Holdings',
'Face Value',
'Accrued Interest',
'Maturity Value'
],
[
24,
12,
18,
18,
18
],
this.portfolioSummary.map((p) => [
p.label,
p.count,
p.faceValue,
p.accrued,
p.maturityValue
])
);
default:
return one('Report', 'Investment Report', this.holdingsExportHeaders, this.holdingsExportWidths, []);
}
}
async exportExcel(): Promise<void> {
this.generating = true;
try {
await exportWorkbook(this.exportSheets(), `Investment-${this.activeReport}-${this.asOnDate}`);
this.toast('Report exported.');
} catch (err) {
console.error('Investment report 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);
}
}