spotlibs-components 0.1.10 → 0.1.11
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/dist/{chunk-OGJBPTUP.mjs → chunk-F6KSEYVB.mjs} +3 -3
- package/dist/{chunk-OGJBPTUP.mjs.map → chunk-F6KSEYVB.mjs.map} +1 -1
- package/dist/chunk-UFE7HFT2.mjs +60 -0
- package/dist/chunk-UFE7HFT2.mjs.map +1 -0
- package/dist/{chunk-NXFLMH2Q.mjs → chunk-YOSPWY5K.mjs} +3 -4
- package/dist/{chunk-NXFLMH2Q.mjs.map → chunk-YOSPWY5K.mjs.map} +1 -1
- package/dist/{atoms → components/atoms}/icons/index.mjs +2 -2
- package/dist/components/index.d.mts +449 -0
- package/dist/components/index.mjs +7241 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/{mui → components/mui}/index.mjs +1 -1
- package/dist/components/types.d.mts +4 -0
- package/dist/{types-B0iF5xX8.d.mts → types-BkrxwBFm.d.mts} +329 -178
- package/dist/utils/index.d.mts +186 -0
- package/dist/utils/index.mjs +477 -0
- package/dist/utils/index.mjs.map +1 -0
- package/package.json +12 -2
- package/src/utils/admin-kuota-nasional/constant.js +95 -0
- package/src/utils/asset.js +10 -0
- package/src/utils/booleanUtils.js +19 -0
- package/src/utils/briguna-digital/constant.js +12 -0
- package/src/utils/compress.js +24 -0
- package/src/utils/compressImage.js +20 -0
- package/src/utils/constants.js +20 -0
- package/src/utils/cookies.js +102 -0
- package/src/utils/crypto.js +104 -0
- package/src/utils/decodeString.js +17 -0
- package/src/utils/disableFormElements.js +19 -0
- package/src/utils/excelUtils.js +60 -0
- package/src/utils/exception.js +86 -0
- package/src/utils/formatString.js +151 -0
- package/src/utils/formatters/account.js +98 -0
- package/src/utils/formatters/currency.js +125 -0
- package/src/utils/formatters/date.js +219 -0
- package/src/utils/formatters/index.js +47 -0
- package/src/utils/formatters/string.js +114 -0
- package/src/utils/generateImportExcel.js +546 -0
- package/src/utils/generateTimestamp.js +4 -0
- package/src/utils/handleApiError.js +17 -0
- package/src/utils/index.js +92 -0
- package/src/utils/json.js +55 -0
- package/src/utils/localStorage.js +8 -0
- package/src/utils/logger.js +46 -0
- package/src/utils/schema/globalSchema.js +10 -0
- package/src/utils/signature.js +75 -0
- package/src/utils/specifics/korporasi/prakarsaKorporasiUtils.js +87 -0
- package/src/utils/specifics/mikro/validationMaintenanceAgunanUtils.js +214 -0
- package/src/utils/static-data/korporasi/prakarsa/analisa-risiko-kredit-nasabah/analisa-kinerja-keuangan/listStaticData.js +290 -0
- package/src/utils/static-data/korporasi/prakarsa/appendix/proyeksi-laporan-arus-kas/listStaticData.js +7 -0
- package/src/utils/static-data/korporasi/prakarsa/appendix/proyeksi-laporan-keuangan/listStaticData.js +75 -0
- package/src/utils/static-data/korporasi/prakarsa/appendix/total-exposure-group/listStaticData.js +7 -0
- package/src/utils/static-data/korporasi/prakarsa/create/listStaticData.js +21 -0
- package/src/utils/static-data/korporasi/prakarsa/generals/listExcelConfigData.js +11 -0
- package/src/utils/static-data/korporasi/prakarsa/pengajuan-fasilitas-kredit/fasilitas-kredit/listStaticData.js +69 -0
- package/src/utils/static-data/korporasi-internasional/eksternal/listStaticData.js +193 -0
- package/src/utils/static-data/korporasi-internasional/master/listStaticData.js +24 -0
- package/src/utils/static-data/korporasi-internasional/uji-kepatuhan/listStaticData.js +250 -0
- package/src/utils/status.js +160 -0
- package/src/utils/store/adminKuotaNasional.js +7 -0
- package/src/utils/store/authStore.js +13 -0
- package/src/utils/store/bankGaransiStore.js +10 -0
- package/src/utils/store/korporasi/approval/interface/interfaceStore.js +10 -0
- package/src/utils/store/korporasi/general/generalKorporasiStore.js +13 -0
- package/src/utils/store/korporasi/loan-disbursement/loanDisbursementStore.js +77 -0
- package/src/utils/store/korporasi/prakarsa/fasilitas-kredit/fasilitasKreditStore.js +141 -0
- package/src/utils/store/migrasi-mandiri/migrasiMandiriStore.js +53 -0
- package/src/utils/store/monitoring-konsumer/monitoring-leads-KPP/monitoring-leads-kpp-internal.js +34 -0
- package/src/utils/store/prakarsaBankGaransiStore.js +21 -0
- package/src/utils/store/prognosa-crr/settingPrognosaROStore.js +9 -0
- package/src/utils/store/prognosaRMStore.js +30 -0
- package/src/utils/store/restrukStore.js +26 -0
- package/src/utils/store/verificationAccessLink.js +11 -0
- package/src/utils/store/warkat-bg-wholesale/warkatBgWholesaleStore.js +22 -0
- package/src/utils/stringUtils.js +226 -0
- package/src/utils/tipeBank.js +14 -0
- package/dist/index.css +0 -32
- package/dist/index.css.map +0 -1
- package/dist/index.d.mts +0 -1726
- package/dist/index.mjs +0 -18560
- package/dist/index.mjs.map +0 -1
- package/dist/types.d.mts +0 -4
- /package/dist/{atoms → components/atoms}/icons/index.d.mts +0 -0
- /package/dist/{atoms → components/atoms}/icons/index.mjs.map +0 -0
- /package/dist/{mui → components/mui}/index.d.mts +0 -0
- /package/dist/{mui → components/mui}/index.mjs.map +0 -0
- /package/dist/{types.mjs → components/types.mjs} +0 -0
- /package/dist/{types.mjs.map → components/types.mjs.map} +0 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
/* eslint-disable no-undef */
|
|
2
|
+
import ExcelJS from 'exceljs';
|
|
3
|
+
import * as XLSX from 'xlsx';
|
|
4
|
+
import { Decimal } from 'decimal.js';
|
|
5
|
+
import { ModalErrorUtil } from '@/helpers/modal';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate and download template excel with dynamic configuration
|
|
9
|
+
* @param {Object} config - Configuration object for generating Excel
|
|
10
|
+
* @param {number} [config.year=new Date().getFullYear()] - Year
|
|
11
|
+
* @param {Array} config.data - Array of data to be rendered
|
|
12
|
+
* @param {Array<string>} [config.headers1=[]] - First row headers
|
|
13
|
+
* @param {Array<string>} [config.headers2=[]] - Second row headers
|
|
14
|
+
* @param {string} [config.title=''] - Title of the document
|
|
15
|
+
* @param {Array<number>} [config.disabledMonths=[]] - Array of months to disable (1-12)
|
|
16
|
+
* @param {number} [config.startDataRow=3] - Starting row index for data
|
|
17
|
+
* @param {boolean} [config.isLocked=false] - Whether to enable month-based cell locking
|
|
18
|
+
* @param {string} [config.password=''] - Password for protection
|
|
19
|
+
* @param {number} [config.freezeColumns=3] - Number of columns to freeze
|
|
20
|
+
* @param {boolean} [config.freezeHeader=false] - Whether to freeze header rows
|
|
21
|
+
* @param {string} [config.filename='Export'] - Output filename
|
|
22
|
+
* @param {boolean} [config.isTotalCell=false] - Whether to add total column
|
|
23
|
+
* @param {Object} [config.mappings] - Object mapping for data to excel columns
|
|
24
|
+
*/
|
|
25
|
+
export async function generateAnnualTemplateExcel({
|
|
26
|
+
year = new Date().getFullYear(),
|
|
27
|
+
data = [],
|
|
28
|
+
headers1 = [],
|
|
29
|
+
headers2 = [],
|
|
30
|
+
title = '',
|
|
31
|
+
disabledMonths = [],
|
|
32
|
+
startDataRow = 3,
|
|
33
|
+
isLocked = false,
|
|
34
|
+
password = '',
|
|
35
|
+
freezeColumns = 3,
|
|
36
|
+
freezeHeader = false,
|
|
37
|
+
filename = 'Export',
|
|
38
|
+
isTotalCell = false,
|
|
39
|
+
mappings = {}
|
|
40
|
+
}) {
|
|
41
|
+
const workbook = new ExcelJS.Workbook();
|
|
42
|
+
const sheet = workbook.addWorksheet(title || `Export ${year}`);
|
|
43
|
+
|
|
44
|
+
const COLOR_HEADER = "A6C8EC";
|
|
45
|
+
const COLOR_VALUE_ID = "D9D9D9";
|
|
46
|
+
const COLOR_WHITE = "FFFFFFFF";
|
|
47
|
+
const FONT_DEFAULT = { name: "Arial", size: 11 };
|
|
48
|
+
|
|
49
|
+
// Add headers
|
|
50
|
+
if (headers1.length > 0) {
|
|
51
|
+
sheet.addRow(headers1);
|
|
52
|
+
}
|
|
53
|
+
if (headers2.length > 0) {
|
|
54
|
+
sheet.addRow(headers2);
|
|
55
|
+
} else if (headers1.length > 0) {
|
|
56
|
+
// If no second header row, add empty row for vertical merging
|
|
57
|
+
sheet.addRow(Array(headers1.length).fill(''));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Handle header merging
|
|
61
|
+
if (headers1.length > 0) {
|
|
62
|
+
// Process each column
|
|
63
|
+
for (let col = 0; col < headers1.length; col++) {
|
|
64
|
+
const header1 = headers1[col];
|
|
65
|
+
const header2 = headers2[col] || '';
|
|
66
|
+
|
|
67
|
+
// Skip empty header1 cells
|
|
68
|
+
if (header1 === '') continue;
|
|
69
|
+
|
|
70
|
+
// Case 1: Vertical merge when header2 is empty or doesn't exist
|
|
71
|
+
if (header2 === '') {
|
|
72
|
+
try {
|
|
73
|
+
sheet.mergeCells(1, col + 1, 2, col + 1);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
// Skip if already merged
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Case 2: Horizontal merge in header1 if next cells are empty
|
|
80
|
+
let span = 1;
|
|
81
|
+
while (col + span < headers1.length && headers1[col + span] === '') {
|
|
82
|
+
span++;
|
|
83
|
+
}
|
|
84
|
+
if (span > 1) {
|
|
85
|
+
try {
|
|
86
|
+
sheet.mergeCells(1, col + 1, 1, col + span);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
// Skip if already merged
|
|
89
|
+
}
|
|
90
|
+
col += span - 1; // Skip the merged columns in next iteration
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Set title cell if there are headers
|
|
96
|
+
if (headers1.length > 0) {
|
|
97
|
+
// Find the range for title based on the month columns (empty strings in headers1)
|
|
98
|
+
const monthStartCol = headers1.findIndex(h => h === '') + 1;
|
|
99
|
+
// Count consecutive empty strings to determine the month span
|
|
100
|
+
let monthSpan = 0;
|
|
101
|
+
for (let i = monthStartCol - 1; i < headers1.length; i++) {
|
|
102
|
+
if (headers1[i] === '') monthSpan++;
|
|
103
|
+
else break;
|
|
104
|
+
}
|
|
105
|
+
// Title should span across all month columns
|
|
106
|
+
const lastCol = monthStartCol + monthSpan - 1;
|
|
107
|
+
|
|
108
|
+
// Unmerge any existing merged cells in the range before merging
|
|
109
|
+
const startCell = sheet.getCell(1, monthStartCol);
|
|
110
|
+
const endCell = sheet.getCell(1, lastCol);
|
|
111
|
+
const titleRange = `${startCell._address}:${endCell._address}`;
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
// Try to unmerge first to prevent errors
|
|
115
|
+
sheet.unMergeCells(titleRange);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
// Ignore if cells weren't merged
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
sheet.mergeCells(titleRange);
|
|
121
|
+
const titleCell = sheet.getCell(1, monthStartCol);
|
|
122
|
+
titleCell.value = title || `Export Data Tahun ${year}`;
|
|
123
|
+
titleCell.font = { ...FONT_DEFAULT, bold: true };
|
|
124
|
+
titleCell.alignment = { horizontal: "center", vertical: "middle" };
|
|
125
|
+
titleCell.fill = {
|
|
126
|
+
type: "pattern",
|
|
127
|
+
pattern: "solid",
|
|
128
|
+
fgColor: { argb: COLOR_HEADER }
|
|
129
|
+
};
|
|
130
|
+
titleCell.border = {
|
|
131
|
+
top: { style: "thin" },
|
|
132
|
+
left: { style: "thin" },
|
|
133
|
+
bottom: { style: "thin" },
|
|
134
|
+
right: { style: "thin" }
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Style all headers
|
|
139
|
+
[1, 2].forEach(row => {
|
|
140
|
+
if ((row === 1 && headers1.length > 0) || (row === 2 && headers2.length > 0)) {
|
|
141
|
+
// Use headers1 length as the column count since it includes all columns
|
|
142
|
+
const totalColumns = headers1.length;
|
|
143
|
+
for (let col = 1; col <= totalColumns; col++) {
|
|
144
|
+
const cell = sheet.getRow(row).getCell(col);
|
|
145
|
+
if (cell.value !== null) {
|
|
146
|
+
cell.font = { ...FONT_DEFAULT, bold: true };
|
|
147
|
+
cell.fill = {
|
|
148
|
+
type: "pattern",
|
|
149
|
+
pattern: "solid",
|
|
150
|
+
fgColor: { argb: COLOR_HEADER }
|
|
151
|
+
};
|
|
152
|
+
cell.alignment = { horizontal: "center", vertical: "middle" };
|
|
153
|
+
cell.border = {
|
|
154
|
+
top: { style: "thin" },
|
|
155
|
+
left: { style: "thin" },
|
|
156
|
+
bottom: { style: "thin" },
|
|
157
|
+
right: { style: "thin" }
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Add data rows
|
|
165
|
+
data.forEach((item, idx) => {
|
|
166
|
+
const rowIdx = idx + startDataRow;
|
|
167
|
+
const row = sheet.getRow(rowIdx);
|
|
168
|
+
|
|
169
|
+
// Map data to columns based on mappings, but skip 'no' mapping since we handle it directly
|
|
170
|
+
Object.entries(mappings).forEach(([key, colIndex]) => {
|
|
171
|
+
if (key !== 'no') { // Skip 'no' mapping since we handle it manually
|
|
172
|
+
row.getCell(colIndex).value = item[key] || '';
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Set row number explicitly
|
|
177
|
+
row.getCell(1).value = idx + 1;
|
|
178
|
+
|
|
179
|
+
// If monthly data exists in mapping
|
|
180
|
+
const monthlyData = Array(12).fill(0);
|
|
181
|
+
for (let i = 0; i < 12; i++) {
|
|
182
|
+
const monthKey = `month_${i + 1}`;
|
|
183
|
+
if (item[monthKey] !== undefined) {
|
|
184
|
+
monthlyData[i] = item[monthKey];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Add monthly values
|
|
189
|
+
for (let i = 0; i < 12; i++) {
|
|
190
|
+
const cell = row.getCell(i + Object.keys(mappings).length + 1);
|
|
191
|
+
cell.value = monthlyData[i];
|
|
192
|
+
cell.numFmt = "#,##0.00";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Add total if required
|
|
196
|
+
if (isTotalCell) {
|
|
197
|
+
const totalCell = row.getCell(Object.keys(mappings).length + 13);
|
|
198
|
+
totalCell.value = { formula: `SUM(${row.getCell(Object.keys(mappings).length + 1)._address}:${row.getCell(Object.keys(mappings).length + 12)._address})` };
|
|
199
|
+
totalCell.numFmt = "#,##0.00";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
row.commit();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Style data rows
|
|
206
|
+
const totalColumns = headers1.length; // Use headers1 length as it includes all columns including Total
|
|
207
|
+
for (let r = startDataRow; r < startDataRow + data.length; r++) {
|
|
208
|
+
for (let c = 1; c <= totalColumns; c++) {
|
|
209
|
+
const cell = sheet.getRow(r).getCell(c);
|
|
210
|
+
cell.font = { ...FONT_DEFAULT };
|
|
211
|
+
|
|
212
|
+
// Style based on column type
|
|
213
|
+
const isMonthColumn = c > Object.keys(mappings).length && c <= Object.keys(mappings).length + 12;
|
|
214
|
+
const isTotalColumn = isTotalCell && c === totalColumns;
|
|
215
|
+
|
|
216
|
+
if (isMonthColumn) {
|
|
217
|
+
const monthIndex = c - Object.keys(mappings).length;
|
|
218
|
+
|
|
219
|
+
// Apply the new disabled months logic
|
|
220
|
+
let cellIsLocked = false;
|
|
221
|
+
if (isLocked) {
|
|
222
|
+
// Use the directly provided disabledMonths array
|
|
223
|
+
cellIsLocked = disabledMonths.includes(monthIndex);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
cell.fill = {
|
|
227
|
+
type: "pattern",
|
|
228
|
+
pattern: "solid",
|
|
229
|
+
fgColor: { argb: cellIsLocked ? COLOR_VALUE_ID : COLOR_WHITE }
|
|
230
|
+
};
|
|
231
|
+
cell.protection = { locked: cellIsLocked };
|
|
232
|
+
cell.alignment = { horizontal: "right" };
|
|
233
|
+
} else if (isTotalColumn) {
|
|
234
|
+
cell.fill = {
|
|
235
|
+
type: "pattern",
|
|
236
|
+
pattern: "solid",
|
|
237
|
+
fgColor: { argb: COLOR_VALUE_ID }
|
|
238
|
+
};
|
|
239
|
+
cell.protection = { locked: true };
|
|
240
|
+
cell.alignment = { horizontal: "right" };
|
|
241
|
+
} else {
|
|
242
|
+
cell.fill = {
|
|
243
|
+
type: "pattern",
|
|
244
|
+
pattern: "solid",
|
|
245
|
+
fgColor: { argb: COLOR_VALUE_ID }
|
|
246
|
+
};
|
|
247
|
+
cell.protection = { locked: true };
|
|
248
|
+
cell.alignment = { horizontal: c === 1 ? "center" : "left" };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
cell.border = {
|
|
252
|
+
top: { style: "thin" },
|
|
253
|
+
left: { style: "thin" },
|
|
254
|
+
bottom: { style: "thin" },
|
|
255
|
+
right: { style: "thin" }
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Set column widths
|
|
261
|
+
const getMaxWidth = (arr) => Math.max(...arr.map(v => v ? v.toString().length : 0));
|
|
262
|
+
|
|
263
|
+
// For mapped columns
|
|
264
|
+
Object.entries(mappings).forEach(([key, colIndex]) => {
|
|
265
|
+
const values = data.map(item => item[key]);
|
|
266
|
+
const headerText = headers1[colIndex - 1] || headers2[colIndex - 1] || key;
|
|
267
|
+
sheet.getColumn(colIndex).width = getMaxWidth([headerText, ...values]) + 5;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// For month columns and total
|
|
271
|
+
for (let i = Object.keys(mappings).length + 1; i <= totalColumns; i++) {
|
|
272
|
+
const isTotalColumn = isTotalCell && i === totalColumns;
|
|
273
|
+
sheet.getColumn(i).width = isTotalColumn ? 25 : 22;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (password) {
|
|
277
|
+
sheet.protect(password, {
|
|
278
|
+
selectLockedCells: true,
|
|
279
|
+
selectUnlockedCells: true,
|
|
280
|
+
formatCells: false,
|
|
281
|
+
formatColumns: false,
|
|
282
|
+
formatRows: false,
|
|
283
|
+
insertColumns: false,
|
|
284
|
+
insertRows: false,
|
|
285
|
+
insertHyperlinks: false,
|
|
286
|
+
deleteColumns: false,
|
|
287
|
+
deleteRows: false,
|
|
288
|
+
sort: false,
|
|
289
|
+
autoFilter: false,
|
|
290
|
+
pivotTables: false,
|
|
291
|
+
objects: false,
|
|
292
|
+
scenarios: false
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (freezeColumns > 0 || freezeHeader) {
|
|
297
|
+
const freezeOptions = { state: "frozen" };
|
|
298
|
+
|
|
299
|
+
if (freezeColumns > 0) {
|
|
300
|
+
freezeOptions.xSplit = freezeColumns;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (freezeHeader) {
|
|
304
|
+
// Freeze header rows based on the number of header rows present
|
|
305
|
+
const headerRowCount = (headers1.length > 0 ? 1 : 0) + (headers2.length > 0 ? 1 : 0);
|
|
306
|
+
if (headerRowCount > 0) {
|
|
307
|
+
freezeOptions.ySplit = headerRowCount;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
sheet.views = [freezeOptions];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const buffer = await workbook.xlsx.writeBuffer();
|
|
315
|
+
const blob = new Blob([buffer], {
|
|
316
|
+
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
317
|
+
});
|
|
318
|
+
const link = document.createElement("a");
|
|
319
|
+
link.href = URL.createObjectURL(blob);
|
|
320
|
+
link.download = `${filename}.xlsx`;
|
|
321
|
+
document.body.appendChild(link);
|
|
322
|
+
link.click();
|
|
323
|
+
document.body.removeChild(link);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Import and validate excel template with dynamic configuration
|
|
328
|
+
* @param {Object} config - Configuration object for importing Excel
|
|
329
|
+
* @param {number} [config.maxValue=9999999999999] - Maximum allowed value for numeric fields
|
|
330
|
+
* @param {File} config.file - The Excel file to import
|
|
331
|
+
* @param {string} [config.sheetName=''] - Specific sheet name to read. If empty, reads first sheet
|
|
332
|
+
* @param {number} [config.headerRow=1] - Row number where headers start (1-based)
|
|
333
|
+
* @param {Array<string>} [config.requiredHeaders=[]] - Array of required header names
|
|
334
|
+
* @param {Object} [config.mappings] - Column mapping for data import (e.g., { 'Kantor Cabang': 'branch_desc' })
|
|
335
|
+
* @param {boolean} [config.hasMonthlyData=true] - Whether the template contains monthly data columns
|
|
336
|
+
* @param {function} [config.validateRow] - Optional custom row validation function
|
|
337
|
+
* @returns {Promise<Array>} - Parsed and validated data
|
|
338
|
+
*/
|
|
339
|
+
export const importAnnualTemplateExcel = ({
|
|
340
|
+
maxValue = 9999999999999,
|
|
341
|
+
file,
|
|
342
|
+
sheetName = '',
|
|
343
|
+
headerRow = 1,
|
|
344
|
+
requiredHeaders = [],
|
|
345
|
+
mappings = {},
|
|
346
|
+
hasMonthlyData = true,
|
|
347
|
+
validateRow
|
|
348
|
+
}) => {
|
|
349
|
+
// Validate file type
|
|
350
|
+
const validTypes = [
|
|
351
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
352
|
+
'application/vnd.ms-excel'
|
|
353
|
+
];
|
|
354
|
+
const fileName = file.name || '';
|
|
355
|
+
const isValidExt = fileName.endsWith('.xlsx') || fileName.endsWith('.xls');
|
|
356
|
+
|
|
357
|
+
if (!validTypes.includes(file.type) && !isValidExt) {
|
|
358
|
+
ModalErrorUtil.showModal('File yang diupload harus memiliki tipe XLSX atau XLS');
|
|
359
|
+
return Promise.reject(new Error('File yang diupload harus memiliki tipe XLSX atau XLS'));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return new Promise((resolve, reject) => {
|
|
363
|
+
try {
|
|
364
|
+
const reader = new FileReader();
|
|
365
|
+
|
|
366
|
+
reader.onload = (e) => {
|
|
367
|
+
try {
|
|
368
|
+
const data = new Uint8Array(e.target.result);
|
|
369
|
+
const workbook = XLSX.read(data, { type: 'array' });
|
|
370
|
+
|
|
371
|
+
// Get the specified sheet or the first sheet
|
|
372
|
+
const sheet = sheetName
|
|
373
|
+
? workbook.Sheets[sheetName]
|
|
374
|
+
: workbook.Sheets[workbook.SheetNames[0]];
|
|
375
|
+
|
|
376
|
+
if (!sheet) {
|
|
377
|
+
reject(new Error(`Sheet ${sheetName || 'pertama'} tidak ditemukan`));
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Convert to JSON with header option
|
|
382
|
+
const jsonData = XLSX.utils.sheet_to_json(sheet, {
|
|
383
|
+
header: 1,
|
|
384
|
+
blankrows: false,
|
|
385
|
+
defval: ''
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
if (jsonData.length < headerRow) {
|
|
389
|
+
reject(new Error('File Excel kosong atau tidak memiliki data yang cukup'));
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Get headers from the specified header row (convert to 0-based index)
|
|
394
|
+
const headers = jsonData[headerRow - 1].map(h => String(h).trim());
|
|
395
|
+
|
|
396
|
+
// For dual-header templates, also get the second row for month names
|
|
397
|
+
let monthHeaders = [];
|
|
398
|
+
if (hasMonthlyData && jsonData.length > headerRow) {
|
|
399
|
+
monthHeaders = jsonData[headerRow].map(h => String(h).trim());
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Validate required headers
|
|
403
|
+
const missingHeaders = requiredHeaders.filter(required => {
|
|
404
|
+
const requiredLower = required.toLowerCase();
|
|
405
|
+
return !headers.some(h => h.toLowerCase() === requiredLower);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
if (missingHeaders.length > 0) {
|
|
409
|
+
reject(new Error(`Kolom yang diperlukan tidak ditemukan: ${missingHeaders.join(', ')}`));
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Define month mappings to match form field names: ex. {branch}_{month}
|
|
414
|
+
const monthMap = {
|
|
415
|
+
'januari': 'jan',
|
|
416
|
+
'jan': 'jan',
|
|
417
|
+
'februari': 'feb',
|
|
418
|
+
'feb': 'feb',
|
|
419
|
+
'maret': 'mar',
|
|
420
|
+
'mar': 'mar',
|
|
421
|
+
'april': 'april',
|
|
422
|
+
'apr': 'april',
|
|
423
|
+
'mei': 'mei',
|
|
424
|
+
'juni': 'jun',
|
|
425
|
+
'jun': 'jun',
|
|
426
|
+
'juli': 'jul',
|
|
427
|
+
'jul': 'jul',
|
|
428
|
+
'agustus': 'agu',
|
|
429
|
+
'agu': 'agu',
|
|
430
|
+
'september': 'sept',
|
|
431
|
+
'sept': 'sept',
|
|
432
|
+
'oktober': 'okt',
|
|
433
|
+
'okt': 'okt',
|
|
434
|
+
'november': 'nov',
|
|
435
|
+
'nov': 'nov',
|
|
436
|
+
'desember': 'des',
|
|
437
|
+
'des': 'des'
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// Create column mappings
|
|
441
|
+
const columnMappings = headers.map((header, index) => {
|
|
442
|
+
const headerLower = header.toLowerCase();
|
|
443
|
+
const monthHeader = monthHeaders[index] || '';
|
|
444
|
+
const monthHeaderLower = monthHeader.toLowerCase();
|
|
445
|
+
|
|
446
|
+
// Check custom mappings first
|
|
447
|
+
for (const [mappingKey, mappingValue] of Object.entries(mappings)) {
|
|
448
|
+
if (mappingKey.toLowerCase() === headerLower) {
|
|
449
|
+
return { index, key: mappingValue, isMonth: false };
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Then check for month columns in the second header row if hasMonthlyData is true
|
|
454
|
+
if (hasMonthlyData && monthMap[monthHeaderLower]) {
|
|
455
|
+
return { index, key: monthMap[monthHeaderLower], isMonth: true };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return { index, key: header, isMonth: false };
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Process data rows (start from headerRow + 1 to skip both header rows)
|
|
462
|
+
const result = [];
|
|
463
|
+
for (let i = headerRow + 1; i < jsonData.length; i++) {
|
|
464
|
+
const row = jsonData[i];
|
|
465
|
+
if (!row || row.length === 0) continue;
|
|
466
|
+
|
|
467
|
+
const newRow = {};
|
|
468
|
+
let hasData = false;
|
|
469
|
+
|
|
470
|
+
// Map columns according to configuration
|
|
471
|
+
columnMappings.forEach(({ index, key, isMonth }) => {
|
|
472
|
+
let value = row[index];
|
|
473
|
+
|
|
474
|
+
// Handle monthly data columns
|
|
475
|
+
if (hasMonthlyData && isMonth) {
|
|
476
|
+
// Enhanced type handling like in excel-utils.js
|
|
477
|
+
if (typeof value === 'string') {
|
|
478
|
+
// Check if string contains only valid numeric characters
|
|
479
|
+
if (!/^[\d.,\-+]*$/.test(value.trim())) {
|
|
480
|
+
value = 0;
|
|
481
|
+
} else {
|
|
482
|
+
// Check for multiple commas - only one comma allowed as decimal separator
|
|
483
|
+
const commaCount = (value.match(/,/g) || []).length;
|
|
484
|
+
if (commaCount > 1) {
|
|
485
|
+
value = 0;
|
|
486
|
+
} else {
|
|
487
|
+
// Replace comma with dot for decimal conversion (European format)
|
|
488
|
+
value = value.replace(/,/g, '.').trim();
|
|
489
|
+
const numValue = new Decimal(value || 0);
|
|
490
|
+
|
|
491
|
+
// Validate and constrain the value
|
|
492
|
+
if (numValue.isNaN() || numValue.isNegative()) {
|
|
493
|
+
value = 0;
|
|
494
|
+
} else if (numValue.gt(maxValue)) {
|
|
495
|
+
value = maxValue;
|
|
496
|
+
} else {
|
|
497
|
+
value = numValue.toNumber();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
} else if (typeof value === 'number') {
|
|
502
|
+
const numValue = new Decimal(value);
|
|
503
|
+
if (numValue.isNegative()) {
|
|
504
|
+
value = 0;
|
|
505
|
+
} else if (numValue.gt(maxValue)) {
|
|
506
|
+
value = maxValue;
|
|
507
|
+
} else {
|
|
508
|
+
value = numValue.toNumber();
|
|
509
|
+
}
|
|
510
|
+
} else {
|
|
511
|
+
// For any other type (null, undefined, boolean, etc.), set to 0
|
|
512
|
+
value = 0;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Store monthly value directly using the month key
|
|
516
|
+
newRow[key] = value;
|
|
517
|
+
hasData = true;
|
|
518
|
+
} else {
|
|
519
|
+
newRow[key] = value;
|
|
520
|
+
if (value) hasData = true;
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Skip empty rows
|
|
525
|
+
if (!hasData && Object.values(newRow).every(v => !v)) continue;
|
|
526
|
+
|
|
527
|
+
// Apply custom row validation if provided
|
|
528
|
+
if (validateRow && !validateRow(newRow)) continue;
|
|
529
|
+
|
|
530
|
+
result.push(newRow);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
resolve(result);
|
|
534
|
+
} catch (error) {
|
|
535
|
+
reject(new Error(`Error parsing Excel file: ${error.message}`));
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
reader.onerror = (error) => reject(error);
|
|
540
|
+
reader.readAsArrayBuffer(file);
|
|
541
|
+
|
|
542
|
+
} catch (error) {
|
|
543
|
+
reject(error);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function handleApiError(err, res) {
|
|
2
|
+
// ! Check if response has already been sent to avoid ERR_HTTP_HEADERS_SENT
|
|
3
|
+
if (res.headersSent) {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// ! Get status from error (axios errors have err.response.status, others might have err.status)
|
|
8
|
+
const status = err?.response?.status || err?.status;
|
|
9
|
+
|
|
10
|
+
if (err.response?.data) {
|
|
11
|
+
res.status(status).json(err.response?.data);
|
|
12
|
+
} else if (err?.message) {
|
|
13
|
+
res.status(status).json({ error: err.message });
|
|
14
|
+
} else {
|
|
15
|
+
res.status(status).json(err);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utils Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Export semua utility functions yang bisa dipakai oleh microfrontend apps.
|
|
5
|
+
* Import di microfrontend: import { formatCurrency, formatAccountNumber } from "spotlibs-components/utils";
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Formatters (consolidated)
|
|
9
|
+
export {
|
|
10
|
+
formatAccountNumber,
|
|
11
|
+
formatNPWP,
|
|
12
|
+
formatCurrency,
|
|
13
|
+
formatRupiah,
|
|
14
|
+
formatNominal,
|
|
15
|
+
formatDigit,
|
|
16
|
+
toYearFirstDash,
|
|
17
|
+
toDateFirstSlash,
|
|
18
|
+
formatDateToDDSlashMMSlashYYYY,
|
|
19
|
+
formatDateToDDMMYYYY,
|
|
20
|
+
yearFirstDashToDateFirstDash,
|
|
21
|
+
yearFirstDashToDateFirstSlash,
|
|
22
|
+
dateFirstSlashToYearFirstDash,
|
|
23
|
+
sevenDigitToDateFirstSlash,
|
|
24
|
+
sevenDigitToYearFirstDash,
|
|
25
|
+
sevenDigitToDateFirstDash,
|
|
26
|
+
toDateTimeHourMinute,
|
|
27
|
+
ddmmyyToSlashDate,
|
|
28
|
+
formatResponseDesc,
|
|
29
|
+
titleCase,
|
|
30
|
+
snakeToCamelWithSpace,
|
|
31
|
+
stringToBoolean,
|
|
32
|
+
matchWiths,
|
|
33
|
+
maskString,
|
|
34
|
+
} from "./formatters";
|
|
35
|
+
|
|
36
|
+
// JSON utilities
|
|
37
|
+
export { parseJSON, jsonToStringLimiter, errorState, ascendingStringify } from "./json";
|
|
38
|
+
|
|
39
|
+
// Asset utility
|
|
40
|
+
export { callAsset } from "./asset";
|
|
41
|
+
|
|
42
|
+
// Boolean utilities
|
|
43
|
+
export { checkRoleUser, toggleUsingDummyWbenchEndpoint } from "./booleanUtils";
|
|
44
|
+
|
|
45
|
+
// Constants
|
|
46
|
+
export {
|
|
47
|
+
NOT_AUTHORIZE_RESPONSE_CODE,
|
|
48
|
+
PUBLIC_PATHS,
|
|
49
|
+
COOKIE_MAX_SIZE,
|
|
50
|
+
CLIENT_PUBLIC_API_ROUTES,
|
|
51
|
+
CLIENT_ALLOWED_ORIGINS,
|
|
52
|
+
} from "./constants";
|
|
53
|
+
|
|
54
|
+
// Disable form utilities
|
|
55
|
+
export { disableFormElements, disableFormBuilder } from "./disableFormElements";
|
|
56
|
+
|
|
57
|
+
// Generate timestamp
|
|
58
|
+
export { generateVcifId } from "./generateTimestamp";
|
|
59
|
+
|
|
60
|
+
// LocalStorage
|
|
61
|
+
export { getLocalStorageItem } from "./localStorage";
|
|
62
|
+
|
|
63
|
+
// Tipe Bank
|
|
64
|
+
export { getTipeBankKorporasi } from "./tipeBank";
|
|
65
|
+
|
|
66
|
+
// Exception
|
|
67
|
+
export {
|
|
68
|
+
HEADER_EXCEPTION,
|
|
69
|
+
ACCESS_EXCEPTION,
|
|
70
|
+
PARAMETER_EXCEPTION,
|
|
71
|
+
NOTFOUND_EXCEPTION,
|
|
72
|
+
INVALIDRULE_EXCEPTION,
|
|
73
|
+
THIRDPARTY_EXCEPTION,
|
|
74
|
+
WAITING_EXCEPTION,
|
|
75
|
+
UNSUPPORTED_EXCEPTION,
|
|
76
|
+
RUNTIME_EXCEPTION,
|
|
77
|
+
EXCEPTION_MESSAGES,
|
|
78
|
+
ApiException,
|
|
79
|
+
isExceptionCode,
|
|
80
|
+
getExceptionMessage,
|
|
81
|
+
} from "./exception";
|
|
82
|
+
|
|
83
|
+
// Format String (legacy exports - for backward compatibility)
|
|
84
|
+
export {
|
|
85
|
+
formatCurrency as formatCurrencyLegacy,
|
|
86
|
+
formatNominal as formatNominalLegacy,
|
|
87
|
+
dashedAccount,
|
|
88
|
+
dashedNPWP,
|
|
89
|
+
formatResponseDesc as formatResponseDescLegacy,
|
|
90
|
+
titleCase as titleCaseLegacy,
|
|
91
|
+
stringToBoolean as stringToBooleanLegacy,
|
|
92
|
+
} from "./formatString";
|