sheetra 1.0.0
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/LICENSE +201 -0
- package/README.md +32 -0
- package/dist/index.esm.js +2392 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +2419 -0
- package/dist/index.js.map +1 -0
- package/dist/types/builders/export-builder.d.ts +20 -0
- package/dist/types/builders/section-builder.d.ts +142 -0
- package/dist/types/builders/sheet-builder.d.ts +297 -0
- package/dist/types/core/cell.d.ts +16 -0
- package/dist/types/core/column.d.ts +13 -0
- package/dist/types/core/row.d.ts +20 -0
- package/dist/types/core/styles.d.ts +23 -0
- package/dist/types/core/workbook.d.ts +19 -0
- package/dist/types/core/worksheet.d.ts +32 -0
- package/dist/types/formatters/date-formatter.d.ts +7 -0
- package/dist/types/formatters/index.d.ts +3 -0
- package/dist/types/formatters/number-formatter.d.ts +6 -0
- package/dist/types/formatters/string-formatter.d.ts +310 -0
- package/dist/types/index.d.ts +1730 -0
- package/dist/types/types/cell.types.d.ts +288 -0
- package/dist/types/types/export.types.d.ts +303 -0
- package/dist/types/types/index.d.ts +13 -0
- package/dist/types/types/workbook.types.d.ts +500 -0
- package/dist/types/utils/constants.d.ts +10 -0
- package/dist/types/utils/helpers.d.ts +36 -0
- package/dist/types/utils/validators.d.ts +3 -0
- package/dist/types/writers/csv-writer.d.ts +6 -0
- package/dist/types/writers/excel-writer.d.ts +16 -0
- package/dist/types/writers/index.d.ts +4 -0
- package/dist/types/writers/json-writer.d.ts +6 -0
- package/package.json +88 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2419 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class DateFormatter {
|
|
4
|
+
static format(date, format) {
|
|
5
|
+
const d = this.parseDate(date);
|
|
6
|
+
if (!d)
|
|
7
|
+
return '';
|
|
8
|
+
if (format) {
|
|
9
|
+
return this.formatWithPattern(d, format);
|
|
10
|
+
}
|
|
11
|
+
// Default format: YYYY-MM-DD
|
|
12
|
+
return d.toISOString().split('T')[0];
|
|
13
|
+
}
|
|
14
|
+
static toExcelDate(date) {
|
|
15
|
+
const d = this.parseDate(date);
|
|
16
|
+
if (!d)
|
|
17
|
+
return 0;
|
|
18
|
+
// Excel date starts from 1900-01-01
|
|
19
|
+
const excelEpoch = new Date(1900, 0, 1);
|
|
20
|
+
const diffTime = d.getTime() - excelEpoch.getTime();
|
|
21
|
+
const diffDays = diffTime / (1000 * 60 * 60 * 24);
|
|
22
|
+
// Add 1 because Excel thinks 1900-01-01 is day 1
|
|
23
|
+
// Subtract 1 for the 1900 leap year bug
|
|
24
|
+
return diffDays + 1;
|
|
25
|
+
}
|
|
26
|
+
static parseDate(date) {
|
|
27
|
+
if (date instanceof Date) {
|
|
28
|
+
return date;
|
|
29
|
+
}
|
|
30
|
+
if (typeof date === 'string' || typeof date === 'number') {
|
|
31
|
+
const d = new Date(date);
|
|
32
|
+
if (!isNaN(d.getTime())) {
|
|
33
|
+
return d;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
static formatWithPattern(date, pattern) {
|
|
39
|
+
const map = {
|
|
40
|
+
'YYYY': date.getFullYear().toString(),
|
|
41
|
+
'YY': date.getFullYear().toString().slice(-2),
|
|
42
|
+
'MM': this.pad(date.getMonth() + 1),
|
|
43
|
+
'M': (date.getMonth() + 1).toString(),
|
|
44
|
+
'DD': this.pad(date.getDate()),
|
|
45
|
+
'D': date.getDate().toString(),
|
|
46
|
+
'HH': this.pad(date.getHours()),
|
|
47
|
+
'H': date.getHours().toString(),
|
|
48
|
+
'mm': this.pad(date.getMinutes()),
|
|
49
|
+
'm': date.getMinutes().toString(),
|
|
50
|
+
'ss': this.pad(date.getSeconds()),
|
|
51
|
+
's': date.getSeconds().toString()
|
|
52
|
+
};
|
|
53
|
+
return pattern.replace(/YYYY|YY|MM|M|DD|D|HH|H|mm|m|ss|s/g, match => map[match] || match);
|
|
54
|
+
}
|
|
55
|
+
static pad(num) {
|
|
56
|
+
return num.toString().padStart(2, '0');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
class NumberFormatter {
|
|
61
|
+
static format(value, format) {
|
|
62
|
+
if (value === null || value === undefined)
|
|
63
|
+
return '';
|
|
64
|
+
if (format) {
|
|
65
|
+
return this.formatWithPattern(value, format);
|
|
66
|
+
}
|
|
67
|
+
return value.toString();
|
|
68
|
+
}
|
|
69
|
+
static formatWithPattern(value, pattern) {
|
|
70
|
+
// Handle currency format
|
|
71
|
+
if (pattern.includes('$')) {
|
|
72
|
+
return this.formatCurrency(value, pattern);
|
|
73
|
+
}
|
|
74
|
+
// Handle percentage
|
|
75
|
+
if (pattern.includes('%')) {
|
|
76
|
+
return this.formatPercentage(value, pattern);
|
|
77
|
+
}
|
|
78
|
+
// Handle decimal places
|
|
79
|
+
const decimalMatch = pattern.match(/\.(0+)/);
|
|
80
|
+
if (decimalMatch) {
|
|
81
|
+
const decimals = decimalMatch[1].length;
|
|
82
|
+
return value.toFixed(decimals);
|
|
83
|
+
}
|
|
84
|
+
return value.toString();
|
|
85
|
+
}
|
|
86
|
+
static formatCurrency(value, pattern) {
|
|
87
|
+
const symbol = pattern.includes('$') ? '$' : '';
|
|
88
|
+
const decimals = pattern.includes('.00') ? 2 : 0;
|
|
89
|
+
const formatted = value.toFixed(decimals);
|
|
90
|
+
const parts = formatted.split('.');
|
|
91
|
+
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
92
|
+
return symbol + parts.join('.');
|
|
93
|
+
}
|
|
94
|
+
static formatPercentage(value, pattern) {
|
|
95
|
+
const percentValue = value * 100;
|
|
96
|
+
const decimals = pattern.includes('.00') ? 2 : 0;
|
|
97
|
+
return percentValue.toFixed(decimals) + '%';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
class Cell {
|
|
102
|
+
constructor(value, style) {
|
|
103
|
+
this.value = null;
|
|
104
|
+
this.type = 'string';
|
|
105
|
+
if (value !== undefined) {
|
|
106
|
+
this.setValue(value);
|
|
107
|
+
}
|
|
108
|
+
if (style) {
|
|
109
|
+
this.setStyle(style);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
setValue(value) {
|
|
113
|
+
this.value = value;
|
|
114
|
+
this.inferType();
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
setStyle(style) {
|
|
118
|
+
this.style = style;
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
setFormula(formula) {
|
|
122
|
+
this.formula = formula;
|
|
123
|
+
this.type = 'formula';
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
setType(type) {
|
|
127
|
+
this.type = type;
|
|
128
|
+
return this;
|
|
129
|
+
}
|
|
130
|
+
inferType() {
|
|
131
|
+
if (this.value instanceof Date) {
|
|
132
|
+
this.type = 'date';
|
|
133
|
+
}
|
|
134
|
+
else if (typeof this.value === 'number') {
|
|
135
|
+
this.type = 'number';
|
|
136
|
+
}
|
|
137
|
+
else if (typeof this.value === 'boolean') {
|
|
138
|
+
this.type = 'boolean';
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
this.type = 'string';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
getFormattedValue() {
|
|
145
|
+
var _a, _b, _c, _d, _e, _f;
|
|
146
|
+
if (this.value === null || this.value === undefined) {
|
|
147
|
+
return '';
|
|
148
|
+
}
|
|
149
|
+
switch (this.type) {
|
|
150
|
+
case 'date': {
|
|
151
|
+
const dateFormat = typeof ((_a = this.style) === null || _a === void 0 ? void 0 : _a.numberFormat) === 'string'
|
|
152
|
+
? this.style.numberFormat
|
|
153
|
+
: (_c = (_b = this.style) === null || _b === void 0 ? void 0 : _b.numberFormat) === null || _c === void 0 ? void 0 : _c.format;
|
|
154
|
+
return DateFormatter.format(this.value, dateFormat);
|
|
155
|
+
}
|
|
156
|
+
case 'number': {
|
|
157
|
+
const numberFormat = typeof ((_d = this.style) === null || _d === void 0 ? void 0 : _d.numberFormat) === 'string'
|
|
158
|
+
? this.style.numberFormat
|
|
159
|
+
: (_f = (_e = this.style) === null || _e === void 0 ? void 0 : _e.numberFormat) === null || _f === void 0 ? void 0 : _f.format;
|
|
160
|
+
return NumberFormatter.format(this.value, numberFormat);
|
|
161
|
+
}
|
|
162
|
+
case 'boolean':
|
|
163
|
+
return this.value ? 'TRUE' : 'FALSE';
|
|
164
|
+
case 'formula':
|
|
165
|
+
return this.formula || '';
|
|
166
|
+
default:
|
|
167
|
+
return String(this.value);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
toData() {
|
|
171
|
+
return {
|
|
172
|
+
value: this.value,
|
|
173
|
+
style: this.style,
|
|
174
|
+
formula: this.formula,
|
|
175
|
+
type: this.type
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
static fromData(data) {
|
|
179
|
+
const cell = new Cell();
|
|
180
|
+
cell.value = data.value;
|
|
181
|
+
cell.style = data.style;
|
|
182
|
+
cell.formula = data.formula;
|
|
183
|
+
const validTypes = ['string', 'number', 'boolean', 'date', 'formula'];
|
|
184
|
+
cell.type = validTypes.includes(data.type) ? data.type : 'string';
|
|
185
|
+
if (cell.value && !cell.type) {
|
|
186
|
+
cell.inferType();
|
|
187
|
+
}
|
|
188
|
+
return cell;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
class Row {
|
|
193
|
+
constructor(cells) {
|
|
194
|
+
this.cells = [];
|
|
195
|
+
this.hidden = false;
|
|
196
|
+
this.outlineLevel = 0;
|
|
197
|
+
this.collapsed = false;
|
|
198
|
+
if (cells) {
|
|
199
|
+
this.cells = cells;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
addCell(cell) {
|
|
203
|
+
this.cells.push(cell);
|
|
204
|
+
return this;
|
|
205
|
+
}
|
|
206
|
+
createCell(value, style) {
|
|
207
|
+
const cell = new Cell(value, style);
|
|
208
|
+
this.cells.push(cell);
|
|
209
|
+
return cell;
|
|
210
|
+
}
|
|
211
|
+
setCell(index, value, style) {
|
|
212
|
+
while (this.cells.length <= index) {
|
|
213
|
+
this.cells.push(new Cell());
|
|
214
|
+
}
|
|
215
|
+
this.cells[index].setValue(value).setStyle(style);
|
|
216
|
+
return this;
|
|
217
|
+
}
|
|
218
|
+
getCell(index) {
|
|
219
|
+
var _a;
|
|
220
|
+
return (_a = this.cells[index]) === null || _a === void 0 ? void 0 : _a.toData();
|
|
221
|
+
}
|
|
222
|
+
getCells() {
|
|
223
|
+
return this.cells;
|
|
224
|
+
}
|
|
225
|
+
setHeight(height) {
|
|
226
|
+
this.height = height;
|
|
227
|
+
return this;
|
|
228
|
+
}
|
|
229
|
+
setHidden(hidden) {
|
|
230
|
+
this.hidden = hidden;
|
|
231
|
+
return this;
|
|
232
|
+
}
|
|
233
|
+
setOutlineLevel(level, collapsed = false) {
|
|
234
|
+
this.outlineLevel = level;
|
|
235
|
+
this.collapsed = collapsed;
|
|
236
|
+
return this;
|
|
237
|
+
}
|
|
238
|
+
toData() {
|
|
239
|
+
return {
|
|
240
|
+
cells: this.cells.map(cell => cell.toData()),
|
|
241
|
+
height: this.height,
|
|
242
|
+
hidden: this.hidden,
|
|
243
|
+
outlineLevel: this.outlineLevel,
|
|
244
|
+
collapsed: this.collapsed
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
static fromData(data) {
|
|
248
|
+
const row = new Row(data.cells.map(cellData => Cell.fromData(cellData)));
|
|
249
|
+
row.height = data.height;
|
|
250
|
+
row.hidden = data.hidden || false;
|
|
251
|
+
row.outlineLevel = data.outlineLevel || 0;
|
|
252
|
+
row.collapsed = data.collapsed || false;
|
|
253
|
+
return row;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
class Column {
|
|
258
|
+
constructor(width) {
|
|
259
|
+
this.hidden = false;
|
|
260
|
+
this.outlineLevel = 0;
|
|
261
|
+
this.collapsed = false;
|
|
262
|
+
this.width = width;
|
|
263
|
+
}
|
|
264
|
+
setWidth(width) {
|
|
265
|
+
this.width = width;
|
|
266
|
+
return this;
|
|
267
|
+
}
|
|
268
|
+
setHidden(hidden) {
|
|
269
|
+
this.hidden = hidden;
|
|
270
|
+
return this;
|
|
271
|
+
}
|
|
272
|
+
setOutlineLevel(level, collapsed = false) {
|
|
273
|
+
this.outlineLevel = level;
|
|
274
|
+
this.collapsed = collapsed;
|
|
275
|
+
return this;
|
|
276
|
+
}
|
|
277
|
+
toData() {
|
|
278
|
+
return {
|
|
279
|
+
width: this.width,
|
|
280
|
+
hidden: this.hidden,
|
|
281
|
+
outlineLevel: this.outlineLevel,
|
|
282
|
+
collapsed: this.collapsed
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
static fromData(data) {
|
|
286
|
+
const column = new Column(data.width);
|
|
287
|
+
column.hidden = data.hidden || false;
|
|
288
|
+
column.outlineLevel = data.outlineLevel || 0;
|
|
289
|
+
column.collapsed = data.collapsed || false;
|
|
290
|
+
return column;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
class Worksheet {
|
|
295
|
+
constructor(name) {
|
|
296
|
+
this.rows = [];
|
|
297
|
+
this.columns = [];
|
|
298
|
+
this.mergedCells = [];
|
|
299
|
+
this.name = name;
|
|
300
|
+
}
|
|
301
|
+
getName() {
|
|
302
|
+
return this.name;
|
|
303
|
+
}
|
|
304
|
+
setName(name) {
|
|
305
|
+
this.name = name;
|
|
306
|
+
return this;
|
|
307
|
+
}
|
|
308
|
+
addRow(row) {
|
|
309
|
+
this.rows.push(row);
|
|
310
|
+
return this;
|
|
311
|
+
}
|
|
312
|
+
createRow(index) {
|
|
313
|
+
const row = new Row();
|
|
314
|
+
if (index !== undefined) {
|
|
315
|
+
this.rows.splice(index, 0, row);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
this.rows.push(row);
|
|
319
|
+
}
|
|
320
|
+
return row;
|
|
321
|
+
}
|
|
322
|
+
getRow(index) {
|
|
323
|
+
return this.rows[index];
|
|
324
|
+
}
|
|
325
|
+
getRows() {
|
|
326
|
+
return this.rows;
|
|
327
|
+
}
|
|
328
|
+
removeRow(index) {
|
|
329
|
+
if (index >= 0 && index < this.rows.length) {
|
|
330
|
+
this.rows.splice(index, 1);
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
addColumn(column) {
|
|
336
|
+
this.columns.push(column);
|
|
337
|
+
return this;
|
|
338
|
+
}
|
|
339
|
+
createColumn(width) {
|
|
340
|
+
const column = new Column(width);
|
|
341
|
+
this.columns.push(column);
|
|
342
|
+
return column;
|
|
343
|
+
}
|
|
344
|
+
getColumn(index) {
|
|
345
|
+
return this.columns[index];
|
|
346
|
+
}
|
|
347
|
+
getColumns() {
|
|
348
|
+
return this.columns;
|
|
349
|
+
}
|
|
350
|
+
setCell(row, col, value, style) {
|
|
351
|
+
while (this.rows.length <= row) {
|
|
352
|
+
this.rows.push(new Row());
|
|
353
|
+
}
|
|
354
|
+
this.rows[row].setCell(col, value, style);
|
|
355
|
+
return this;
|
|
356
|
+
}
|
|
357
|
+
getCell(row, col) {
|
|
358
|
+
const rowObj = this.rows[row];
|
|
359
|
+
return rowObj === null || rowObj === void 0 ? void 0 : rowObj.getCell(col);
|
|
360
|
+
}
|
|
361
|
+
mergeCells(startRow, startCol, endRow, endCol) {
|
|
362
|
+
this.mergedCells.push({ startRow, startCol, endRow, endCol });
|
|
363
|
+
return this;
|
|
364
|
+
}
|
|
365
|
+
setFreezePane(row, col) {
|
|
366
|
+
this.freezePane = { rows: row, columns: col };
|
|
367
|
+
return this;
|
|
368
|
+
}
|
|
369
|
+
setPrintOptions(options) {
|
|
370
|
+
this.printOptions = options;
|
|
371
|
+
return this;
|
|
372
|
+
}
|
|
373
|
+
setOutlineLevel(row, level, collapsed = false) {
|
|
374
|
+
if (this.rows[row]) {
|
|
375
|
+
this.rows[row].setOutlineLevel(level, collapsed);
|
|
376
|
+
}
|
|
377
|
+
return this;
|
|
378
|
+
}
|
|
379
|
+
setColumnOutlineLevel(col, level, collapsed = false) {
|
|
380
|
+
while (this.columns.length <= col) {
|
|
381
|
+
this.columns.push(new Column());
|
|
382
|
+
}
|
|
383
|
+
this.columns[col].setOutlineLevel(level, collapsed);
|
|
384
|
+
return this;
|
|
385
|
+
}
|
|
386
|
+
toData() {
|
|
387
|
+
return {
|
|
388
|
+
name: this.name,
|
|
389
|
+
rows: this.rows.map(row => row.toData()),
|
|
390
|
+
columns: this.columns.map(col => col.toData()),
|
|
391
|
+
mergeCells: this.mergedCells,
|
|
392
|
+
freezePane: this.freezePane,
|
|
393
|
+
printOptions: this.printOptions
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
static fromData(data) {
|
|
397
|
+
const sheet = new Worksheet(data.name);
|
|
398
|
+
sheet.rows = data.rows.map(rowData => Row.fromData(rowData));
|
|
399
|
+
sheet.columns = (data.columns || []).map(colData => Column.fromData(colData));
|
|
400
|
+
sheet.mergedCells = data.mergeCells || [];
|
|
401
|
+
sheet.freezePane = data.freezePane;
|
|
402
|
+
sheet.printOptions = data.printOptions;
|
|
403
|
+
return sheet;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
class CSVWriter {
|
|
408
|
+
static async write(workbook, _options) {
|
|
409
|
+
const sheets = workbook['sheets'];
|
|
410
|
+
const csvData = this.generateCSVData(sheets[0]); // Use first sheet for CSV
|
|
411
|
+
return new Blob([csvData], { type: 'text/csv;charset=utf-8;' });
|
|
412
|
+
}
|
|
413
|
+
static generateCSVData(sheet) {
|
|
414
|
+
const rows = sheet.getRows();
|
|
415
|
+
const csvRows = [];
|
|
416
|
+
rows.forEach((row) => {
|
|
417
|
+
const rowData = [];
|
|
418
|
+
row.getCells().forEach(cell => {
|
|
419
|
+
const cellData = cell.toData();
|
|
420
|
+
let value = '';
|
|
421
|
+
if (cellData.value !== null && cellData.value !== undefined) {
|
|
422
|
+
if (cellData.type === 'date' && cellData.value instanceof Date) {
|
|
423
|
+
value = cellData.value.toISOString().split('T')[0];
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
value = String(cellData.value);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// Escape quotes and wrap in quotes if contains comma or quote
|
|
430
|
+
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
|
|
431
|
+
value = '"' + value.replace(/"/g, '""') + '"';
|
|
432
|
+
}
|
|
433
|
+
rowData.push(value);
|
|
434
|
+
});
|
|
435
|
+
csvRows.push(rowData.join(','));
|
|
436
|
+
});
|
|
437
|
+
return csvRows.join('\n');
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
class JSONWriter {
|
|
442
|
+
static async write(workbook, _options) {
|
|
443
|
+
const sheets = workbook['sheets'];
|
|
444
|
+
const jsonData = this.generateJSONData(sheets[0]); // Use first sheet for JSON
|
|
445
|
+
return new Blob([JSON.stringify(jsonData, null, 2)], {
|
|
446
|
+
type: 'application/json;charset=utf-8;'
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
static generateJSONData(sheet) {
|
|
450
|
+
const rows = sheet.getRows();
|
|
451
|
+
if (rows.length === 0)
|
|
452
|
+
return [];
|
|
453
|
+
// Assume first row contains headers
|
|
454
|
+
const headers = rows[0].getCells().map((cell) => {
|
|
455
|
+
const value = cell.toData().value;
|
|
456
|
+
return value ? String(value) : `Column${headers.length + 1}`;
|
|
457
|
+
});
|
|
458
|
+
const data = [];
|
|
459
|
+
for (let i = 1; i < rows.length; i++) {
|
|
460
|
+
const row = rows[i];
|
|
461
|
+
const rowData = {};
|
|
462
|
+
row.getCells().forEach((cell, index) => {
|
|
463
|
+
if (index < headers.length) {
|
|
464
|
+
const cellData = cell.toData();
|
|
465
|
+
let value = cellData.value;
|
|
466
|
+
if (cellData.type === 'date' && value instanceof Date) {
|
|
467
|
+
value = value.toISOString();
|
|
468
|
+
}
|
|
469
|
+
rowData[headers[index]] = value;
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
data.push(rowData);
|
|
473
|
+
}
|
|
474
|
+
return data;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
class ExcelWriter {
|
|
479
|
+
static async write(workbook, _options) {
|
|
480
|
+
const data = this.generateExcelData(workbook);
|
|
481
|
+
const buffer = this.createExcelFile(data);
|
|
482
|
+
const uint8Buffer = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
483
|
+
const arrayBuffer = uint8Buffer.slice().buffer;
|
|
484
|
+
return new Blob([arrayBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
|
485
|
+
}
|
|
486
|
+
static generateExcelData(workbook) {
|
|
487
|
+
const sheets = workbook['sheets'];
|
|
488
|
+
return {
|
|
489
|
+
Sheets: sheets.reduce((acc, sheet, index) => {
|
|
490
|
+
const sheetName = sheet.getName() || `Sheet${index + 1}`;
|
|
491
|
+
acc[sheetName] = this.generateSheetData(sheet);
|
|
492
|
+
return acc;
|
|
493
|
+
}, {}),
|
|
494
|
+
SheetNames: sheets.map((sheet, index) => sheet.getName() || `Sheet${index + 1}`)
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
static generateSheetData(sheet) {
|
|
498
|
+
const rows = sheet.getRows();
|
|
499
|
+
const data = [];
|
|
500
|
+
rows.forEach((row) => {
|
|
501
|
+
const rowData = [];
|
|
502
|
+
row.getCells().forEach((cell) => {
|
|
503
|
+
const cellData = cell.toData();
|
|
504
|
+
if (cellData.formula) {
|
|
505
|
+
rowData.push({ f: cellData.formula });
|
|
506
|
+
}
|
|
507
|
+
else if (cellData.type === 'date' &&
|
|
508
|
+
(typeof cellData.value === 'string' ||
|
|
509
|
+
typeof cellData.value === 'number' ||
|
|
510
|
+
cellData.value instanceof Date)) {
|
|
511
|
+
rowData.push(DateFormatter.toExcelDate(cellData.value));
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
rowData.push(cellData.value);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
data.push(rowData);
|
|
518
|
+
});
|
|
519
|
+
return {
|
|
520
|
+
'!ref': this.getSheetRange(data),
|
|
521
|
+
'!rows': rows.map(row => ({
|
|
522
|
+
hpt: row['height'],
|
|
523
|
+
hidden: row['hidden'],
|
|
524
|
+
outlineLevel: row['outlineLevel'],
|
|
525
|
+
collapsed: row['collapsed']
|
|
526
|
+
})),
|
|
527
|
+
'!cols': sheet.getColumns().map(col => ({
|
|
528
|
+
wch: col['width'],
|
|
529
|
+
hidden: col['hidden'],
|
|
530
|
+
outlineLevel: col['outlineLevel'],
|
|
531
|
+
collapsed: col['collapsed']
|
|
532
|
+
})),
|
|
533
|
+
'!merges': sheet['mergeCells'],
|
|
534
|
+
'!freeze': sheet['freezePane']
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
static getSheetRange(data) {
|
|
538
|
+
if (data.length === 0)
|
|
539
|
+
return 'A1:A1';
|
|
540
|
+
const lastRow = data.length;
|
|
541
|
+
const lastCol = Math.max(...data.map(row => row.length));
|
|
542
|
+
return `A1:${this.columnToLetter(lastCol)}${lastRow}`;
|
|
543
|
+
}
|
|
544
|
+
static columnToLetter(column) {
|
|
545
|
+
let letters = '';
|
|
546
|
+
while (column > 0) {
|
|
547
|
+
const temp = (column - 1) % 26;
|
|
548
|
+
letters = String.fromCharCode(temp + 65) + letters;
|
|
549
|
+
column = (column - temp - 1) / 26;
|
|
550
|
+
}
|
|
551
|
+
return letters || 'A';
|
|
552
|
+
}
|
|
553
|
+
static createExcelFile(data) {
|
|
554
|
+
// This is a simplified Excel file generator
|
|
555
|
+
// In a real implementation, you'd generate proper XLSX binary format
|
|
556
|
+
// For now, we'll return a simple XML-based structure
|
|
557
|
+
const xmlHeader = '<?xml version="1.0" encoding="UTF-8"?>';
|
|
558
|
+
const workbook = this.generateWorkbookXML(data);
|
|
559
|
+
const encoder = new TextEncoder();
|
|
560
|
+
return encoder.encode(xmlHeader + workbook);
|
|
561
|
+
}
|
|
562
|
+
static generateWorkbookXML(data) {
|
|
563
|
+
let xml = '<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">';
|
|
564
|
+
xml += '<sheets>';
|
|
565
|
+
data.SheetNames.forEach((name, index) => {
|
|
566
|
+
xml += `<sheet name="${name}" sheetId="${index + 1}" r:id="rId${index + 1}"/>`;
|
|
567
|
+
});
|
|
568
|
+
xml += '</sheets>';
|
|
569
|
+
xml += '</workbook>';
|
|
570
|
+
return xml;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
class Workbook {
|
|
575
|
+
constructor(data) {
|
|
576
|
+
this.sheets = [];
|
|
577
|
+
this.properties = {};
|
|
578
|
+
if (data) {
|
|
579
|
+
this.fromData(data);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
addSheet(sheet) {
|
|
583
|
+
this.sheets.push(sheet);
|
|
584
|
+
return this;
|
|
585
|
+
}
|
|
586
|
+
createSheet(name) {
|
|
587
|
+
const sheet = new Worksheet(name);
|
|
588
|
+
this.sheets.push(sheet);
|
|
589
|
+
return sheet;
|
|
590
|
+
}
|
|
591
|
+
getSheet(index) {
|
|
592
|
+
return this.sheets[index];
|
|
593
|
+
}
|
|
594
|
+
getSheetByName(name) {
|
|
595
|
+
return this.sheets.find(s => s.getName() === name);
|
|
596
|
+
}
|
|
597
|
+
removeSheet(index) {
|
|
598
|
+
if (index >= 0 && index < this.sheets.length) {
|
|
599
|
+
this.sheets.splice(index, 1);
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
setProperty(key, value) {
|
|
605
|
+
this.properties[key] = value;
|
|
606
|
+
return this;
|
|
607
|
+
}
|
|
608
|
+
toData() {
|
|
609
|
+
return {
|
|
610
|
+
sheets: this.sheets.map(sheet => sheet.toData()),
|
|
611
|
+
properties: this.properties
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
fromData(data) {
|
|
615
|
+
this.sheets = data.sheets.map(sheetData => Worksheet.fromData(sheetData));
|
|
616
|
+
this.properties = data.properties || {};
|
|
617
|
+
}
|
|
618
|
+
async export(options = {}) {
|
|
619
|
+
const { format = 'xlsx' } = options;
|
|
620
|
+
switch (format) {
|
|
621
|
+
case 'csv':
|
|
622
|
+
return CSVWriter.write(this, options);
|
|
623
|
+
case 'json':
|
|
624
|
+
return JSONWriter.write(this, options);
|
|
625
|
+
case 'xlsx':
|
|
626
|
+
default:
|
|
627
|
+
return ExcelWriter.write(this, options);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
download(options = {}) {
|
|
631
|
+
const { filename = 'workbook.xlsx' } = options;
|
|
632
|
+
this.export(options).then(blob => {
|
|
633
|
+
const url = window.URL.createObjectURL(blob);
|
|
634
|
+
const a = document.createElement('a');
|
|
635
|
+
a.href = url;
|
|
636
|
+
a.download = filename;
|
|
637
|
+
a.click();
|
|
638
|
+
window.URL.revokeObjectURL(url);
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
static create() {
|
|
642
|
+
return new Workbook();
|
|
643
|
+
}
|
|
644
|
+
static fromData(data) {
|
|
645
|
+
return new Workbook(data);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
class StyleBuilder {
|
|
650
|
+
constructor() {
|
|
651
|
+
this.style = {};
|
|
652
|
+
}
|
|
653
|
+
bold(bold = true) {
|
|
654
|
+
this.style.bold = bold;
|
|
655
|
+
return this;
|
|
656
|
+
}
|
|
657
|
+
italic(italic = true) {
|
|
658
|
+
this.style.italic = italic;
|
|
659
|
+
return this;
|
|
660
|
+
}
|
|
661
|
+
underline(underline = true) {
|
|
662
|
+
this.style.underline = underline;
|
|
663
|
+
return this;
|
|
664
|
+
}
|
|
665
|
+
fontSize(size) {
|
|
666
|
+
this.style.fontSize = size;
|
|
667
|
+
return this;
|
|
668
|
+
}
|
|
669
|
+
fontFamily(family) {
|
|
670
|
+
this.style.fontFamily = family;
|
|
671
|
+
return this;
|
|
672
|
+
}
|
|
673
|
+
color(color) {
|
|
674
|
+
this.style.color = color;
|
|
675
|
+
return this;
|
|
676
|
+
}
|
|
677
|
+
backgroundColor(color) {
|
|
678
|
+
this.style.backgroundColor = color;
|
|
679
|
+
return this;
|
|
680
|
+
}
|
|
681
|
+
align(alignment) {
|
|
682
|
+
this.style.alignment = alignment;
|
|
683
|
+
return this;
|
|
684
|
+
}
|
|
685
|
+
verticalAlign(alignment) {
|
|
686
|
+
this.style.verticalAlignment = alignment;
|
|
687
|
+
return this;
|
|
688
|
+
}
|
|
689
|
+
wrapText(wrap = true) {
|
|
690
|
+
this.style.wrapText = wrap;
|
|
691
|
+
return this;
|
|
692
|
+
}
|
|
693
|
+
border(border) {
|
|
694
|
+
this.style.border = border;
|
|
695
|
+
return this;
|
|
696
|
+
}
|
|
697
|
+
borderTop(style = 'thin', color) {
|
|
698
|
+
if (!this.style.border)
|
|
699
|
+
this.style.border = {};
|
|
700
|
+
this.style.border.top = { style, color };
|
|
701
|
+
return this;
|
|
702
|
+
}
|
|
703
|
+
borderRight(style = 'thin', color) {
|
|
704
|
+
if (!this.style.border)
|
|
705
|
+
this.style.border = {};
|
|
706
|
+
this.style.border.right = { style, color };
|
|
707
|
+
return this;
|
|
708
|
+
}
|
|
709
|
+
borderBottom(style = 'thin', color) {
|
|
710
|
+
if (!this.style.border)
|
|
711
|
+
this.style.border = {};
|
|
712
|
+
this.style.border.bottom = { style, color };
|
|
713
|
+
return this;
|
|
714
|
+
}
|
|
715
|
+
borderLeft(style = 'thin', color) {
|
|
716
|
+
if (!this.style.border)
|
|
717
|
+
this.style.border = {};
|
|
718
|
+
this.style.border.left = { style, color };
|
|
719
|
+
return this;
|
|
720
|
+
}
|
|
721
|
+
borderAll(style = 'thin', color) {
|
|
722
|
+
const borderStyle = { style, color };
|
|
723
|
+
this.style.border = {
|
|
724
|
+
top: borderStyle,
|
|
725
|
+
bottom: borderStyle,
|
|
726
|
+
left: borderStyle,
|
|
727
|
+
right: borderStyle
|
|
728
|
+
};
|
|
729
|
+
return this;
|
|
730
|
+
}
|
|
731
|
+
numberFormat(format) {
|
|
732
|
+
this.style.numberFormat = format;
|
|
733
|
+
return this;
|
|
734
|
+
}
|
|
735
|
+
build() {
|
|
736
|
+
return { ...this.style };
|
|
737
|
+
}
|
|
738
|
+
static create() {
|
|
739
|
+
return new StyleBuilder();
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
class ExportBuilder {
|
|
744
|
+
constructor(sheetName = 'Sheet1') {
|
|
745
|
+
this.workbook = new Workbook();
|
|
746
|
+
this.currentSheet = new Worksheet(sheetName);
|
|
747
|
+
this.workbook.addSheet(this.currentSheet);
|
|
748
|
+
}
|
|
749
|
+
addHeaderRow(headers, style) {
|
|
750
|
+
const row = this.currentSheet.createRow();
|
|
751
|
+
headers.forEach(header => {
|
|
752
|
+
row.createCell(header, style);
|
|
753
|
+
});
|
|
754
|
+
return this;
|
|
755
|
+
}
|
|
756
|
+
addDataRows(data, fields) {
|
|
757
|
+
data.forEach(item => {
|
|
758
|
+
const row = this.currentSheet.createRow();
|
|
759
|
+
if (fields && fields.length > 0) {
|
|
760
|
+
fields.forEach(field => {
|
|
761
|
+
const value = this.getNestedValue(item, field);
|
|
762
|
+
row.createCell(value);
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
else if (Array.isArray(item)) {
|
|
766
|
+
item.forEach(value => row.createCell(value));
|
|
767
|
+
}
|
|
768
|
+
else if (typeof item === 'object') {
|
|
769
|
+
Object.values(item).forEach(value => row.createCell(value));
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
row.createCell(item);
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
return this;
|
|
776
|
+
}
|
|
777
|
+
addSection(config) {
|
|
778
|
+
// Add section header
|
|
779
|
+
const headerRow = this.currentSheet.createRow();
|
|
780
|
+
headerRow.setOutlineLevel(config.level);
|
|
781
|
+
const headerCell = headerRow.createCell(config.title);
|
|
782
|
+
headerCell.setStyle(StyleBuilder.create()
|
|
783
|
+
.bold(true)
|
|
784
|
+
.backgroundColor('#E0E0E0')
|
|
785
|
+
.build());
|
|
786
|
+
// Add section data
|
|
787
|
+
if (config.data && config.data.length > 0) {
|
|
788
|
+
if (config.groupBy && config.subSections) {
|
|
789
|
+
// Handle grouped data
|
|
790
|
+
const groupedData = this.groupData(config.data, config.groupBy);
|
|
791
|
+
Object.entries(groupedData).forEach(([key, items]) => {
|
|
792
|
+
const subHeaderRow = this.currentSheet.createRow();
|
|
793
|
+
subHeaderRow.setOutlineLevel(config.level + 1);
|
|
794
|
+
subHeaderRow.createCell(`${config.groupBy}: ${key}`);
|
|
795
|
+
items.forEach((item) => {
|
|
796
|
+
const dataRow = this.currentSheet.createRow();
|
|
797
|
+
dataRow.setOutlineLevel(config.level + 2);
|
|
798
|
+
if (config.fields) {
|
|
799
|
+
config.fields.forEach((field) => {
|
|
800
|
+
dataRow.createCell(this.getNestedValue(item, field));
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
// Add data rows with outline level
|
|
808
|
+
config.data.forEach((item) => {
|
|
809
|
+
const row = this.currentSheet.createRow();
|
|
810
|
+
row.setOutlineLevel(config.level + 1);
|
|
811
|
+
if (config.fields) {
|
|
812
|
+
config.fields.forEach((field) => {
|
|
813
|
+
row.createCell(this.getNestedValue(item, field));
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return this;
|
|
820
|
+
}
|
|
821
|
+
addSections(sections) {
|
|
822
|
+
sections.forEach(section => this.addSection(section));
|
|
823
|
+
return this;
|
|
824
|
+
}
|
|
825
|
+
setColumnWidths(widths) {
|
|
826
|
+
widths.forEach((width) => {
|
|
827
|
+
this.currentSheet.createColumn(width);
|
|
828
|
+
});
|
|
829
|
+
return this;
|
|
830
|
+
}
|
|
831
|
+
autoSizeColumns() {
|
|
832
|
+
const rows = this.currentSheet.getRows();
|
|
833
|
+
const maxLengths = [];
|
|
834
|
+
rows.forEach(row => {
|
|
835
|
+
row.getCells().forEach((cell, index) => {
|
|
836
|
+
const value = cell.toData().value;
|
|
837
|
+
const length = value ? String(value).length : 0;
|
|
838
|
+
if (!maxLengths[index] || length > maxLengths[index]) {
|
|
839
|
+
maxLengths[index] = Math.min(length, 50); // Cap at 50 characters
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
maxLengths.forEach((length) => {
|
|
844
|
+
this.currentSheet.createColumn(Math.max(length, 10)); // Minimum width 10
|
|
845
|
+
});
|
|
846
|
+
return this;
|
|
847
|
+
}
|
|
848
|
+
addStyle(style) {
|
|
849
|
+
// Apply style to last row
|
|
850
|
+
const rows = this.currentSheet.getRows();
|
|
851
|
+
if (rows.length > 0) {
|
|
852
|
+
const lastRow = rows[rows.length - 1];
|
|
853
|
+
lastRow.getCells().forEach(cell => {
|
|
854
|
+
cell.setStyle(style);
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
return this;
|
|
858
|
+
}
|
|
859
|
+
groupData(data, field) {
|
|
860
|
+
return data.reduce((groups, item) => {
|
|
861
|
+
const key = this.getNestedValue(item, field);
|
|
862
|
+
if (!groups[key]) {
|
|
863
|
+
groups[key] = [];
|
|
864
|
+
}
|
|
865
|
+
groups[key].push(item);
|
|
866
|
+
return groups;
|
|
867
|
+
}, {});
|
|
868
|
+
}
|
|
869
|
+
getNestedValue(obj, path) {
|
|
870
|
+
return path.split('.').reduce((current, key) => {
|
|
871
|
+
return current && current[key] !== undefined ? current[key] : undefined;
|
|
872
|
+
}, obj);
|
|
873
|
+
}
|
|
874
|
+
build() {
|
|
875
|
+
return this.workbook;
|
|
876
|
+
}
|
|
877
|
+
async export(options) {
|
|
878
|
+
return this.workbook.export(options);
|
|
879
|
+
}
|
|
880
|
+
download(options = {}) {
|
|
881
|
+
const filename = options.filename || 'export.xlsx';
|
|
882
|
+
const format = options.format || (filename.endsWith('.csv') ? 'csv' :
|
|
883
|
+
filename.endsWith('.json') ? 'json' :
|
|
884
|
+
'xlsx');
|
|
885
|
+
let writer;
|
|
886
|
+
if (format === 'xlsx')
|
|
887
|
+
writer = ExcelWriter;
|
|
888
|
+
else if (format === 'csv')
|
|
889
|
+
writer = CSVWriter;
|
|
890
|
+
else if (format === 'json')
|
|
891
|
+
writer = JSONWriter;
|
|
892
|
+
else
|
|
893
|
+
throw new Error('Unsupported format');
|
|
894
|
+
writer.write(this.workbook, options).then((blob) => {
|
|
895
|
+
const url = URL.createObjectURL(blob);
|
|
896
|
+
const a = document.createElement('a');
|
|
897
|
+
a.href = url;
|
|
898
|
+
a.download = filename;
|
|
899
|
+
document.body.appendChild(a);
|
|
900
|
+
a.click();
|
|
901
|
+
setTimeout(() => {
|
|
902
|
+
document.body.removeChild(a);
|
|
903
|
+
URL.revokeObjectURL(url);
|
|
904
|
+
}, 100);
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
static create(sheetName) {
|
|
908
|
+
return new ExportBuilder(sheetName);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* SheetBuilder provides a fluent API for building Excel worksheets
|
|
914
|
+
*/
|
|
915
|
+
class SheetBuilder {
|
|
916
|
+
/**
|
|
917
|
+
* Create a new SheetBuilder instance
|
|
918
|
+
* @param name Worksheet name
|
|
919
|
+
*/
|
|
920
|
+
constructor(name = 'Sheet1') {
|
|
921
|
+
this.currentRow = 0;
|
|
922
|
+
this.worksheet = new Worksheet(name);
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Get the underlying worksheet
|
|
926
|
+
*/
|
|
927
|
+
getWorksheet() {
|
|
928
|
+
return this.worksheet;
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Set the worksheet name
|
|
932
|
+
*/
|
|
933
|
+
setName(name) {
|
|
934
|
+
this.worksheet.setName(name);
|
|
935
|
+
return this;
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Add a header row with styling
|
|
939
|
+
* @param headers Array of header text
|
|
940
|
+
* @param style Optional style for all headers
|
|
941
|
+
*/
|
|
942
|
+
addHeaderRow(headers, style) {
|
|
943
|
+
const row = this.createRow();
|
|
944
|
+
headers.forEach((header) => {
|
|
945
|
+
const cellStyle = style || StyleBuilder.create()
|
|
946
|
+
.bold(true)
|
|
947
|
+
.backgroundColor('#F0F0F0')
|
|
948
|
+
.borderAll('thin')
|
|
949
|
+
.align('center')
|
|
950
|
+
.build();
|
|
951
|
+
row.createCell(header, cellStyle);
|
|
952
|
+
});
|
|
953
|
+
return this;
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Add a title row
|
|
957
|
+
* @param title Title text
|
|
958
|
+
* @param colSpan Number of columns to span
|
|
959
|
+
*/
|
|
960
|
+
addTitle(title, colSpan = 1) {
|
|
961
|
+
const row = this.createRow();
|
|
962
|
+
const cell = row.createCell(title);
|
|
963
|
+
cell.setStyle(StyleBuilder.create()
|
|
964
|
+
.bold(true)
|
|
965
|
+
.fontSize(14)
|
|
966
|
+
.align('center')
|
|
967
|
+
.build());
|
|
968
|
+
if (colSpan > 1) {
|
|
969
|
+
this.mergeCells(this.currentRow - 1, 0, this.currentRow - 1, colSpan - 1);
|
|
970
|
+
}
|
|
971
|
+
return this;
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Add a subtitle row
|
|
975
|
+
* @param subtitle Subtitle text
|
|
976
|
+
* @param colSpan Number of columns to span
|
|
977
|
+
*/
|
|
978
|
+
addSubtitle(subtitle, colSpan = 1) {
|
|
979
|
+
const row = this.createRow();
|
|
980
|
+
const cell = row.createCell(subtitle);
|
|
981
|
+
cell.setStyle(StyleBuilder.create()
|
|
982
|
+
.italic(true)
|
|
983
|
+
.color('#666666')
|
|
984
|
+
.align('center')
|
|
985
|
+
.build());
|
|
986
|
+
if (colSpan > 1) {
|
|
987
|
+
this.mergeCells(this.currentRow - 1, 0, this.currentRow - 1, colSpan - 1);
|
|
988
|
+
}
|
|
989
|
+
return this;
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Add a row of data
|
|
993
|
+
* @param data Array of cell values
|
|
994
|
+
* @param styles Optional array of styles per cell
|
|
995
|
+
*/
|
|
996
|
+
addRow(data, styles) {
|
|
997
|
+
const row = this.createRow();
|
|
998
|
+
data.forEach((value, index) => {
|
|
999
|
+
const style = styles && styles[index] ? styles[index] : undefined;
|
|
1000
|
+
row.createCell(value, style);
|
|
1001
|
+
});
|
|
1002
|
+
return this;
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Add multiple rows of data
|
|
1006
|
+
* @param rows Array of row data
|
|
1007
|
+
* @param styles Optional array of styles per row or per cell
|
|
1008
|
+
*/
|
|
1009
|
+
addRows(rows, styles) {
|
|
1010
|
+
rows.forEach((rowData, rowIndex) => {
|
|
1011
|
+
const rowStyles = styles && styles[rowIndex];
|
|
1012
|
+
if (Array.isArray(rowStyles)) {
|
|
1013
|
+
this.addRow(rowData, rowStyles);
|
|
1014
|
+
}
|
|
1015
|
+
else {
|
|
1016
|
+
this.addRow(rowData, rowStyles ? [rowStyles] : undefined);
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
return this;
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Add data from objects
|
|
1023
|
+
* @param data Array of objects
|
|
1024
|
+
* @param fields Fields to extract (keys or dot notation paths)
|
|
1025
|
+
* @param headers Optional header labels
|
|
1026
|
+
*/
|
|
1027
|
+
addObjects(data, fields, headers) {
|
|
1028
|
+
// Add headers if provided
|
|
1029
|
+
if (headers) {
|
|
1030
|
+
this.addHeaderRow(headers);
|
|
1031
|
+
}
|
|
1032
|
+
// Add data rows
|
|
1033
|
+
data.forEach(item => {
|
|
1034
|
+
const rowData = fields.map(field => this.getNestedValue(item, field));
|
|
1035
|
+
this.addRow(rowData);
|
|
1036
|
+
});
|
|
1037
|
+
return this;
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Add a section with header and data
|
|
1041
|
+
* @param title Section title
|
|
1042
|
+
* @param data Section data
|
|
1043
|
+
* @param fields Fields to display
|
|
1044
|
+
* @param level Outline level
|
|
1045
|
+
*/
|
|
1046
|
+
addSection(title, data, fields, level = 0) {
|
|
1047
|
+
// Add section header
|
|
1048
|
+
const headerRow = this.createRow();
|
|
1049
|
+
headerRow.setOutlineLevel(level);
|
|
1050
|
+
const headerCell = headerRow.createCell(title);
|
|
1051
|
+
headerCell.setStyle(StyleBuilder.create()
|
|
1052
|
+
.bold(true)
|
|
1053
|
+
.fontSize(12)
|
|
1054
|
+
.backgroundColor('#E0E0E0')
|
|
1055
|
+
.borderAll('thin')
|
|
1056
|
+
.build());
|
|
1057
|
+
// Merge header across all columns
|
|
1058
|
+
if (fields.length > 1) {
|
|
1059
|
+
this.mergeCells(this.currentRow - 1, 0, this.currentRow - 1, fields.length - 1);
|
|
1060
|
+
}
|
|
1061
|
+
// Add field headers
|
|
1062
|
+
const fieldRow = this.createRow();
|
|
1063
|
+
fieldRow.setOutlineLevel(level + 1);
|
|
1064
|
+
fields.forEach(field => {
|
|
1065
|
+
fieldRow.createCell(this.formatFieldName(field), StyleBuilder.create()
|
|
1066
|
+
.bold(true)
|
|
1067
|
+
.backgroundColor('#F5F5F5')
|
|
1068
|
+
.borderAll('thin')
|
|
1069
|
+
.build());
|
|
1070
|
+
});
|
|
1071
|
+
// Add data rows
|
|
1072
|
+
data.forEach(item => {
|
|
1073
|
+
const row = this.createRow();
|
|
1074
|
+
row.setOutlineLevel(level + 2);
|
|
1075
|
+
fields.forEach(field => {
|
|
1076
|
+
const value = this.getNestedValue(item, field);
|
|
1077
|
+
row.createCell(value);
|
|
1078
|
+
});
|
|
1079
|
+
});
|
|
1080
|
+
return this;
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Add grouped data with sub-sections
|
|
1084
|
+
* @param data Data to group
|
|
1085
|
+
* @param groupBy Field to group by
|
|
1086
|
+
* @param fields Fields to display
|
|
1087
|
+
* @param level Outline level
|
|
1088
|
+
*/
|
|
1089
|
+
addGroupedData(data, groupBy, fields, level = 0) {
|
|
1090
|
+
const groups = this.groupData(data, groupBy);
|
|
1091
|
+
Object.entries(groups).forEach(([key, items]) => {
|
|
1092
|
+
// Add group header
|
|
1093
|
+
const groupRow = this.createRow();
|
|
1094
|
+
groupRow.setOutlineLevel(level);
|
|
1095
|
+
const groupCell = groupRow.createCell(`${groupBy}: ${key}`);
|
|
1096
|
+
groupCell.setStyle(StyleBuilder.create()
|
|
1097
|
+
.bold(true)
|
|
1098
|
+
.backgroundColor('#E8E8E8')
|
|
1099
|
+
.build());
|
|
1100
|
+
if (fields.length > 1) {
|
|
1101
|
+
this.mergeCells(this.currentRow - 1, 0, this.currentRow - 1, fields.length - 1);
|
|
1102
|
+
}
|
|
1103
|
+
// Add field headers
|
|
1104
|
+
const fieldRow = this.createRow();
|
|
1105
|
+
fieldRow.setOutlineLevel(level + 1);
|
|
1106
|
+
fields.forEach(field => {
|
|
1107
|
+
fieldRow.createCell(this.formatFieldName(field), StyleBuilder.create()
|
|
1108
|
+
.bold(true)
|
|
1109
|
+
.backgroundColor('#F5F5F5')
|
|
1110
|
+
.build());
|
|
1111
|
+
});
|
|
1112
|
+
// Add items
|
|
1113
|
+
items.forEach((item) => {
|
|
1114
|
+
const row = this.createRow();
|
|
1115
|
+
row.setOutlineLevel(level + 2);
|
|
1116
|
+
fields.forEach(field => {
|
|
1117
|
+
const value = this.getNestedValue(item, field);
|
|
1118
|
+
row.createCell(value);
|
|
1119
|
+
});
|
|
1120
|
+
});
|
|
1121
|
+
// Add group summary
|
|
1122
|
+
this.addGroupSummary(items, fields, level + 1);
|
|
1123
|
+
});
|
|
1124
|
+
return this;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Add summary row for a group
|
|
1128
|
+
*/
|
|
1129
|
+
addGroupSummary(items, fields, level) {
|
|
1130
|
+
const summaryRow = this.createRow();
|
|
1131
|
+
summaryRow.setOutlineLevel(level + 1);
|
|
1132
|
+
// Create summary cells
|
|
1133
|
+
fields.forEach((_, index) => {
|
|
1134
|
+
if (index === 0) {
|
|
1135
|
+
summaryRow.createCell('Group Summary', StyleBuilder.create()
|
|
1136
|
+
.italic(true)
|
|
1137
|
+
.bold(true)
|
|
1138
|
+
.build());
|
|
1139
|
+
}
|
|
1140
|
+
else {
|
|
1141
|
+
const numericValues = items
|
|
1142
|
+
.map(item => this.getNestedValue(item, fields[index]))
|
|
1143
|
+
.filter(val => typeof val === 'number');
|
|
1144
|
+
if (numericValues.length > 0) {
|
|
1145
|
+
const sum = numericValues.reduce((a, b) => a + b, 0);
|
|
1146
|
+
summaryRow.createCell(sum, StyleBuilder.create()
|
|
1147
|
+
.bold(true)
|
|
1148
|
+
.numberFormat('#,##0.00')
|
|
1149
|
+
.build());
|
|
1150
|
+
}
|
|
1151
|
+
else {
|
|
1152
|
+
summaryRow.createCell('');
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
return this;
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Add a summary row with totals
|
|
1160
|
+
* @param fields Fields to summarize
|
|
1161
|
+
* @param functions Summary functions (sum, average, count, min, max)
|
|
1162
|
+
* @param label Summary label
|
|
1163
|
+
*/
|
|
1164
|
+
addSummaryRow(fields, functions, label = 'Total') {
|
|
1165
|
+
const row = this.createRow();
|
|
1166
|
+
fields.forEach((_, index) => {
|
|
1167
|
+
if (index === 0) {
|
|
1168
|
+
row.createCell(label, StyleBuilder.create()
|
|
1169
|
+
.bold(true)
|
|
1170
|
+
.borderTop('double')
|
|
1171
|
+
.build());
|
|
1172
|
+
}
|
|
1173
|
+
else {
|
|
1174
|
+
const function_ = functions[index - 1] || 'sum';
|
|
1175
|
+
row.createCell(`=${function_.toUpperCase()}(${this.getColumnRange(index)})`, StyleBuilder.create()
|
|
1176
|
+
.bold(true)
|
|
1177
|
+
.borderTop('double')
|
|
1178
|
+
.numberFormat('#,##0.00')
|
|
1179
|
+
.build());
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
return this;
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Set column widths
|
|
1186
|
+
* @param widths Array of column widths
|
|
1187
|
+
*/
|
|
1188
|
+
setColumnWidths(widths) {
|
|
1189
|
+
widths.forEach((width, index) => {
|
|
1190
|
+
const column = this.getOrCreateColumn(index);
|
|
1191
|
+
column.setWidth(width);
|
|
1192
|
+
});
|
|
1193
|
+
return this;
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Auto-size columns based on content
|
|
1197
|
+
* @param maxWidth Maximum width in characters
|
|
1198
|
+
*/
|
|
1199
|
+
autoSizeColumns(maxWidth = 50) {
|
|
1200
|
+
const rows = this.worksheet.getRows();
|
|
1201
|
+
const columnWidths = new Map();
|
|
1202
|
+
rows.forEach(row => {
|
|
1203
|
+
row.getCells().forEach((cell, index) => {
|
|
1204
|
+
const cellData = cell.toData();
|
|
1205
|
+
const value = cellData.value;
|
|
1206
|
+
const length = value ? String(value).length : 0;
|
|
1207
|
+
const currentMax = columnWidths.get(index) || 0;
|
|
1208
|
+
columnWidths.set(index, Math.min(Math.max(length, currentMax), maxWidth));
|
|
1209
|
+
});
|
|
1210
|
+
});
|
|
1211
|
+
columnWidths.forEach((width, index) => {
|
|
1212
|
+
const column = this.getOrCreateColumn(index);
|
|
1213
|
+
column.setWidth(Math.max(width + 2, 8)); // Add padding, minimum 8
|
|
1214
|
+
});
|
|
1215
|
+
return this;
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Set column to auto-fit
|
|
1219
|
+
* @param colIndex Column index
|
|
1220
|
+
*/
|
|
1221
|
+
setColumnAutoFit(colIndex) {
|
|
1222
|
+
const column = this.getOrCreateColumn(colIndex);
|
|
1223
|
+
// Auto-fit will be applied during export
|
|
1224
|
+
column.setWidth(0); // 0 indicates auto-fit
|
|
1225
|
+
return this;
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Hide a column
|
|
1229
|
+
* @param colIndex Column index
|
|
1230
|
+
*/
|
|
1231
|
+
hideColumn(colIndex) {
|
|
1232
|
+
const column = this.getOrCreateColumn(colIndex);
|
|
1233
|
+
column.setHidden(true);
|
|
1234
|
+
return this;
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Hide a row
|
|
1238
|
+
* @param rowIndex Row index
|
|
1239
|
+
*/
|
|
1240
|
+
hideRow(rowIndex) {
|
|
1241
|
+
const row = this.worksheet.getRow(rowIndex);
|
|
1242
|
+
if (row) {
|
|
1243
|
+
row.setHidden(true);
|
|
1244
|
+
}
|
|
1245
|
+
return this;
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Set outline level for a row
|
|
1249
|
+
* @param rowIndex Row index
|
|
1250
|
+
* @param level Outline level (0-7)
|
|
1251
|
+
* @param collapsed Whether the outline is collapsed
|
|
1252
|
+
*/
|
|
1253
|
+
setRowOutlineLevel(rowIndex, level, collapsed = false) {
|
|
1254
|
+
const row = this.worksheet.getRow(rowIndex);
|
|
1255
|
+
if (row) {
|
|
1256
|
+
row.setOutlineLevel(level, collapsed);
|
|
1257
|
+
}
|
|
1258
|
+
return this;
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Set outline level for a column
|
|
1262
|
+
* @param colIndex Column index
|
|
1263
|
+
* @param level Outline level (0-7)
|
|
1264
|
+
* @param collapsed Whether the outline is collapsed
|
|
1265
|
+
*/
|
|
1266
|
+
setColumnOutlineLevel(colIndex, level, collapsed = false) {
|
|
1267
|
+
const column = this.getOrCreateColumn(colIndex);
|
|
1268
|
+
column.setOutlineLevel(level, collapsed);
|
|
1269
|
+
return this;
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Create an outline group for rows
|
|
1273
|
+
* @param startRow Start row index
|
|
1274
|
+
* @param endRow End row index
|
|
1275
|
+
* @param level Outline level
|
|
1276
|
+
* @param collapsed Whether the group is collapsed
|
|
1277
|
+
*/
|
|
1278
|
+
groupRows(startRow, endRow, level = 1, collapsed = false) {
|
|
1279
|
+
for (let i = startRow; i <= endRow; i++) {
|
|
1280
|
+
const row = this.worksheet.getRow(i);
|
|
1281
|
+
if (row) {
|
|
1282
|
+
row.setOutlineLevel(level, collapsed && i === startRow);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
return this;
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Create an outline group for columns
|
|
1289
|
+
* @param startCol Start column index
|
|
1290
|
+
* @param endCol End column index
|
|
1291
|
+
* @param level Outline level
|
|
1292
|
+
* @param collapsed Whether the group is collapsed
|
|
1293
|
+
*/
|
|
1294
|
+
groupColumns(startCol, endCol, level = 1, collapsed = false) {
|
|
1295
|
+
for (let i = startCol; i <= endCol; i++) {
|
|
1296
|
+
const column = this.getOrCreateColumn(i);
|
|
1297
|
+
column.setOutlineLevel(level, collapsed && i === startCol);
|
|
1298
|
+
}
|
|
1299
|
+
return this;
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Merge cells
|
|
1303
|
+
* @param startRow Start row
|
|
1304
|
+
* @param startCol Start column
|
|
1305
|
+
* @param endRow End row
|
|
1306
|
+
* @param endCol End column
|
|
1307
|
+
*/
|
|
1308
|
+
mergeCells(startRow, startCol, endRow, endCol) {
|
|
1309
|
+
this.worksheet.mergeCells(startRow, startCol, endRow, endCol);
|
|
1310
|
+
return this;
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Freeze panes
|
|
1314
|
+
* @param rows Number of rows to freeze
|
|
1315
|
+
* @param columns Number of columns to freeze
|
|
1316
|
+
*/
|
|
1317
|
+
freezePanes(rows = 0, columns = 0) {
|
|
1318
|
+
this.worksheet.setFreezePane(rows, columns);
|
|
1319
|
+
return this;
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Set print options
|
|
1323
|
+
* @param options Print options
|
|
1324
|
+
*/
|
|
1325
|
+
setPrintOptions(options) {
|
|
1326
|
+
this.worksheet.setPrintOptions(options);
|
|
1327
|
+
return this;
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Set header and footer
|
|
1331
|
+
* @param headerFooter Header/footer configuration
|
|
1332
|
+
*/
|
|
1333
|
+
setHeaderFooter(headerFooter) {
|
|
1334
|
+
// Store in worksheet (would be implemented in worksheet class)
|
|
1335
|
+
this.worksheet.headerFooter = headerFooter;
|
|
1336
|
+
return this;
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Add auto-filter
|
|
1340
|
+
* @param startRow Start row
|
|
1341
|
+
* @param startCol Start column
|
|
1342
|
+
* @param endRow End row
|
|
1343
|
+
* @param endCol End column
|
|
1344
|
+
*/
|
|
1345
|
+
addAutoFilter(startRow, startCol, endRow, endCol) {
|
|
1346
|
+
const range = `${this.columnToLetter(startCol)}${startRow + 1}:${this.columnToLetter(endCol)}${endRow + 1}`;
|
|
1347
|
+
this.worksheet.autoFilter = { range };
|
|
1348
|
+
return this;
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Add a table
|
|
1352
|
+
* @param config Table configuration
|
|
1353
|
+
*/
|
|
1354
|
+
addTable(config) {
|
|
1355
|
+
if (!this.worksheet.tables) {
|
|
1356
|
+
this.worksheet.tables = [];
|
|
1357
|
+
}
|
|
1358
|
+
this.worksheet.tables.push(config);
|
|
1359
|
+
return this;
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Add data validation to a cell or range
|
|
1363
|
+
* @param range Cell range (e.g., 'A1:B10')
|
|
1364
|
+
* @param validation Data validation rules
|
|
1365
|
+
*/
|
|
1366
|
+
addDataValidation(range, validation) {
|
|
1367
|
+
if (!this.worksheet.dataValidations) {
|
|
1368
|
+
this.worksheet.dataValidations = {};
|
|
1369
|
+
}
|
|
1370
|
+
this.worksheet.dataValidations[range] = validation;
|
|
1371
|
+
return this;
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Add conditional formatting
|
|
1375
|
+
* @param range Cell range
|
|
1376
|
+
* @param rules Conditional formatting rules
|
|
1377
|
+
*/
|
|
1378
|
+
addConditionalFormatting(range, rules) {
|
|
1379
|
+
if (!this.worksheet.conditionalFormats) {
|
|
1380
|
+
this.worksheet.conditionalFormats = {};
|
|
1381
|
+
}
|
|
1382
|
+
this.worksheet.conditionalFormats[range] = rules;
|
|
1383
|
+
return this;
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Add a chart
|
|
1387
|
+
* @param config Chart configuration
|
|
1388
|
+
*/
|
|
1389
|
+
addChart(config) {
|
|
1390
|
+
if (!this.worksheet.charts) {
|
|
1391
|
+
this.worksheet.charts = [];
|
|
1392
|
+
}
|
|
1393
|
+
this.worksheet.charts.push(config);
|
|
1394
|
+
return this;
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Add an image or drawing
|
|
1398
|
+
* @param drawing Drawing configuration
|
|
1399
|
+
*/
|
|
1400
|
+
addDrawing(drawing) {
|
|
1401
|
+
if (!this.worksheet.drawings) {
|
|
1402
|
+
this.worksheet.drawings = [];
|
|
1403
|
+
}
|
|
1404
|
+
this.worksheet.drawings.push(drawing);
|
|
1405
|
+
return this;
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Set cell value with style
|
|
1409
|
+
* @param row Row index
|
|
1410
|
+
* @param col Column index
|
|
1411
|
+
* @param value Cell value
|
|
1412
|
+
* @param style Cell style
|
|
1413
|
+
*/
|
|
1414
|
+
setCell(row, col, value, style) {
|
|
1415
|
+
this.worksheet.setCell(row, col, value, style);
|
|
1416
|
+
return this;
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Get cell value
|
|
1420
|
+
* @param row Row index
|
|
1421
|
+
* @param col Column index
|
|
1422
|
+
*/
|
|
1423
|
+
getCell(row, col) {
|
|
1424
|
+
return this.worksheet.getCell(row, col);
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Add a comment to a cell
|
|
1428
|
+
* @param row Row index
|
|
1429
|
+
* @param col Column index
|
|
1430
|
+
* @param comment Comment text
|
|
1431
|
+
* @param author Comment author
|
|
1432
|
+
*/
|
|
1433
|
+
addComment(row, col, comment, author) {
|
|
1434
|
+
const cell = this.worksheet.getCell(row, col);
|
|
1435
|
+
if (cell) {
|
|
1436
|
+
// Would need to extend Cell class to support comments
|
|
1437
|
+
cell.comment = { text: comment, author };
|
|
1438
|
+
}
|
|
1439
|
+
return this;
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Add a hyperlink to a cell
|
|
1443
|
+
* @param row Row index
|
|
1444
|
+
* @param col Column index
|
|
1445
|
+
* @param url Hyperlink URL
|
|
1446
|
+
* @param displayText Display text (optional)
|
|
1447
|
+
*/
|
|
1448
|
+
addHyperlink(row, col, url, displayText) {
|
|
1449
|
+
this.worksheet.setCell(row, col, displayText || url);
|
|
1450
|
+
const cell = this.worksheet.getCell(row, col);
|
|
1451
|
+
if (cell) {
|
|
1452
|
+
cell.hyperlink = url;
|
|
1453
|
+
}
|
|
1454
|
+
return this;
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Apply a style to a range
|
|
1458
|
+
* @param startRow Start row
|
|
1459
|
+
* @param startCol Start column
|
|
1460
|
+
* @param endRow End row
|
|
1461
|
+
* @param endCol End column
|
|
1462
|
+
* @param style Style to apply
|
|
1463
|
+
*/
|
|
1464
|
+
applyStyleToRange(startRow, startCol, endRow, endCol, style) {
|
|
1465
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
1466
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
1467
|
+
const cell = this.worksheet.getCell(r, c);
|
|
1468
|
+
if (cell) {
|
|
1469
|
+
// Would need to update cell style
|
|
1470
|
+
cell.style = { ...cell.style, ...style };
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
return this;
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Insert a blank row
|
|
1478
|
+
* @param count Number of blank rows to insert
|
|
1479
|
+
*/
|
|
1480
|
+
insertBlankRows(count = 1) {
|
|
1481
|
+
for (let i = 0; i < count; i++) {
|
|
1482
|
+
this.createRow();
|
|
1483
|
+
}
|
|
1484
|
+
return this;
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Create a new row
|
|
1488
|
+
*/
|
|
1489
|
+
createRow() {
|
|
1490
|
+
const row = this.worksheet.createRow();
|
|
1491
|
+
this.currentRow++;
|
|
1492
|
+
return row;
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Get or create a column
|
|
1496
|
+
*/
|
|
1497
|
+
getOrCreateColumn(index) {
|
|
1498
|
+
let column = this.worksheet.getColumn(index);
|
|
1499
|
+
if (!column) {
|
|
1500
|
+
column = this.worksheet.createColumn();
|
|
1501
|
+
}
|
|
1502
|
+
return column;
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* Get nested value from object using dot notation
|
|
1506
|
+
*/
|
|
1507
|
+
getNestedValue(obj, path) {
|
|
1508
|
+
return path.split('.').reduce((current, key) => {
|
|
1509
|
+
return current && current[key] !== undefined ? current[key] : undefined;
|
|
1510
|
+
}, obj);
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Format field name for display
|
|
1514
|
+
*/
|
|
1515
|
+
formatFieldName(field) {
|
|
1516
|
+
return field
|
|
1517
|
+
.split('.')
|
|
1518
|
+
.pop()
|
|
1519
|
+
.split('_')
|
|
1520
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
1521
|
+
.join(' ');
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Group data by field
|
|
1525
|
+
*/
|
|
1526
|
+
groupData(data, field) {
|
|
1527
|
+
return data.reduce((groups, item) => {
|
|
1528
|
+
const key = this.getNestedValue(item, field);
|
|
1529
|
+
if (!groups[key]) {
|
|
1530
|
+
groups[key] = [];
|
|
1531
|
+
}
|
|
1532
|
+
groups[key].push(item);
|
|
1533
|
+
return groups;
|
|
1534
|
+
}, {});
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Convert column index to letter (A, B, C, ...)
|
|
1538
|
+
*/
|
|
1539
|
+
columnToLetter(column) {
|
|
1540
|
+
let letters = '';
|
|
1541
|
+
while (column >= 0) {
|
|
1542
|
+
letters = String.fromCharCode((column % 26) + 65) + letters;
|
|
1543
|
+
column = Math.floor(column / 26) - 1;
|
|
1544
|
+
}
|
|
1545
|
+
return letters;
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Get column range for formula (e.g., A:A)
|
|
1549
|
+
*/
|
|
1550
|
+
getColumnRange(colIndex) {
|
|
1551
|
+
const colLetter = this.columnToLetter(colIndex);
|
|
1552
|
+
return `${colLetter}:${colLetter}`;
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Build and return the worksheet
|
|
1556
|
+
*/
|
|
1557
|
+
build() {
|
|
1558
|
+
return this.worksheet;
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Reset the builder
|
|
1562
|
+
*/
|
|
1563
|
+
reset() {
|
|
1564
|
+
this.worksheet = new Worksheet(this.worksheet.getName());
|
|
1565
|
+
this.currentRow = 0;
|
|
1566
|
+
return this;
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Create a new SheetBuilder instance
|
|
1570
|
+
*/
|
|
1571
|
+
static create(name) {
|
|
1572
|
+
return new SheetBuilder(name);
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Create from existing worksheet
|
|
1576
|
+
*/
|
|
1577
|
+
static fromWorksheet(worksheet) {
|
|
1578
|
+
const builder = new SheetBuilder(worksheet.getName());
|
|
1579
|
+
builder.worksheet = worksheet;
|
|
1580
|
+
return builder;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
/**
|
|
1585
|
+
* Builder class for creating collapsible sections in worksheets
|
|
1586
|
+
* Supports nested sections, grouping, summaries, and conditional styling
|
|
1587
|
+
*/
|
|
1588
|
+
class SectionBuilder {
|
|
1589
|
+
constructor(worksheet) {
|
|
1590
|
+
this.currentRow = 0;
|
|
1591
|
+
this.sections = new Map();
|
|
1592
|
+
this.styles = new Map();
|
|
1593
|
+
this.formatters = new Map();
|
|
1594
|
+
this.conditionalFormats = [];
|
|
1595
|
+
this.worksheet = worksheet;
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Add a section to the worksheet
|
|
1599
|
+
*/
|
|
1600
|
+
addSection(config) {
|
|
1601
|
+
const section = new Section(this.worksheet, config, this.currentRow);
|
|
1602
|
+
if (config.title !== undefined) {
|
|
1603
|
+
this.sections.set(config.title, section);
|
|
1604
|
+
}
|
|
1605
|
+
// Build the section
|
|
1606
|
+
this.currentRow = section.build();
|
|
1607
|
+
return this;
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Add multiple sections at once
|
|
1611
|
+
*/
|
|
1612
|
+
addSections(configs) {
|
|
1613
|
+
configs.forEach(config => this.addSection(config));
|
|
1614
|
+
return this;
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* Add a nested section (child of current section)
|
|
1618
|
+
*/
|
|
1619
|
+
addNestedSection(parentTitle, config) {
|
|
1620
|
+
const parent = this.sections.get(parentTitle);
|
|
1621
|
+
if (parent) {
|
|
1622
|
+
parent.addSubSection(config);
|
|
1623
|
+
}
|
|
1624
|
+
return this;
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Create a section from data with automatic grouping
|
|
1628
|
+
*/
|
|
1629
|
+
createFromData(data, options) {
|
|
1630
|
+
const { title, groupBy, fields = Object.keys(data[0] || {}), fieldLabels = {}, level = 0, collapsed = false, summary } = options;
|
|
1631
|
+
// If no grouping, create a simple section
|
|
1632
|
+
if (!groupBy) {
|
|
1633
|
+
return this.addSection({
|
|
1634
|
+
title,
|
|
1635
|
+
level,
|
|
1636
|
+
collapsed,
|
|
1637
|
+
data,
|
|
1638
|
+
fields: fields,
|
|
1639
|
+
fieldLabels,
|
|
1640
|
+
summary: summary ? {
|
|
1641
|
+
fields: summary.fields,
|
|
1642
|
+
function: summary.functions[0] || 'count',
|
|
1643
|
+
label: 'Total'
|
|
1644
|
+
} : undefined
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
// Group the data
|
|
1648
|
+
const groups = this.groupData(data, groupBy);
|
|
1649
|
+
// Create main section with groups as subsections
|
|
1650
|
+
const sectionConfig = {
|
|
1651
|
+
title,
|
|
1652
|
+
level,
|
|
1653
|
+
collapsed,
|
|
1654
|
+
data: [],
|
|
1655
|
+
subSections: Object.entries(groups).map(([key, items]) => ({
|
|
1656
|
+
title: `${String(groupBy)}: ${key}`,
|
|
1657
|
+
level: level + 1,
|
|
1658
|
+
collapsed: true,
|
|
1659
|
+
data: items,
|
|
1660
|
+
fields: fields,
|
|
1661
|
+
fieldLabels,
|
|
1662
|
+
summary: summary ? {
|
|
1663
|
+
fields: summary.fields,
|
|
1664
|
+
function: summary.functions[0] || 'count',
|
|
1665
|
+
label: `Total for ${key}`
|
|
1666
|
+
} : undefined
|
|
1667
|
+
}))
|
|
1668
|
+
};
|
|
1669
|
+
return this.addSection(sectionConfig);
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Add a summary section (totals, averages, etc.)
|
|
1673
|
+
*/
|
|
1674
|
+
addSummarySection(data, fields, functions, options) {
|
|
1675
|
+
const { level = 0, style, showPercentage = false, label = 'Summary' } = options || {};
|
|
1676
|
+
// Calculate summary values
|
|
1677
|
+
const summary = {};
|
|
1678
|
+
fields.forEach((field, index) => {
|
|
1679
|
+
const func = functions[index] || functions[0];
|
|
1680
|
+
const values = data.map(item => this.getNestedValue(item, field)).filter(v => v != null);
|
|
1681
|
+
switch (func) {
|
|
1682
|
+
case 'sum':
|
|
1683
|
+
summary[field] = values.reduce((a, b) => a + b, 0);
|
|
1684
|
+
break;
|
|
1685
|
+
case 'average':
|
|
1686
|
+
summary[field] = values.reduce((a, b) => a + b, 0) / values.length;
|
|
1687
|
+
break;
|
|
1688
|
+
case 'count':
|
|
1689
|
+
summary[field] = values.length;
|
|
1690
|
+
break;
|
|
1691
|
+
case 'min':
|
|
1692
|
+
summary[field] = Math.min(...values);
|
|
1693
|
+
break;
|
|
1694
|
+
case 'max':
|
|
1695
|
+
summary[field] = Math.max(...values);
|
|
1696
|
+
break;
|
|
1697
|
+
}
|
|
1698
|
+
// Add percentage if requested
|
|
1699
|
+
if (showPercentage && func === 'count') {
|
|
1700
|
+
const total = data.length;
|
|
1701
|
+
summary[`${field}_percentage`] = ((summary[field] / total) * 100).toFixed(1) + '%';
|
|
1702
|
+
}
|
|
1703
|
+
});
|
|
1704
|
+
// Add the summary row
|
|
1705
|
+
const summaryRow = this.worksheet.createRow();
|
|
1706
|
+
summaryRow.setOutlineLevel(level + 1);
|
|
1707
|
+
// Add label cell
|
|
1708
|
+
const labelCell = summaryRow.createCell(`${label}:`);
|
|
1709
|
+
labelCell.setStyle(StyleBuilder.create()
|
|
1710
|
+
.bold(true)
|
|
1711
|
+
.italic(true)
|
|
1712
|
+
.backgroundColor('#f0f0f0')
|
|
1713
|
+
.build());
|
|
1714
|
+
// Add summary values
|
|
1715
|
+
fields.forEach(field => {
|
|
1716
|
+
const value = summary[field];
|
|
1717
|
+
const cell = summaryRow.createCell(value);
|
|
1718
|
+
if (style) {
|
|
1719
|
+
cell.setStyle(style);
|
|
1720
|
+
}
|
|
1721
|
+
else {
|
|
1722
|
+
cell.setStyle(StyleBuilder.create()
|
|
1723
|
+
.bold(true)
|
|
1724
|
+
.numberFormat(functions[0] === 'count' ? '#,##0' : '#,##0.00')
|
|
1725
|
+
.build());
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
return this;
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Add a hierarchical section (tree structure)
|
|
1732
|
+
*/
|
|
1733
|
+
addHierarchicalSection(items, childrenAccessor, options) {
|
|
1734
|
+
const { title = 'Hierarchy', level = 0, fields, collapsed = true, showCount = true } = options || {};
|
|
1735
|
+
const buildHierarchy = (items, currentLevel) => {
|
|
1736
|
+
return items.map(item => {
|
|
1737
|
+
const children = childrenAccessor(item);
|
|
1738
|
+
const hasChildren = children && children.length > 0;
|
|
1739
|
+
// Get display value for title
|
|
1740
|
+
const titleField = (fields === null || fields === void 0 ? void 0 : fields[0]) || 'name';
|
|
1741
|
+
const titleValue = this.getNestedValue(item, titleField);
|
|
1742
|
+
const count = hasChildren ? ` (${children.length})` : '';
|
|
1743
|
+
return {
|
|
1744
|
+
title: `${titleValue}${showCount && hasChildren ? count : ''}`,
|
|
1745
|
+
level: currentLevel,
|
|
1746
|
+
collapsed,
|
|
1747
|
+
data: [item],
|
|
1748
|
+
fields: fields,
|
|
1749
|
+
subSections: hasChildren ? buildHierarchy(children, currentLevel + 1) : undefined
|
|
1750
|
+
};
|
|
1751
|
+
});
|
|
1752
|
+
};
|
|
1753
|
+
const hierarchySections = buildHierarchy(items, level + 1);
|
|
1754
|
+
return this.addSection({
|
|
1755
|
+
title: title,
|
|
1756
|
+
level,
|
|
1757
|
+
collapsed,
|
|
1758
|
+
data: [],
|
|
1759
|
+
subSections: hierarchySections
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Add a pivot-like section with multiple dimensions
|
|
1764
|
+
*/
|
|
1765
|
+
addPivotSection(data, dimensions, options) {
|
|
1766
|
+
const { level = 0, showGrandTotals = true, showSubTotals = true } = options || {};
|
|
1767
|
+
// Group by row dimensions
|
|
1768
|
+
const rowGroups = this.groupMultiLevel(data, dimensions.rows);
|
|
1769
|
+
// Create sections for each row group
|
|
1770
|
+
Object.entries(rowGroups).forEach(([rowKey, rowItems]) => {
|
|
1771
|
+
const rowSection = {
|
|
1772
|
+
title: rowKey,
|
|
1773
|
+
level: level + 1,
|
|
1774
|
+
collapsed: true,
|
|
1775
|
+
data: [],
|
|
1776
|
+
subSections: []
|
|
1777
|
+
};
|
|
1778
|
+
// Group by column dimensions within each row
|
|
1779
|
+
if (dimensions.columns.length > 0) {
|
|
1780
|
+
const colGroups = this.groupMultiLevel(rowItems, dimensions.columns);
|
|
1781
|
+
Object.entries(colGroups).forEach(([colKey, colItems]) => {
|
|
1782
|
+
// Calculate values for this cell
|
|
1783
|
+
const values = {};
|
|
1784
|
+
dimensions.values.forEach(v => {
|
|
1785
|
+
const nums = colItems.map(item => item[v.field]).filter((n) => !isNaN(n));
|
|
1786
|
+
switch (v.aggregate) {
|
|
1787
|
+
case 'sum':
|
|
1788
|
+
values[v.field] = nums.reduce((a, b) => a + b, 0);
|
|
1789
|
+
break;
|
|
1790
|
+
case 'average':
|
|
1791
|
+
values[v.field] = nums.length ? nums.reduce((a, b) => a + b, 0) / nums.length : 0;
|
|
1792
|
+
break;
|
|
1793
|
+
case 'count':
|
|
1794
|
+
values[v.field] = colItems.length;
|
|
1795
|
+
break;
|
|
1796
|
+
case 'min':
|
|
1797
|
+
values[v.field] = Math.min(...nums);
|
|
1798
|
+
break;
|
|
1799
|
+
case 'max':
|
|
1800
|
+
values[v.field] = Math.max(...nums);
|
|
1801
|
+
break;
|
|
1802
|
+
}
|
|
1803
|
+
});
|
|
1804
|
+
rowSection.subSections.push({
|
|
1805
|
+
title: colKey,
|
|
1806
|
+
level: level + 2,
|
|
1807
|
+
data: [values],
|
|
1808
|
+
fields: dimensions.values.map(v => v.field)
|
|
1809
|
+
});
|
|
1810
|
+
});
|
|
1811
|
+
// Add row subtotal
|
|
1812
|
+
if (showSubTotals) {
|
|
1813
|
+
const subtotalValues = {};
|
|
1814
|
+
dimensions.values.forEach(v => {
|
|
1815
|
+
const nums = rowItems.map(item => item[v.field]).filter((n) => !isNaN(n));
|
|
1816
|
+
switch (v.aggregate) {
|
|
1817
|
+
case 'sum':
|
|
1818
|
+
subtotalValues[v.field] = nums.reduce((a, b) => a + b, 0);
|
|
1819
|
+
break;
|
|
1820
|
+
case 'average':
|
|
1821
|
+
subtotalValues[v.field] = nums.length ? nums.reduce((a, b) => a + b, 0) / nums.length : 0;
|
|
1822
|
+
break;
|
|
1823
|
+
case 'count':
|
|
1824
|
+
subtotalValues[v.field] = rowItems.length;
|
|
1825
|
+
break;
|
|
1826
|
+
case 'min':
|
|
1827
|
+
subtotalValues[v.field] = Math.min(...nums);
|
|
1828
|
+
break;
|
|
1829
|
+
case 'max':
|
|
1830
|
+
subtotalValues[v.field] = Math.max(...nums);
|
|
1831
|
+
break;
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
rowSection.subSections.push({
|
|
1835
|
+
title: `${rowKey} Subtotal`,
|
|
1836
|
+
level: level + 2,
|
|
1837
|
+
data: [subtotalValues],
|
|
1838
|
+
fields: dimensions.values.map(v => v.field)
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
this.addSection(rowSection);
|
|
1843
|
+
});
|
|
1844
|
+
// Add grand total
|
|
1845
|
+
if (showGrandTotals) {
|
|
1846
|
+
const grandTotalValues = {};
|
|
1847
|
+
dimensions.values.forEach(v => {
|
|
1848
|
+
const nums = data.map(item => item[v.field]).filter((n) => !isNaN(n));
|
|
1849
|
+
switch (v.aggregate) {
|
|
1850
|
+
case 'sum':
|
|
1851
|
+
grandTotalValues[v.field] = nums.reduce((a, b) => a + b, 0);
|
|
1852
|
+
break;
|
|
1853
|
+
case 'average':
|
|
1854
|
+
grandTotalValues[v.field] = nums.length ? nums.reduce((a, b) => a + b, 0) / nums.length : 0;
|
|
1855
|
+
break;
|
|
1856
|
+
case 'count':
|
|
1857
|
+
grandTotalValues[v.field] = data.length;
|
|
1858
|
+
break;
|
|
1859
|
+
case 'min':
|
|
1860
|
+
grandTotalValues[v.field] = Math.min(...nums);
|
|
1861
|
+
break;
|
|
1862
|
+
case 'max':
|
|
1863
|
+
grandTotalValues[v.field] = Math.max(...nums);
|
|
1864
|
+
break;
|
|
1865
|
+
}
|
|
1866
|
+
});
|
|
1867
|
+
this.addSection({
|
|
1868
|
+
title: 'Grand Total',
|
|
1869
|
+
level,
|
|
1870
|
+
data: [grandTotalValues],
|
|
1871
|
+
fields: dimensions.values.map(v => v.field)
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
return this;
|
|
1875
|
+
}
|
|
1876
|
+
/**
|
|
1877
|
+
* Add a timeline section (grouped by date periods)
|
|
1878
|
+
*/
|
|
1879
|
+
addTimelineSection(data, dateField, period, options) {
|
|
1880
|
+
const { fields, level = 0, showTrends = true, format } = options || {};
|
|
1881
|
+
// Group by date period
|
|
1882
|
+
const grouped = this.groupByDate(data, dateField, period, format);
|
|
1883
|
+
// Create sections for each period
|
|
1884
|
+
Object.entries(grouped).forEach(([periodKey, periodData]) => {
|
|
1885
|
+
const section = {
|
|
1886
|
+
title: periodKey,
|
|
1887
|
+
level: level + 1,
|
|
1888
|
+
collapsed: true,
|
|
1889
|
+
data: periodData,
|
|
1890
|
+
fields
|
|
1891
|
+
};
|
|
1892
|
+
// Add trend indicators if requested
|
|
1893
|
+
if (showTrends && periodData.length > 0) {
|
|
1894
|
+
const prevPeriod = this.getPreviousPeriod(periodKey, grouped);
|
|
1895
|
+
if (prevPeriod) {
|
|
1896
|
+
const trend = this.calculateTrend(periodData, prevPeriod, (fields === null || fields === void 0 ? void 0 : fields[0]) || 'value');
|
|
1897
|
+
section.summary = {
|
|
1898
|
+
fields: [(fields === null || fields === void 0 ? void 0 : fields[0]) || 'value'],
|
|
1899
|
+
function: 'sum',
|
|
1900
|
+
label: `Trend: ${trend > 0 ? '↑' : '↓'} ${Math.abs(trend).toFixed(1)}%`
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
this.addSection(section);
|
|
1905
|
+
});
|
|
1906
|
+
return this;
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* Add a filtered section
|
|
1910
|
+
*/
|
|
1911
|
+
addFilteredSection(config, filters) {
|
|
1912
|
+
const filteredData = this.applyFilters(config.data, filters);
|
|
1913
|
+
return this.addSection({
|
|
1914
|
+
...config,
|
|
1915
|
+
data: filteredData,
|
|
1916
|
+
title: `${config.title} (Filtered)`
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
/**
|
|
1920
|
+
* Add conditional formatting to the current section
|
|
1921
|
+
*/
|
|
1922
|
+
addConditionalFormat(rule) {
|
|
1923
|
+
this.conditionalFormats.push(rule);
|
|
1924
|
+
return this;
|
|
1925
|
+
}
|
|
1926
|
+
/**
|
|
1927
|
+
* Add a custom formatter for a field
|
|
1928
|
+
*/
|
|
1929
|
+
addFormatter(field, formatter) {
|
|
1930
|
+
this.formatters.set(field, formatter);
|
|
1931
|
+
return this;
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Register a reusable style
|
|
1935
|
+
*/
|
|
1936
|
+
registerStyle(name, style) {
|
|
1937
|
+
this.styles.set(name, style);
|
|
1938
|
+
return this;
|
|
1939
|
+
}
|
|
1940
|
+
/**
|
|
1941
|
+
* Apply a registered style
|
|
1942
|
+
*/
|
|
1943
|
+
applyStyle(styleName) {
|
|
1944
|
+
const style = this.styles.get(styleName);
|
|
1945
|
+
if (style) {
|
|
1946
|
+
const lastRow = this.worksheet.getRows().slice(-1)[0];
|
|
1947
|
+
if (lastRow) {
|
|
1948
|
+
lastRow.getCells().forEach(cell => cell.setStyle(style));
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
return this;
|
|
1952
|
+
}
|
|
1953
|
+
/**
|
|
1954
|
+
* Get the current row count
|
|
1955
|
+
*/
|
|
1956
|
+
getCurrentRow() {
|
|
1957
|
+
return this.currentRow;
|
|
1958
|
+
}
|
|
1959
|
+
/**
|
|
1960
|
+
* Reset the builder
|
|
1961
|
+
*/
|
|
1962
|
+
reset() {
|
|
1963
|
+
this.currentRow = 0;
|
|
1964
|
+
this.sections.clear();
|
|
1965
|
+
return this;
|
|
1966
|
+
}
|
|
1967
|
+
// Private helper methods
|
|
1968
|
+
groupData(data, groupBy) {
|
|
1969
|
+
return data.reduce((groups, item) => {
|
|
1970
|
+
const key = typeof groupBy === 'function'
|
|
1971
|
+
? groupBy(item)
|
|
1972
|
+
: String(item[groupBy]);
|
|
1973
|
+
if (!groups[key]) {
|
|
1974
|
+
groups[key] = [];
|
|
1975
|
+
}
|
|
1976
|
+
groups[key].push(item);
|
|
1977
|
+
return groups;
|
|
1978
|
+
}, {});
|
|
1979
|
+
}
|
|
1980
|
+
groupMultiLevel(data, dimensions) {
|
|
1981
|
+
return data.reduce((groups, item) => {
|
|
1982
|
+
const key = dimensions.map(dim => this.getNestedValue(item, dim)).join(' › ');
|
|
1983
|
+
if (!groups[key]) {
|
|
1984
|
+
groups[key] = [];
|
|
1985
|
+
}
|
|
1986
|
+
groups[key].push(item);
|
|
1987
|
+
return groups;
|
|
1988
|
+
}, {});
|
|
1989
|
+
}
|
|
1990
|
+
groupByDate(data, dateField, period, format) {
|
|
1991
|
+
return data.reduce((groups, item) => {
|
|
1992
|
+
const date = new Date(this.getNestedValue(item, dateField));
|
|
1993
|
+
let key;
|
|
1994
|
+
switch (period) {
|
|
1995
|
+
case 'day':
|
|
1996
|
+
key = format || date.toISOString().split('T')[0];
|
|
1997
|
+
break;
|
|
1998
|
+
case 'week': {
|
|
1999
|
+
const week = this.getWeekNumber(date);
|
|
2000
|
+
key = format || `${date.getFullYear()}-W${week}`;
|
|
2001
|
+
break;
|
|
2002
|
+
}
|
|
2003
|
+
case 'month':
|
|
2004
|
+
key = format || `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|
2005
|
+
break;
|
|
2006
|
+
case 'quarter': {
|
|
2007
|
+
const quarter = Math.floor(date.getMonth() / 3) + 1;
|
|
2008
|
+
key = format || `${date.getFullYear()}-Q${quarter}`;
|
|
2009
|
+
break;
|
|
2010
|
+
}
|
|
2011
|
+
case 'year':
|
|
2012
|
+
key = format || String(date.getFullYear());
|
|
2013
|
+
break;
|
|
2014
|
+
}
|
|
2015
|
+
if (!groups[key]) {
|
|
2016
|
+
groups[key] = [];
|
|
2017
|
+
}
|
|
2018
|
+
groups[key].push(item);
|
|
2019
|
+
return groups;
|
|
2020
|
+
}, {});
|
|
2021
|
+
}
|
|
2022
|
+
getWeekNumber(date) {
|
|
2023
|
+
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
|
2024
|
+
const dayNum = d.getUTCDay() || 7;
|
|
2025
|
+
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
|
2026
|
+
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
2027
|
+
return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
|
|
2028
|
+
}
|
|
2029
|
+
getPreviousPeriod(currentKey, groups) {
|
|
2030
|
+
const keys = Object.keys(groups).sort();
|
|
2031
|
+
const currentIndex = keys.indexOf(currentKey);
|
|
2032
|
+
if (currentIndex > 0) {
|
|
2033
|
+
return groups[keys[currentIndex - 1]];
|
|
2034
|
+
}
|
|
2035
|
+
return null;
|
|
2036
|
+
}
|
|
2037
|
+
calculateTrend(current, previous, field) {
|
|
2038
|
+
const currentSum = current.reduce((sum, item) => sum + (item[field] || 0), 0);
|
|
2039
|
+
const previousSum = previous.reduce((sum, item) => sum + (item[field] || 0), 0);
|
|
2040
|
+
if (previousSum === 0)
|
|
2041
|
+
return 0;
|
|
2042
|
+
return ((currentSum - previousSum) / previousSum) * 100;
|
|
2043
|
+
}
|
|
2044
|
+
applyFilters(data, filters) {
|
|
2045
|
+
return data.filter(item => {
|
|
2046
|
+
// Date range filter
|
|
2047
|
+
if (filters.dateRange) {
|
|
2048
|
+
const dateValue = new Date(this.getNestedValue(item, filters.dateRange.field || 'date'));
|
|
2049
|
+
const start = new Date(filters.dateRange.start);
|
|
2050
|
+
const end = new Date(filters.dateRange.end);
|
|
2051
|
+
if (dateValue < start || dateValue > end) {
|
|
2052
|
+
return false;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
// Field filters
|
|
2056
|
+
if (filters.filters) {
|
|
2057
|
+
for (const [field, values] of Object.entries(filters.filters)) {
|
|
2058
|
+
const itemValue = this.getNestedValue(item, field);
|
|
2059
|
+
const arrValues = values;
|
|
2060
|
+
if (arrValues.length > 0 && !arrValues.includes(itemValue)) {
|
|
2061
|
+
return false;
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
// Search filter
|
|
2066
|
+
if (filters.search) {
|
|
2067
|
+
const searchTerm = filters.search.toLowerCase();
|
|
2068
|
+
const matches = Object.values(item).some(val => String(val).toLowerCase().includes(searchTerm));
|
|
2069
|
+
if (!matches)
|
|
2070
|
+
return false;
|
|
2071
|
+
}
|
|
2072
|
+
// Custom filter
|
|
2073
|
+
if (filters.customFilter && !filters.customFilter(item)) {
|
|
2074
|
+
return false;
|
|
2075
|
+
}
|
|
2076
|
+
return true;
|
|
2077
|
+
}).slice(filters.offset || 0, (filters.offset || 0) + (filters.limit || Infinity));
|
|
2078
|
+
}
|
|
2079
|
+
getNestedValue(obj, path) {
|
|
2080
|
+
return path.split('.').reduce((current, key) => {
|
|
2081
|
+
return current && current[key] !== undefined ? current[key] : undefined;
|
|
2082
|
+
}, obj);
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
/**
|
|
2086
|
+
* Internal Section class for building collapsible sections
|
|
2087
|
+
*/
|
|
2088
|
+
class Section {
|
|
2089
|
+
constructor(worksheet, config, startRow) {
|
|
2090
|
+
this.subSections = [];
|
|
2091
|
+
this.worksheet = worksheet;
|
|
2092
|
+
this.config = config;
|
|
2093
|
+
this.startRow = startRow;
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* Build the section in the worksheet
|
|
2097
|
+
*/
|
|
2098
|
+
build() {
|
|
2099
|
+
let currentRow = this.startRow;
|
|
2100
|
+
// Add section header
|
|
2101
|
+
if (this.config.title) {
|
|
2102
|
+
const headerRow = this.worksheet.createRow();
|
|
2103
|
+
headerRow.setOutlineLevel(this.config.level);
|
|
2104
|
+
const headerCell = headerRow.createCell(this.config.title);
|
|
2105
|
+
// Apply header style
|
|
2106
|
+
const headerStyle = this.config.headerStyle ||
|
|
2107
|
+
StyleBuilder.create()
|
|
2108
|
+
.bold(true)
|
|
2109
|
+
.fontSize(12)
|
|
2110
|
+
.backgroundColor(this.getHeaderColor(this.config.level))
|
|
2111
|
+
.borderAll('thin')
|
|
2112
|
+
.build();
|
|
2113
|
+
headerCell.setStyle(headerStyle);
|
|
2114
|
+
// Add collapse/expand indicator if section has data or subsections
|
|
2115
|
+
if (this.hasContent()) {
|
|
2116
|
+
const indicatorCell = headerRow.createCell(this.config.collapsed ? '▶' : '▼');
|
|
2117
|
+
indicatorCell.setStyle(StyleBuilder.create()
|
|
2118
|
+
.bold(true)
|
|
2119
|
+
.color('#666666')
|
|
2120
|
+
.build());
|
|
2121
|
+
}
|
|
2122
|
+
currentRow++;
|
|
2123
|
+
}
|
|
2124
|
+
// Add section data if not collapsed
|
|
2125
|
+
if (!this.config.collapsed && this.config.data.length > 0) {
|
|
2126
|
+
// Add headers if fields are specified
|
|
2127
|
+
if (this.config.fields && this.config.fields.length > 0) {
|
|
2128
|
+
const headerRow = this.worksheet.createRow();
|
|
2129
|
+
headerRow.setOutlineLevel(this.config.level + 1);
|
|
2130
|
+
this.config.fields.forEach((field) => {
|
|
2131
|
+
var _a;
|
|
2132
|
+
const label = ((_a = this.config.fieldLabels) === null || _a === void 0 ? void 0 : _a[field]) || field;
|
|
2133
|
+
const cell = headerRow.createCell(label);
|
|
2134
|
+
cell.setStyle(StyleBuilder.create()
|
|
2135
|
+
.bold(true)
|
|
2136
|
+
.backgroundColor('#e6e6e6')
|
|
2137
|
+
.borderAll('thin')
|
|
2138
|
+
.build());
|
|
2139
|
+
});
|
|
2140
|
+
currentRow++;
|
|
2141
|
+
}
|
|
2142
|
+
// Add data rows
|
|
2143
|
+
this.config.data.forEach((item) => {
|
|
2144
|
+
const dataRow = this.worksheet.createRow();
|
|
2145
|
+
dataRow.setOutlineLevel(this.config.level + 1);
|
|
2146
|
+
const fields = this.config.fields || Object.keys(item);
|
|
2147
|
+
fields.forEach((field) => {
|
|
2148
|
+
const value = this.getNestedValue(item, field);
|
|
2149
|
+
const cell = dataRow.createCell(value);
|
|
2150
|
+
// Apply conditional formatting if any
|
|
2151
|
+
if (this.config.conditionalStyles) {
|
|
2152
|
+
this.applyConditionalStyles(cell, item, field);
|
|
2153
|
+
}
|
|
2154
|
+
// Apply field-specific style
|
|
2155
|
+
if (this.config.style) {
|
|
2156
|
+
cell.setStyle(this.config.style);
|
|
2157
|
+
}
|
|
2158
|
+
});
|
|
2159
|
+
currentRow++;
|
|
2160
|
+
});
|
|
2161
|
+
// Add summary row if configured
|
|
2162
|
+
if (this.config.summary) {
|
|
2163
|
+
this.addSummaryRow();
|
|
2164
|
+
currentRow++;
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
// Build sub-sections
|
|
2168
|
+
if (this.config.subSections && !this.config.collapsed) {
|
|
2169
|
+
this.config.subSections.forEach((subConfig) => {
|
|
2170
|
+
const subSection = new Section(this.worksheet, subConfig, currentRow);
|
|
2171
|
+
this.subSections.push(subSection);
|
|
2172
|
+
currentRow = subSection.build();
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
return currentRow;
|
|
2176
|
+
}
|
|
2177
|
+
/**
|
|
2178
|
+
* Add a sub-section
|
|
2179
|
+
*/
|
|
2180
|
+
addSubSection(config) {
|
|
2181
|
+
if (!this.config.subSections) {
|
|
2182
|
+
this.config.subSections = [];
|
|
2183
|
+
}
|
|
2184
|
+
this.config.subSections.push(config);
|
|
2185
|
+
}
|
|
2186
|
+
/**
|
|
2187
|
+
* Check if section has any content
|
|
2188
|
+
*/
|
|
2189
|
+
hasContent() {
|
|
2190
|
+
return this.config.data.length > 0 ||
|
|
2191
|
+
(this.config.subSections && this.config.subSections.length > 0);
|
|
2192
|
+
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Get header color based on outline level
|
|
2195
|
+
*/
|
|
2196
|
+
getHeaderColor(level) {
|
|
2197
|
+
const colors = [
|
|
2198
|
+
'#e6f2ff', // Level 0 - Light blue
|
|
2199
|
+
'#f0f0f0', // Level 1 - Light gray
|
|
2200
|
+
'#f9f9f9', // Level 2 - Lighter gray
|
|
2201
|
+
'#ffffff', // Level 3+ - White
|
|
2202
|
+
];
|
|
2203
|
+
return colors[Math.min(level, colors.length - 1)];
|
|
2204
|
+
}
|
|
2205
|
+
/**
|
|
2206
|
+
* Add summary row (totals, averages, etc.)
|
|
2207
|
+
*/
|
|
2208
|
+
addSummaryRow() {
|
|
2209
|
+
if (!this.config.summary)
|
|
2210
|
+
return;
|
|
2211
|
+
const summaryRow = this.worksheet.createRow();
|
|
2212
|
+
summaryRow.setOutlineLevel(this.config.level + 2);
|
|
2213
|
+
// Add label cell
|
|
2214
|
+
const labelCell = summaryRow.createCell(this.config.summary.label || 'Total');
|
|
2215
|
+
labelCell.setStyle(StyleBuilder.create()
|
|
2216
|
+
.bold(true)
|
|
2217
|
+
.italic(true)
|
|
2218
|
+
.backgroundColor('#f5f5f5')
|
|
2219
|
+
.build());
|
|
2220
|
+
// Calculate and add summary values
|
|
2221
|
+
const fields = this.config.fields || [];
|
|
2222
|
+
fields.forEach((field) => {
|
|
2223
|
+
if (this.config.summary.fields.includes(field)) {
|
|
2224
|
+
const values = this.config.data
|
|
2225
|
+
.map((item) => this.getNestedValue(item, field))
|
|
2226
|
+
.filter((v) => v != null && !isNaN(v));
|
|
2227
|
+
let result;
|
|
2228
|
+
switch (this.config.summary.function) {
|
|
2229
|
+
case 'sum':
|
|
2230
|
+
result = values.reduce((a, b) => a + b, 0);
|
|
2231
|
+
break;
|
|
2232
|
+
case 'average':
|
|
2233
|
+
result = values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0;
|
|
2234
|
+
break;
|
|
2235
|
+
case 'count':
|
|
2236
|
+
result = values.length;
|
|
2237
|
+
break;
|
|
2238
|
+
case 'min':
|
|
2239
|
+
result = Math.min(...values);
|
|
2240
|
+
break;
|
|
2241
|
+
case 'max':
|
|
2242
|
+
result = Math.max(...values);
|
|
2243
|
+
break;
|
|
2244
|
+
default:
|
|
2245
|
+
result = 0;
|
|
2246
|
+
}
|
|
2247
|
+
const cell = summaryRow.createCell(result);
|
|
2248
|
+
cell.setStyle(StyleBuilder.create()
|
|
2249
|
+
.bold(true)
|
|
2250
|
+
.numberFormat(this.config.summary.function === 'count' ? '#,##0' : '#,##0.00')
|
|
2251
|
+
.build());
|
|
2252
|
+
}
|
|
2253
|
+
else {
|
|
2254
|
+
summaryRow.createCell('');
|
|
2255
|
+
}
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
/**
|
|
2259
|
+
* Apply conditional formatting to a cell
|
|
2260
|
+
*/
|
|
2261
|
+
applyConditionalStyles(cell, item, field) {
|
|
2262
|
+
if (!this.config.conditionalStyles)
|
|
2263
|
+
return;
|
|
2264
|
+
for (const rule of this.config.conditionalStyles) {
|
|
2265
|
+
if (rule.field === field && rule.condition(item)) {
|
|
2266
|
+
cell.setStyle(rule.style);
|
|
2267
|
+
break;
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
/**
|
|
2272
|
+
* Get nested value from object
|
|
2273
|
+
*/
|
|
2274
|
+
getNestedValue(obj, path) {
|
|
2275
|
+
return path.split('.').reduce((current, key) => {
|
|
2276
|
+
return current && current[key] !== undefined ? current[key] : undefined;
|
|
2277
|
+
}, obj);
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
/**
|
|
2282
|
+
* Format currency values
|
|
2283
|
+
*/
|
|
2284
|
+
const formatCurrency = (value) => {
|
|
2285
|
+
return new Intl.NumberFormat('en-US', {
|
|
2286
|
+
style: 'currency',
|
|
2287
|
+
currency: 'USD',
|
|
2288
|
+
minimumFractionDigits: 2,
|
|
2289
|
+
maximumFractionDigits: 2,
|
|
2290
|
+
}).format(value);
|
|
2291
|
+
};
|
|
2292
|
+
/**
|
|
2293
|
+
* Format date values
|
|
2294
|
+
*/
|
|
2295
|
+
const formatDate = (date) => {
|
|
2296
|
+
const d = typeof date === 'string' ? new Date(date) : date;
|
|
2297
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
2298
|
+
year: 'numeric',
|
|
2299
|
+
month: 'short',
|
|
2300
|
+
day: 'numeric',
|
|
2301
|
+
}).format(d);
|
|
2302
|
+
};
|
|
2303
|
+
/**
|
|
2304
|
+
* Format number with commas
|
|
2305
|
+
*/
|
|
2306
|
+
const formatNumber = (num) => {
|
|
2307
|
+
return new Intl.NumberFormat('en-US').format(num);
|
|
2308
|
+
};
|
|
2309
|
+
/**
|
|
2310
|
+
* Truncate text with ellipsis
|
|
2311
|
+
*/
|
|
2312
|
+
const truncateText = (text, maxLength) => {
|
|
2313
|
+
if (text.length <= maxLength)
|
|
2314
|
+
return text;
|
|
2315
|
+
return text.slice(0, maxLength) + '...';
|
|
2316
|
+
};
|
|
2317
|
+
/**
|
|
2318
|
+
* Generate random ID
|
|
2319
|
+
*/
|
|
2320
|
+
const generateId = () => {
|
|
2321
|
+
return Math.random().toString(36).substring(2, 15) +
|
|
2322
|
+
Math.random().toString(36).substring(2, 15);
|
|
2323
|
+
};
|
|
2324
|
+
/**
|
|
2325
|
+
* Debounce function
|
|
2326
|
+
*/
|
|
2327
|
+
const debounce = (func, wait) => {
|
|
2328
|
+
let timeout;
|
|
2329
|
+
return (...args) => {
|
|
2330
|
+
clearTimeout(timeout);
|
|
2331
|
+
timeout = setTimeout(() => func(...args), wait);
|
|
2332
|
+
};
|
|
2333
|
+
};
|
|
2334
|
+
/**
|
|
2335
|
+
* Group array by key
|
|
2336
|
+
*/
|
|
2337
|
+
const groupBy = (array, key) => {
|
|
2338
|
+
return array.reduce((result, item) => {
|
|
2339
|
+
const groupKey = String(item[key]);
|
|
2340
|
+
if (!result[groupKey]) {
|
|
2341
|
+
result[groupKey] = [];
|
|
2342
|
+
}
|
|
2343
|
+
result[groupKey].push(item);
|
|
2344
|
+
return result;
|
|
2345
|
+
}, {});
|
|
2346
|
+
};
|
|
2347
|
+
/**
|
|
2348
|
+
* Calculate percentage
|
|
2349
|
+
*/
|
|
2350
|
+
const calculatePercentage = (value, total) => {
|
|
2351
|
+
if (total === 0)
|
|
2352
|
+
return 0;
|
|
2353
|
+
return (value / total) * 100;
|
|
2354
|
+
};
|
|
2355
|
+
/**
|
|
2356
|
+
* Download file
|
|
2357
|
+
*/
|
|
2358
|
+
const downloadFile = (content, filename, type) => {
|
|
2359
|
+
const blob = new Blob([content], { type });
|
|
2360
|
+
const url = window.URL.createObjectURL(blob);
|
|
2361
|
+
const link = document.createElement('a');
|
|
2362
|
+
link.href = url;
|
|
2363
|
+
link.download = filename;
|
|
2364
|
+
link.click();
|
|
2365
|
+
window.URL.revokeObjectURL(url);
|
|
2366
|
+
};
|
|
2367
|
+
|
|
2368
|
+
// Core exports
|
|
2369
|
+
function exportToExcel(data, options) {
|
|
2370
|
+
const builder = ExportBuilder.create((options === null || options === void 0 ? void 0 : options.sheetName) || 'Sheet1');
|
|
2371
|
+
if (options === null || options === void 0 ? void 0 : options.headers) {
|
|
2372
|
+
builder.addHeaderRow(options.headers);
|
|
2373
|
+
}
|
|
2374
|
+
builder.addDataRows(data, options === null || options === void 0 ? void 0 : options.fields);
|
|
2375
|
+
if (options === null || options === void 0 ? void 0 : options.columnWidths) {
|
|
2376
|
+
builder.setColumnWidths(options.columnWidths);
|
|
2377
|
+
}
|
|
2378
|
+
return builder.build();
|
|
2379
|
+
}
|
|
2380
|
+
function exportToCSV(data, options) {
|
|
2381
|
+
// Simplified CSV export
|
|
2382
|
+
const headers = (options === null || options === void 0 ? void 0 : options.headers) || Object.keys(data[0] || {});
|
|
2383
|
+
const rows = [headers];
|
|
2384
|
+
data.forEach(item => {
|
|
2385
|
+
const row = headers.map((header) => item[header] || '');
|
|
2386
|
+
rows.push(row);
|
|
2387
|
+
});
|
|
2388
|
+
return rows.map(row => row.map((cell) => String(cell).includes(',') ? `"${cell}"` : cell).join(',')).join('\n');
|
|
2389
|
+
}
|
|
2390
|
+
// Version
|
|
2391
|
+
const VERSION = '1.0.0';
|
|
2392
|
+
|
|
2393
|
+
exports.CSVWriter = CSVWriter;
|
|
2394
|
+
exports.Cell = Cell;
|
|
2395
|
+
exports.Column = Column;
|
|
2396
|
+
exports.DateFormatter = DateFormatter;
|
|
2397
|
+
exports.ExcelWriter = ExcelWriter;
|
|
2398
|
+
exports.ExportBuilder = ExportBuilder;
|
|
2399
|
+
exports.JSONWriter = JSONWriter;
|
|
2400
|
+
exports.NumberFormatter = NumberFormatter;
|
|
2401
|
+
exports.Row = Row;
|
|
2402
|
+
exports.SectionBuilder = SectionBuilder;
|
|
2403
|
+
exports.SheetBuilder = SheetBuilder;
|
|
2404
|
+
exports.StyleBuilder = StyleBuilder;
|
|
2405
|
+
exports.VERSION = VERSION;
|
|
2406
|
+
exports.Workbook = Workbook;
|
|
2407
|
+
exports.Worksheet = Worksheet;
|
|
2408
|
+
exports.calculatePercentage = calculatePercentage;
|
|
2409
|
+
exports.debounce = debounce;
|
|
2410
|
+
exports.downloadFile = downloadFile;
|
|
2411
|
+
exports.exportToCSV = exportToCSV;
|
|
2412
|
+
exports.exportToExcel = exportToExcel;
|
|
2413
|
+
exports.formatCurrency = formatCurrency;
|
|
2414
|
+
exports.formatDate = formatDate;
|
|
2415
|
+
exports.formatNumber = formatNumber;
|
|
2416
|
+
exports.generateId = generateId;
|
|
2417
|
+
exports.groupBy = groupBy;
|
|
2418
|
+
exports.truncateText = truncateText;
|
|
2419
|
+
//# sourceMappingURL=index.js.map
|