Transaction Details now renders each voucher as a paired Dr/Cr journal entry with a derived contra account and a Balanced status chip, so both sides of a transaction are visible at a glance for reconciliation. Adds a shared cashier-export.ts (mirrors the Investment module's exporter) and an Export to Excel button producing a Summary + Transaction Details workbook, plus a Branch Breakup sheet for Head Office consolidated view.
89 lines
3.2 KiB
TypeScript
89 lines
3.2 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/.
|
|
*/
|
|
|
|
// Shared "Export to Excel" builder for Cashier reports (mirrors ../investment/investment-export.ts)
|
|
// — centralised so the ExcelJS boilerplate (title/meta/header rows, number formatting, safe sheet
|
|
// names, Blob download) is written once and every cashier report's export behaves the same way.
|
|
|
|
import * as ExcelJS from 'exceljs';
|
|
|
|
export interface ExportSheetSpec {
|
|
/** Worksheet tab name — sanitised for Excel's forbidden characters (\ / ? * [ ]) and length. */
|
|
sheetName: string;
|
|
/** Bold title row, merged across all columns. */
|
|
title: string;
|
|
/** Optional italic subtitle row (e.g. scope/date), merged across all columns. */
|
|
subtitle?: string;
|
|
headers: string[];
|
|
rows: (string | number | null)[][];
|
|
widths?: number[];
|
|
}
|
|
|
|
/** Excel worksheet names can't contain \ / ? * [ ] or exceed 31 chars. */
|
|
function safeSheetName(name: string): string {
|
|
return name.replace(/[\\/?*[\]]/g, '-').slice(0, 31);
|
|
}
|
|
|
|
function buildSheet(wb: ExcelJS.Workbook, spec: ExportSheetSpec): void {
|
|
const sheet = wb.addWorksheet(safeSheetName(spec.sheetName));
|
|
sheet.columns = (spec.widths || spec.headers.map(() => 18)).map((w) => ({ width: w }));
|
|
|
|
const title = sheet.addRow([spec.title]);
|
|
sheet.mergeCells(title.number, 1, title.number, spec.headers.length);
|
|
title.font = { bold: true, size: 14, color: { argb: 'FFFFFFFF' } };
|
|
title.alignment = { horizontal: 'center', vertical: 'middle' };
|
|
title.height = 24;
|
|
title.getCell(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF0F4F3C' } };
|
|
|
|
if (spec.subtitle) {
|
|
const meta = sheet.addRow([spec.subtitle]);
|
|
sheet.mergeCells(meta.number, 1, meta.number, spec.headers.length);
|
|
meta.font = { italic: true, size: 10 };
|
|
}
|
|
sheet.addRow([]);
|
|
|
|
const head = sheet.addRow(spec.headers);
|
|
head.font = { bold: true };
|
|
head.alignment = { wrapText: true, horizontal: 'center' };
|
|
head.eachCell((c) => {
|
|
c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE2E8F0' } };
|
|
c.border = { bottom: { style: 'thin' } };
|
|
});
|
|
|
|
for (const r of spec.rows) {
|
|
const row = sheet.addRow(r);
|
|
r.forEach((v, idx) => {
|
|
if (typeof v === 'number') {
|
|
row.getCell(idx + 1).numFmt = '#,##0.00';
|
|
}
|
|
});
|
|
}
|
|
sheet.views = [{ state: 'frozen', ySplit: spec.subtitle ? 4 : 3 }];
|
|
}
|
|
|
|
/** Builds a (possibly multi-sheet) workbook and triggers a browser download. */
|
|
export async function exportWorkbook(sheets: ExportSheetSpec[], fileName: string): Promise<void> {
|
|
const wb = new ExcelJS.Workbook();
|
|
wb.creator = 'CBSNexora';
|
|
wb.created = new Date();
|
|
sheets.forEach((s) => buildSheet(wb, s));
|
|
|
|
const buffer = await wb.xlsx.writeBuffer();
|
|
const blob = new Blob([buffer], {
|
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `${fileName.replace(/[^\w]+/g, '-')}.xlsx`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}
|