/** * 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 { 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); }