@skalfa/skalfa-app 1.0.0 → 1.0.2

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.
Files changed (87) hide show
  1. package/.env.example +43 -43
  2. package/.github/workflows/publish.yml +39 -0
  3. package/CONTRIBUTING.md +45 -0
  4. package/LICENSE +21 -0
  5. package/README.md +91 -28
  6. package/app/auth/edit/page.tsx +65 -65
  7. package/app/auth/login/page.tsx +63 -63
  8. package/app/auth/me/page.tsx +58 -58
  9. package/app/auth/register/page.tsx +69 -69
  10. package/app/auth/verify/page.tsx +53 -53
  11. package/app/dashboard/user/page.tsx +76 -76
  12. package/app/layout.tsx +37 -37
  13. package/app/manifest.ts +25 -0
  14. package/app/page.tsx +13 -13
  15. package/barrels.json +5 -5
  16. package/blueprints/starter.blueprint.json +102 -102
  17. package/bun.lock +916 -0
  18. package/components/base.components/chip/Chip.component.tsx +39 -39
  19. package/components/base.components/document/DocumentViewer.component.tsx +163 -163
  20. package/components/base.components/document/ExportExcel.component.tsx +340 -340
  21. package/components/base.components/document/ImportExcel.component.tsx +315 -315
  22. package/components/base.components/document/PrintTable.component.tsx +204 -204
  23. package/components/base.components/document/RenderPDF.component.tsx +415 -415
  24. package/components/base.components/input/Checkbox.component.tsx +109 -109
  25. package/components/base.components/input/Input.component.tsx +332 -332
  26. package/components/base.components/input/InputCheckbox.component.tsx +174 -174
  27. package/components/base.components/input/InputCurrency.component.tsx +163 -163
  28. package/components/base.components/input/InputDate.component.tsx +352 -352
  29. package/components/base.components/input/InputDatetime.component.tsx +260 -260
  30. package/components/base.components/input/InputDocument.component.tsx +351 -351
  31. package/components/base.components/input/InputImage.component.tsx +533 -533
  32. package/components/base.components/input/InputMap.component.tsx +317 -317
  33. package/components/base.components/input/InputNumber.component.tsx +192 -192
  34. package/components/base.components/input/InputOtp.component.tsx +169 -169
  35. package/components/base.components/input/InputPassword.component.tsx +236 -236
  36. package/components/base.components/input/InputRadio.component.tsx +175 -175
  37. package/components/base.components/input/InputTime.component.tsx +275 -275
  38. package/components/base.components/input/InputValues.component.tsx +68 -68
  39. package/components/base.components/input/Radio.component.tsx +102 -102
  40. package/components/base.components/input/Select.component.tsx +541 -541
  41. package/components/base.components/modal/BottomSheet.component.tsx +245 -245
  42. package/components/base.components/supervision/FormSupervision.component.tsx +433 -433
  43. package/components/base.components/supervision/TableSupervision.component.tsx +697 -697
  44. package/components/base.components/table/ControlBar.component.tsx +497 -497
  45. package/components/base.components/table/FilterComponent.tsx +518 -518
  46. package/components/base.components/table/Table.component.tsx +469 -469
  47. package/components/base.components/typography/TypographyArticle.component.tsx +26 -26
  48. package/components/base.components/typography/TypographyColumn.component.tsx +20 -20
  49. package/components/base.components/typography/TypographyContent.component.tsx +20 -20
  50. package/components/base.components/typography/TypographyTips.component.tsx +20 -20
  51. package/components/base.components/wrap/Draggable.component.tsx +303 -303
  52. package/components/base.components/wrap/IDBProvider.tsx +12 -12
  53. package/components/base.components/wrap/Image.component.tsx +9 -9
  54. package/components/base.components/wrap/ShortcutProvider.tsx +57 -57
  55. package/components/base.components/wrap/Swipe.component.tsx +93 -93
  56. package/components/index.ts +2 -2
  57. package/contexts/AppProvider.tsx +11 -11
  58. package/contexts/Auth.context.tsx +64 -64
  59. package/contexts/Toggle.context.tsx +44 -44
  60. package/next.config.ts +15 -1
  61. package/package.json +14 -13
  62. package/public/204.svg +19 -19
  63. package/public/500.svg +39 -39
  64. package/public/icon-192.png +0 -0
  65. package/public/icon-512.png +0 -0
  66. package/public/images/logo-fill.png +0 -0
  67. package/public/images/logo-full-fill.png +0 -0
  68. package/public/images/logo-full.png +0 -0
  69. package/public/images/logo.png +0 -0
  70. package/schema/idb/app.schema.ts +8 -8
  71. package/src-tauri/Cargo.toml +14 -0
  72. package/src-tauri/build.rs +3 -0
  73. package/src-tauri/capabilities/default.json +11 -0
  74. package/src-tauri/icons/128x128.png +0 -0
  75. package/src-tauri/icons/128x128@2x.png +0 -0
  76. package/src-tauri/icons/32x32.png +0 -0
  77. package/src-tauri/icons/icon.icns +0 -0
  78. package/src-tauri/icons/icon.ico +0 -0
  79. package/src-tauri/src/main.rs +7 -0
  80. package/src-tauri/tauri.conf.json +36 -0
  81. package/styles/globals.css +231 -231
  82. package/styles/tailwind.safelist +68 -68
  83. package/utils/commands/barrels.ts +27 -27
  84. package/utils/commands/light.ts +21 -21
  85. package/utils/commands/logger.ts +42 -42
  86. package/utils/commands/stubs/table-blueprint.stub +12 -12
  87. package/utils/commands/use-pdf.ts +29 -29
@@ -1,340 +1,340 @@
1
- "use client";
2
-
3
- import ExcelJS from "exceljs";
4
- import { useEffect, useMemo, useState } from "react";
5
- import { faArrowLeft, faArrowRight, faEdit, faPlus, faTimes } from "@fortawesome/free-solid-svg-icons";
6
- import { api, FetchControlType, useTable } from "@utils";
7
- import { TableComponent, ButtonComponent, IconButtonComponent, SelectComponent, ModalComponent } from "@components";
8
- import { useToggleContext } from "@contexts";
9
- import { FilterComponent } from "../table/FilterComponent";
10
-
11
-
12
-
13
- type ExportColumn = {
14
- selector : string;
15
- label : string;
16
- source : string | null;
17
- };
18
-
19
- export type ExportExcelColumnControlType = {
20
- label : string;
21
- selector : string;
22
- status ?: "default" | "optional" | "hidden";
23
- };
24
-
25
- type ExportExcelProps = {
26
- fetchControl : FetchControlType;
27
- columnControl ?: ExportExcelColumnControlType[];
28
- filename ?: string;
29
- };
30
-
31
-
32
-
33
- const ADD_COLUMN_KEY = "__add_column__";
34
-
35
- function numberToExcelColumn(index: number): string {
36
- let column = "";
37
- let n = index;
38
-
39
- while (n >= 0) {
40
- column = String.fromCharCode((n % 26) + 65) + column;
41
- n = Math.floor(n / 26) - 1;
42
- }
43
-
44
- return column;
45
- }
46
-
47
-
48
-
49
- export function ExportExcel({ fetchControl, columnControl, filename }: ExportExcelProps) {
50
- const { toggle, setToggle } = useToggleContext()
51
-
52
- const { data, tableControl, setParam, params } = useTable(fetchControl, undefined, undefined, false);
53
-
54
- const fields = useMemo(() => {
55
- if (columnControl?.length) {
56
- return columnControl.filter(c => c.status !== "hidden").map(c => c.selector);
57
- }
58
-
59
- if (!data?.data?.[0]) return [];
60
- return Object.keys(data.data[0]);
61
- }, [data, columnControl]);
62
-
63
- const [columns, setColumns] = useState<ExportColumn[]>([]);
64
-
65
-
66
- useEffect(() => {
67
- if (!data?.data?.[0]) return;
68
-
69
- setColumns(prev => {
70
- if (prev.length) return prev;
71
-
72
- const baseFields = columnControl?.length ? columnControl.filter(c => c.status === "default" || c.status === undefined).map(c => c.selector) : Object.keys(data.data[0]);
73
-
74
- return baseFields.map((field, i) => {
75
- const label = numberToExcelColumn(i);
76
-
77
- return {
78
- selector : label,
79
- label : label,
80
- source : field,
81
- };
82
- });
83
- });
84
- }, [data, columnControl]);
85
-
86
-
87
- const addColumn = (selector: string, source: string) => {
88
- setColumns(prev => [...prev, { selector, label: selector, source }]);
89
- };
90
-
91
- const removeColumn = (selector: string) => {
92
- setColumns(prev => prev.filter(c => c.selector !== selector).map((c, i) => {
93
- const label = numberToExcelColumn(i);
94
- return { ...c, selector: label, label };
95
- }));
96
- };
97
-
98
- const moveColumn = (selector: string, dir: "left" | "right") => {
99
- setColumns(prev => {
100
- const idx = prev.findIndex(c => c.selector === selector);
101
- if (idx === -1) return prev;
102
-
103
- const target = dir === "left" ? idx - 1 : idx + 1;
104
- if (target < 0 || target >= prev.length) return prev;
105
-
106
- const next = [...prev];
107
- [next[idx], next[target]] = [next[target], next[idx]];
108
-
109
- return next.map((c, i) => {
110
- const label = numberToExcelColumn(i);
111
- return { ...c, selector: label, label };
112
- });
113
- });
114
- };
115
-
116
- const setSource = (selector: string, source: string | null) => setColumns(prev => prev.map(c => (c.selector === selector ? { ...c, source } : c)));
117
-
118
- const getColumnLabel = (source: string | null) => {
119
- if (!source) return "";
120
-
121
- const found = columnControl?.find(c => c.selector === source);
122
-
123
- return found?.label ?? source;
124
- };
125
-
126
-
127
- const tableColumns = useMemo(() => {
128
- return [
129
- ...columns?.map((c => ({
130
- ...c,
131
- label: <div className="w-full text-center">{c.label}</div>
132
- }))),
133
- { selector: ADD_COLUMN_KEY, label: "" } as any,
134
- ];
135
- }, [columns]);
136
-
137
- const tableData = useMemo(() => {
138
- if (!data?.data || !columns.length) return [];
139
-
140
- const controlRow: Record<string, any> = {};
141
-
142
- columns.forEach((col, iCol) => {
143
- controlRow[col.selector] = (
144
- <div className="flex justify-between">
145
- <p className="font-semibold">{getColumnLabel(col.source)}</p>
146
-
147
- <div className="flex gap-1">
148
- {iCol > 0 && (
149
- <IconButtonComponent
150
- icon={faArrowLeft}
151
- size="xs"
152
- variant="outline"
153
- className="!text-foreground"
154
- onClick={() => moveColumn(col.selector, "left")}
155
- />
156
- )}
157
-
158
- {iCol < (columns.length - 1) && (
159
- <IconButtonComponent
160
- icon={faArrowRight}
161
- size="xs"
162
- variant="outline"
163
- className="!text-foreground"
164
- onClick={() => moveColumn(col.selector, "right")}
165
- />
166
- )}
167
-
168
- <IconButtonComponent
169
- icon={faEdit}
170
- size="xs"
171
- paint="warning"
172
- variant="outline"
173
- disabled={columns.length <= 1}
174
- onClick={() => setToggle("MODAL_FIELD_EXPORT", {selector: col.selector, value: col.source})}
175
- />
176
-
177
- <IconButtonComponent
178
- icon={faTimes}
179
- size="xs"
180
- paint="danger"
181
- variant="outline"
182
- disabled={columns.length <= 1}
183
- onClick={() => removeColumn(col.selector)}
184
- />
185
- </div>
186
-
187
-
188
- </div>
189
- );
190
- });
191
-
192
- controlRow[ADD_COLUMN_KEY] = (
193
- <div className="flex justify-center">
194
- <ButtonComponent
195
- icon={faPlus}
196
- label="Tambah Kolom"
197
- size="xs"
198
- variant="outline"
199
- onClick={() => {
200
- const idx = columns.length;
201
- const label = numberToExcelColumn(idx);
202
- setToggle("MODAL_FIELD_EXPORT", { selector: label, value: "", add: true })
203
- }}
204
- />
205
- </div>
206
- );
207
-
208
- const excelRows = data.data.map((item: Record<string, any>) => {
209
- const row: Record<string, any> = {};
210
-
211
- row[ADD_COLUMN_KEY] = "-"
212
- columns.forEach(col => {
213
- row[col.selector] = col.source ? <span className="line-clamp-1">{item[col.source] || "-"}</span> : "-";
214
- });
215
-
216
- return row;
217
- });
218
-
219
- return [controlRow, ...excelRows];
220
- }, [data, columns, fields]);
221
-
222
-
223
- const handleDownloadExcel = async () => {
224
- const record = await api({...fetchControl, params: {...fetchControl.params, ...params, page: 1, paginate: 9999}});
225
- if (!record?.data?.data?.length || !columns.length) return;
226
-
227
- const workbook = new ExcelJS.Workbook();
228
- const worksheet = workbook.addWorksheet("Export");
229
-
230
- worksheet.columns = columns.map(col => ({
231
- header : getColumnLabel(col.source),
232
- key : col.selector,
233
- width : 20,
234
- }));
235
-
236
- worksheet.getRow(1).eachCell(cell => {
237
- cell.font = { bold: true };
238
- cell.alignment = { horizontal: "center", vertical: "middle" };
239
- });
240
-
241
- record.data.data.forEach((item: Record<string, any>) => {
242
- const row: Record<string, any> = {};
243
-
244
- columns.forEach(col => {
245
- row[col.selector] = col.source ? item[col.source] : "";
246
- });
247
-
248
- worksheet.addRow(row);
249
- });
250
-
251
- const buffer = await workbook.xlsx.writeBuffer();
252
- const blob = new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
253
-
254
- const link = document.createElement("a");
255
- link.href = URL.createObjectURL(blob);
256
- link.download = filename ? filename + ".xlsx" : "export.xlsx";
257
- link.click();
258
-
259
- URL.revokeObjectURL(link.href);
260
- };
261
-
262
-
263
-
264
- return (
265
- <>
266
- <div className="p-4">
267
- <FilterComponent
268
- columns={columnControl?.length
269
- ? columnControl.filter(c => c.status !== "hidden").map(c => ({
270
- label: c.label,
271
- selector: c.selector,
272
- type: "text"
273
- }))
274
- : (data?.data?.[0] ? Object.keys(data.data[0]).map((c) => ({
275
- label: c,
276
- selector: c,
277
- type: "text"
278
- })) : [])
279
- }
280
- onChange={(filters) => setParam('filter', filters)}
281
- value={params?.filter || []}
282
- />
283
- </div>
284
-
285
- <TableComponent
286
- controlBar={false}
287
- columns={tableColumns}
288
- data={tableData}
289
- {...tableControl}
290
- pagination={false}
291
- className="p-4 bg-background row::bg-transparent row::border-0 row::gap-0 row::!hover:bg-background column::p-2 column::border head-column::p-2 head-column::border"
292
- />
293
-
294
- <div className="px-4 mt-8">
295
- <ButtonComponent label="Download Excel" block onClick={() => handleDownloadExcel()} />
296
- </div>
297
-
298
- <ModalComponent
299
- show={!!toggle["MODAL_FIELD_EXPORT"]}
300
- onClose={() => setToggle("MODAL_FIELD_EXPORT", false)}
301
- title="Pilih Kolom"
302
- footer={
303
- <div className="flex justify-end">
304
- <ButtonComponent
305
- label="Terapkan"
306
- onClick={() => {
307
- if(!!(toggle["MODAL_FIELD_EXPORT"] as { value: string })?.value) {
308
- if((toggle["MODAL_FIELD_EXPORT"] as { add: boolean })?.add) {
309
- addColumn((toggle["MODAL_FIELD_EXPORT"] as { selector: string })?.selector, (toggle["MODAL_FIELD_EXPORT"] as { value: string })?.value)
310
- } else {
311
- setSource((toggle["MODAL_FIELD_EXPORT"] as { selector: string })?.selector, (toggle["MODAL_FIELD_EXPORT"] as { value: string })?.value)
312
- }
313
- }
314
- setToggle("MODAL_FIELD_EXPORT", false)
315
- }}
316
- />
317
- </div>
318
- }
319
- >
320
- <div className="p-4">
321
- <SelectComponent
322
- name={`column_${(toggle["MODAL_FIELD_EXPORT"] as { selector: string })?.selector}`}
323
- placeholder="Pilih kolom data..."
324
- value={(toggle["MODAL_FIELD_EXPORT"] as { value: string })?.value ?? ""}
325
- onChange={e => setToggle("MODAL_FIELD_EXPORT", {...(toggle["MODAL_FIELD_EXPORT"] as object), value: e})}
326
- options={fields.map((f: string) => {
327
- const col = columnControl?.find(c => c.selector === f);
328
- if (col?.status === "hidden") return null;
329
-
330
- return {
331
- label: col?.label || f,
332
- value: f,
333
- };
334
- }).filter(Boolean) as any}
335
- />
336
- </div>
337
- </ModalComponent>
338
- </>
339
- );
340
- }
1
+ "use client";
2
+
3
+ import ExcelJS from "exceljs";
4
+ import { useEffect, useMemo, useState } from "react";
5
+ import { faArrowLeft, faArrowRight, faEdit, faPlus, faTimes } from "@fortawesome/free-solid-svg-icons";
6
+ import { api, FetchControlType, useTable } from "@utils";
7
+ import { TableComponent, ButtonComponent, IconButtonComponent, SelectComponent, ModalComponent } from "@components";
8
+ import { useToggleContext } from "@contexts";
9
+ import { FilterComponent } from "../table/FilterComponent";
10
+
11
+
12
+
13
+ type ExportColumn = {
14
+ selector : string;
15
+ label : string;
16
+ source : string | null;
17
+ };
18
+
19
+ export type ExportExcelColumnControlType = {
20
+ label : string;
21
+ selector : string;
22
+ status ?: "default" | "optional" | "hidden";
23
+ };
24
+
25
+ type ExportExcelProps = {
26
+ fetchControl : FetchControlType;
27
+ columnControl ?: ExportExcelColumnControlType[];
28
+ filename ?: string;
29
+ };
30
+
31
+
32
+
33
+ const ADD_COLUMN_KEY = "__add_column__";
34
+
35
+ function numberToExcelColumn(index: number): string {
36
+ let column = "";
37
+ let n = index;
38
+
39
+ while (n >= 0) {
40
+ column = String.fromCharCode((n % 26) + 65) + column;
41
+ n = Math.floor(n / 26) - 1;
42
+ }
43
+
44
+ return column;
45
+ }
46
+
47
+
48
+
49
+ export function ExportExcel({ fetchControl, columnControl, filename }: ExportExcelProps) {
50
+ const { toggle, setToggle } = useToggleContext()
51
+
52
+ const { data, tableControl, setParam, params } = useTable(fetchControl, undefined, undefined, false);
53
+
54
+ const fields = useMemo(() => {
55
+ if (columnControl?.length) {
56
+ return columnControl.filter(c => c.status !== "hidden").map(c => c.selector);
57
+ }
58
+
59
+ if (!data?.data?.[0]) return [];
60
+ return Object.keys(data.data[0]);
61
+ }, [data, columnControl]);
62
+
63
+ const [columns, setColumns] = useState<ExportColumn[]>([]);
64
+
65
+
66
+ useEffect(() => {
67
+ if (!data?.data?.[0]) return;
68
+
69
+ setColumns(prev => {
70
+ if (prev.length) return prev;
71
+
72
+ const baseFields = columnControl?.length ? columnControl.filter(c => c.status === "default" || c.status === undefined).map(c => c.selector) : Object.keys(data.data[0]);
73
+
74
+ return baseFields.map((field, i) => {
75
+ const label = numberToExcelColumn(i);
76
+
77
+ return {
78
+ selector : label,
79
+ label : label,
80
+ source : field,
81
+ };
82
+ });
83
+ });
84
+ }, [data, columnControl]);
85
+
86
+
87
+ const addColumn = (selector: string, source: string) => {
88
+ setColumns(prev => [...prev, { selector, label: selector, source }]);
89
+ };
90
+
91
+ const removeColumn = (selector: string) => {
92
+ setColumns(prev => prev.filter(c => c.selector !== selector).map((c, i) => {
93
+ const label = numberToExcelColumn(i);
94
+ return { ...c, selector: label, label };
95
+ }));
96
+ };
97
+
98
+ const moveColumn = (selector: string, dir: "left" | "right") => {
99
+ setColumns(prev => {
100
+ const idx = prev.findIndex(c => c.selector === selector);
101
+ if (idx === -1) return prev;
102
+
103
+ const target = dir === "left" ? idx - 1 : idx + 1;
104
+ if (target < 0 || target >= prev.length) return prev;
105
+
106
+ const next = [...prev];
107
+ [next[idx], next[target]] = [next[target], next[idx]];
108
+
109
+ return next.map((c, i) => {
110
+ const label = numberToExcelColumn(i);
111
+ return { ...c, selector: label, label };
112
+ });
113
+ });
114
+ };
115
+
116
+ const setSource = (selector: string, source: string | null) => setColumns(prev => prev.map(c => (c.selector === selector ? { ...c, source } : c)));
117
+
118
+ const getColumnLabel = (source: string | null) => {
119
+ if (!source) return "";
120
+
121
+ const found = columnControl?.find(c => c.selector === source);
122
+
123
+ return found?.label ?? source;
124
+ };
125
+
126
+
127
+ const tableColumns = useMemo(() => {
128
+ return [
129
+ ...columns?.map((c => ({
130
+ ...c,
131
+ label: <div className="w-full text-center">{c.label}</div>
132
+ }))),
133
+ { selector: ADD_COLUMN_KEY, label: "" } as any,
134
+ ];
135
+ }, [columns]);
136
+
137
+ const tableData = useMemo(() => {
138
+ if (!data?.data || !columns.length) return [];
139
+
140
+ const controlRow: Record<string, any> = {};
141
+
142
+ columns.forEach((col, iCol) => {
143
+ controlRow[col.selector] = (
144
+ <div className="flex justify-between">
145
+ <p className="font-semibold">{getColumnLabel(col.source)}</p>
146
+
147
+ <div className="flex gap-1">
148
+ {iCol > 0 && (
149
+ <IconButtonComponent
150
+ icon={faArrowLeft}
151
+ size="xs"
152
+ variant="outline"
153
+ className="!text-foreground"
154
+ onClick={() => moveColumn(col.selector, "left")}
155
+ />
156
+ )}
157
+
158
+ {iCol < (columns.length - 1) && (
159
+ <IconButtonComponent
160
+ icon={faArrowRight}
161
+ size="xs"
162
+ variant="outline"
163
+ className="!text-foreground"
164
+ onClick={() => moveColumn(col.selector, "right")}
165
+ />
166
+ )}
167
+
168
+ <IconButtonComponent
169
+ icon={faEdit}
170
+ size="xs"
171
+ paint="warning"
172
+ variant="outline"
173
+ disabled={columns.length <= 1}
174
+ onClick={() => setToggle("MODAL_FIELD_EXPORT", {selector: col.selector, value: col.source})}
175
+ />
176
+
177
+ <IconButtonComponent
178
+ icon={faTimes}
179
+ size="xs"
180
+ paint="danger"
181
+ variant="outline"
182
+ disabled={columns.length <= 1}
183
+ onClick={() => removeColumn(col.selector)}
184
+ />
185
+ </div>
186
+
187
+
188
+ </div>
189
+ );
190
+ });
191
+
192
+ controlRow[ADD_COLUMN_KEY] = (
193
+ <div className="flex justify-center">
194
+ <ButtonComponent
195
+ icon={faPlus}
196
+ label="Tambah Kolom"
197
+ size="xs"
198
+ variant="outline"
199
+ onClick={() => {
200
+ const idx = columns.length;
201
+ const label = numberToExcelColumn(idx);
202
+ setToggle("MODAL_FIELD_EXPORT", { selector: label, value: "", add: true })
203
+ }}
204
+ />
205
+ </div>
206
+ );
207
+
208
+ const excelRows = data.data.map((item: Record<string, any>) => {
209
+ const row: Record<string, any> = {};
210
+
211
+ row[ADD_COLUMN_KEY] = "-"
212
+ columns.forEach(col => {
213
+ row[col.selector] = col.source ? <span className="line-clamp-1">{item[col.source] || "-"}</span> : "-";
214
+ });
215
+
216
+ return row;
217
+ });
218
+
219
+ return [controlRow, ...excelRows];
220
+ }, [data, columns, fields]);
221
+
222
+
223
+ const handleDownloadExcel = async () => {
224
+ const record = await api({...fetchControl, params: {...fetchControl.params, ...params, page: 1, paginate: 9999}});
225
+ if (!record?.data?.data?.length || !columns.length) return;
226
+
227
+ const workbook = new ExcelJS.Workbook();
228
+ const worksheet = workbook.addWorksheet("Export");
229
+
230
+ worksheet.columns = columns.map(col => ({
231
+ header : getColumnLabel(col.source),
232
+ key : col.selector,
233
+ width : 20,
234
+ }));
235
+
236
+ worksheet.getRow(1).eachCell(cell => {
237
+ cell.font = { bold: true };
238
+ cell.alignment = { horizontal: "center", vertical: "middle" };
239
+ });
240
+
241
+ record.data.data.forEach((item: Record<string, any>) => {
242
+ const row: Record<string, any> = {};
243
+
244
+ columns.forEach(col => {
245
+ row[col.selector] = col.source ? item[col.source] : "";
246
+ });
247
+
248
+ worksheet.addRow(row);
249
+ });
250
+
251
+ const buffer = await workbook.xlsx.writeBuffer();
252
+ const blob = new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
253
+
254
+ const link = document.createElement("a");
255
+ link.href = URL.createObjectURL(blob);
256
+ link.download = filename ? filename + ".xlsx" : "export.xlsx";
257
+ link.click();
258
+
259
+ URL.revokeObjectURL(link.href);
260
+ };
261
+
262
+
263
+
264
+ return (
265
+ <>
266
+ <div className="p-4">
267
+ <FilterComponent
268
+ columns={columnControl?.length
269
+ ? columnControl.filter(c => c.status !== "hidden").map(c => ({
270
+ label: c.label,
271
+ selector: c.selector,
272
+ type: "text"
273
+ }))
274
+ : (data?.data?.[0] ? Object.keys(data.data[0]).map((c) => ({
275
+ label: c,
276
+ selector: c,
277
+ type: "text"
278
+ })) : [])
279
+ }
280
+ onChange={(filters) => setParam('filter', filters)}
281
+ value={params?.filter || []}
282
+ />
283
+ </div>
284
+
285
+ <TableComponent
286
+ controlBar={false}
287
+ columns={tableColumns}
288
+ data={tableData}
289
+ {...tableControl}
290
+ pagination={false}
291
+ className="p-4 bg-background row::bg-transparent row::border-0 row::gap-0 row::!hover:bg-background column::p-2 column::border head-column::p-2 head-column::border"
292
+ />
293
+
294
+ <div className="px-4 mt-8">
295
+ <ButtonComponent label="Download Excel" block onClick={() => handleDownloadExcel()} />
296
+ </div>
297
+
298
+ <ModalComponent
299
+ show={!!toggle["MODAL_FIELD_EXPORT"]}
300
+ onClose={() => setToggle("MODAL_FIELD_EXPORT", false)}
301
+ title="Pilih Kolom"
302
+ footer={
303
+ <div className="flex justify-end">
304
+ <ButtonComponent
305
+ label="Terapkan"
306
+ onClick={() => {
307
+ if(!!(toggle["MODAL_FIELD_EXPORT"] as { value: string })?.value) {
308
+ if((toggle["MODAL_FIELD_EXPORT"] as { add: boolean })?.add) {
309
+ addColumn((toggle["MODAL_FIELD_EXPORT"] as { selector: string })?.selector, (toggle["MODAL_FIELD_EXPORT"] as { value: string })?.value)
310
+ } else {
311
+ setSource((toggle["MODAL_FIELD_EXPORT"] as { selector: string })?.selector, (toggle["MODAL_FIELD_EXPORT"] as { value: string })?.value)
312
+ }
313
+ }
314
+ setToggle("MODAL_FIELD_EXPORT", false)
315
+ }}
316
+ />
317
+ </div>
318
+ }
319
+ >
320
+ <div className="p-4">
321
+ <SelectComponent
322
+ name={`column_${(toggle["MODAL_FIELD_EXPORT"] as { selector: string })?.selector}`}
323
+ placeholder="Pilih kolom data..."
324
+ value={(toggle["MODAL_FIELD_EXPORT"] as { value: string })?.value ?? ""}
325
+ onChange={e => setToggle("MODAL_FIELD_EXPORT", {...(toggle["MODAL_FIELD_EXPORT"] as object), value: e})}
326
+ options={fields.map((f: string) => {
327
+ const col = columnControl?.find(c => c.selector === f);
328
+ if (col?.status === "hidden") return null;
329
+
330
+ return {
331
+ label: col?.label || f,
332
+ value: f,
333
+ };
334
+ }).filter(Boolean) as any}
335
+ />
336
+ </div>
337
+ </ModalComponent>
338
+ </>
339
+ );
340
+ }