@isi-ui7/bos7-shared 0.2.11 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/export.ts CHANGED
@@ -1,11 +1,33 @@
1
- // lib/export.ts — utility export Excel dan PDF untuk halaman laporan
1
+ // lib/export.ts — utility export Excel (styled, via exceljs) dan PDF untuk
2
+ // halaman laporan (DG-02 PM review: export mentah -> format cantik).
2
3
 
3
- import * as XLSX from 'xlsx';
4
+ import ExcelJS from 'exceljs';
4
5
 
5
6
  export interface ExportColumn {
6
7
  field: string;
7
8
  title: string;
8
9
  displayFormat?: string;
10
+ /** Cell alignment. Default: 'right' for displayFormat="currency", else 'left'. */
11
+ align?: 'left' | 'center' | 'right';
12
+ }
13
+
14
+ /** Header block rendered above the column headers (report title/period/applied filters). */
15
+ export interface ExportMeta {
16
+ title?: string;
17
+ /** e.g. "01 Jul 2026 – 24 Jul 2026" or "Per 24 Jul 2026". */
18
+ period?: string;
19
+ /** Applied filter values, e.g. [{label:"Cabang", value:"001 — Pusat"}]. Omitted/empty filters should not be passed in. */
20
+ filters?: Array<{ label: string; value: string }>;
21
+ }
22
+
23
+ /** Values for a highlighted total/subtotal row, keyed by column `field`. Fields absent here render blank in the total row. */
24
+ export type ExportTotals = Record<string, unknown>;
25
+
26
+ export interface ExportOptions {
27
+ meta?: ExportMeta;
28
+ totals?: ExportTotals;
29
+ /** Label shown in the total row's leading cell(s). Default: "Total". */
30
+ totalsLabel?: string;
9
31
  }
10
32
 
11
33
  function cellValue(val: unknown, format?: string): unknown {
@@ -15,20 +37,128 @@ function cellValue(val: unknown, format?: string): unknown {
15
37
  return val;
16
38
  }
17
39
 
18
- export function exportToExcel<T extends Record<string, unknown>>(
40
+ function columnAlign(c: ExportColumn): 'left' | 'center' | 'right' {
41
+ return c.align ?? (c.displayFormat === 'currency' ? 'right' : 'left');
42
+ }
43
+
44
+ const THIN_BORDER: Partial<ExcelJS.Borders> = {
45
+ top: { style: 'thin', color: { argb: 'FFD0D0D0' } },
46
+ left: { style: 'thin', color: { argb: 'FFD0D0D0' } },
47
+ bottom: { style: 'thin', color: { argb: 'FFD0D0D0' } },
48
+ right: { style: 'thin', color: { argb: 'FFD0D0D0' } },
49
+ };
50
+ const HEADER_FILL: ExcelJS.Fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF1F61FE' } };
51
+ const TOTAL_FILL: ExcelJS.Fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE8F0FE' } };
52
+
53
+ export async function exportToExcel<T extends Record<string, unknown>>(
19
54
  data: T[],
20
55
  columns: ExportColumn[],
21
- sheetName: string
22
- ): void {
23
- const headers = columns.map((c) => c.title);
24
- const rows = data.map((row) =>
25
- columns.map((c) => cellValue(row[c.field], c.displayFormat))
26
- );
56
+ sheetName: string,
57
+ options?: ExportOptions,
58
+ ): Promise<void> {
59
+ const wb = new ExcelJS.Workbook();
60
+ const ws = wb.addWorksheet(sheetName.slice(0, 31));
61
+ const colCount = columns.length;
62
+
63
+ let cursorRow = 1;
64
+ const meta = options?.meta;
65
+ if (meta?.title) {
66
+ const row = ws.getRow(cursorRow);
67
+ row.getCell(1).value = meta.title;
68
+ row.getCell(1).font = { bold: true, size: 14 };
69
+ ws.mergeCells(cursorRow, 1, cursorRow, colCount);
70
+ cursorRow += 1;
71
+ }
72
+ if (meta?.period) {
73
+ const row = ws.getRow(cursorRow);
74
+ row.getCell(1).value = `Periode: ${meta.period}`;
75
+ row.getCell(1).font = { italic: true, color: { argb: 'FF525252' } };
76
+ ws.mergeCells(cursorRow, 1, cursorRow, colCount);
77
+ cursorRow += 1;
78
+ }
79
+ if (meta?.filters?.length) {
80
+ const row = ws.getRow(cursorRow);
81
+ row.getCell(1).value = meta.filters.map((f) => `${f.label}: ${f.value}`).join(' · ');
82
+ row.getCell(1).font = { italic: true, size: 10, color: { argb: 'FF525252' } };
83
+ ws.mergeCells(cursorRow, 1, cursorRow, colCount);
84
+ cursorRow += 1;
85
+ }
86
+ if (meta?.title || meta?.period || meta?.filters?.length) {
87
+ cursorRow += 1; // blank spacer row before the column headers
88
+ }
89
+
90
+ const headerRowIndex = cursorRow;
91
+ const headerRow = ws.getRow(headerRowIndex);
92
+ columns.forEach((c, i) => {
93
+ const cell = headerRow.getCell(i + 1);
94
+ cell.value = c.title;
95
+ cell.font = { bold: true, color: { argb: 'FFFFFFFF' } };
96
+ cell.fill = HEADER_FILL;
97
+ cell.border = THIN_BORDER;
98
+ cell.alignment = { horizontal: columnAlign(c), vertical: 'middle' };
99
+ });
100
+ headerRow.height = 20;
101
+ cursorRow += 1;
102
+
103
+ data.forEach((row) => {
104
+ const r = ws.getRow(cursorRow);
105
+ columns.forEach((c, i) => {
106
+ const cell = r.getCell(i + 1);
107
+ cell.value = cellValue(row[c.field], c.displayFormat) as ExcelJS.CellValue;
108
+ cell.border = THIN_BORDER;
109
+ cell.alignment = { horizontal: columnAlign(c), vertical: 'middle' };
110
+ if (c.displayFormat === 'currency') cell.numFmt = '#,##0.00';
111
+ });
112
+ cursorRow += 1;
113
+ });
114
+
115
+ if (options?.totals && Object.keys(options.totals).length > 0) {
116
+ const r = ws.getRow(cursorRow);
117
+ const totalsLabel = options.totalsLabel ?? 'Total';
118
+ let labelWritten = false;
119
+ columns.forEach((c, i) => {
120
+ const cell = r.getCell(i + 1);
121
+ cell.font = { bold: true };
122
+ cell.fill = TOTAL_FILL;
123
+ cell.border = { ...THIN_BORDER, top: { style: 'medium', color: { argb: 'FF1F61FE' } } };
124
+ cell.alignment = { horizontal: columnAlign(c), vertical: 'middle' };
125
+ if (c.field in options.totals!) {
126
+ cell.value = cellValue(options.totals![c.field], c.displayFormat) as ExcelJS.CellValue;
127
+ if (c.displayFormat === 'currency') cell.numFmt = '#,##0.00';
128
+ } else if (!labelWritten) {
129
+ cell.value = totalsLabel;
130
+ labelWritten = true;
131
+ }
132
+ });
133
+ cursorRow += 1;
134
+ }
135
+
136
+ columns.forEach((c, i) => {
137
+ const headerLen = c.title.length;
138
+ const sampleLen = data.slice(0, 50).reduce((max, row) => {
139
+ const v = cellValue(row[c.field], c.displayFormat);
140
+ return Math.max(max, String(v ?? '').length);
141
+ }, 0);
142
+ ws.getColumn(i + 1).width = Math.min(Math.max(headerLen, sampleLen) + 2, 40);
143
+ });
144
+
145
+ // Freeze everything above and including the column header row.
146
+ ws.views = [{ state: 'frozen', ySplit: headerRowIndex }];
147
+
148
+ const buf = await wb.xlsx.writeBuffer();
149
+ const blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
150
+ const url = URL.createObjectURL(blob);
151
+ const a = document.createElement('a');
152
+ a.href = url;
153
+ a.download = `${sheetName}.xlsx`;
154
+ document.body.appendChild(a);
155
+ a.click();
156
+ a.remove();
157
+ URL.revokeObjectURL(url);
158
+ }
27
159
 
28
- const ws = XLSX.utils.aoa_to_sheet([headers, ...rows]);
29
- const wb = XLSX.utils.book_new();
30
- XLSX.utils.book_append_sheet(wb, ws, sheetName.slice(0, 31));
31
- XLSX.writeFile(wb, `${sheetName}.xlsx`);
160
+ function cellText(val: unknown, format?: string): string {
161
+ return String(cellValue(val, format));
32
162
  }
33
163
 
34
164
  export async function exportToPDF<T extends Record<string, unknown>>(
@@ -47,12 +177,7 @@ export async function exportToPDF<T extends Record<string, unknown>>(
47
177
 
48
178
  autoTable(doc, {
49
179
  head: [columns.map((c) => c.title)],
50
- body: data.map((row) =>
51
- columns.map((c) => {
52
- const val = cellValue(row[c.field], c.displayFormat);
53
- return String(val);
54
- })
55
- ),
180
+ body: data.map((row) => columns.map((c) => cellText(row[c.field], c.displayFormat))),
56
181
  startY: 22,
57
182
  styles: { fontSize: 8 },
58
183
  headStyles: { fillColor: [31, 97, 254] }, // Carbon blue
package/src/i18n.tsx CHANGED
@@ -24,6 +24,7 @@ export type BosSharedLabels = {
24
24
  toggleOn: string;
25
25
  toggleOff: string;
26
26
  emptyValue: string;
27
+ showInactive: string;
27
28
 
28
29
  loading: string;
29
30
  saving: string;
@@ -71,6 +72,7 @@ export function useBosSharedI18n(): BosSharedLabels {
71
72
  toggleOn: t("bos7.toggleOn", "On"),
72
73
  toggleOff: t("bos7.toggleOff", "Off"),
73
74
  emptyValue: t("bos7.emptyValue", "—"),
75
+ showInactive: t("bos7.showInactive", "Tampilkan Nonaktif"),
74
76
 
75
77
  loading: t("bos7.loading", "Memuat data..."),
76
78
  saving: t("bos7.saving", "Memproses..."),