@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,697 +1,697 @@
1
- "use client"
2
-
3
- import { ReactNode, Suspense, useEffect, useMemo } from "react";
4
- import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
5
- import { faEdit, faFileExcel, faFilePdf, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
6
- import { ApiType, cn, conversion, FetchControlType, registry, shortcut, ShortcutHandler, UseResourceIdb, UseResourceProps, useResponsive, useTable } from "@utils";
7
- import { useToggleContext } from "@contexts";
8
- import { FloatingPageComponent, FloatingPageProps, ButtonComponent, IconButtonComponent, TableColumnType, TableComponent, FormSupervisionComponent, FormType, ModalConfirmComponent, TypographyColumnComponent, ButtonProps, ModalConfirmProps, TableProps, ControlBarOptionType, BottomSheetComponent, SwipeActionType } from "@components";
9
-
10
- const ExportExcel = registry.get("ExportExcel");
11
- const ImportExcel = registry.get("ImportExcel");
12
-
13
-
14
-
15
- export interface TableSupervisionColumnProps {
16
- selector : string;
17
- label ?: string;
18
- width ?: string;
19
- sortable ?: boolean;
20
- searchable ?: boolean;
21
- filterable ?: boolean | {
22
- type : "text" | "number" | "currency" | "date";
23
- } | {
24
- type : "select";
25
- options : { label: string; value: any }[];
26
- };
27
- accessCode ?: string;
28
- item ?: (data: any) => string | ReactNode;
29
- tip ?: string | ((data: any) => string);
30
- exportable ?: boolean | "default" | "optional" | "hidden";
31
- importable ?: boolean;
32
- };
33
-
34
- export interface TableSupervisionFormProps {
35
- fields : string[] | (FormType & { visibility?: "*" | "create" | "update" })[];
36
- defaultValue ?: (item: Record<string, any> | null) => Promise<Record<string, any>> | Record<string, any>;
37
- payload ?: (values: any) => Promise<Record<string, any>> | object;
38
- modalControl ?: Omit<FloatingPageProps, "show" | "onClose" | "children">;
39
- contentType ?: "application/json" | "multipart/form-data";
40
- };
41
-
42
-
43
- export type TableSupervisionProps = {
44
- fetchControl : UseResourceProps;
45
- title ?: string;
46
- id ?: string;
47
- accessCode ?: number;
48
- urlParam ?: boolean | { compressed ?: boolean }
49
- onRowClick ?: (data: Record<string, any>) => void;
50
- columnControl ?: string[] | TableSupervisionColumnProps[];
51
- formControl ?: TableSupervisionFormProps;
52
- detailControl ?: boolean
53
- | (
54
- | string
55
- | { label: string, item: string | ((data: Record<string, any>) => ReactNode) }
56
- | ((data: Record<string, any>) => ReactNode)
57
- )[]
58
- | ((data: Record<string, any>) => ReactNode);
59
- actionControl ?: boolean | (
60
- | 'EDIT' | 'DELETE' | {
61
- label : string,
62
- modal ?: Omit<ModalConfirmProps, "show" | "onClose">,
63
- button ?: ButtonProps,
64
- shortcut ?: { key: string, description: string },
65
- } | ((
66
- row : Record<string, any>,
67
- setModal : (type: "EDIT" | "DELETE") => void,
68
- setDataSelected ?: () => void,
69
- setShortcut ?: (key: string, handler: ShortcutHandler, description?: string) => void
70
- ) => ReactNode)
71
- )[];
72
- block ?: boolean,
73
- noIndex ?: boolean;
74
- actionBulkingControl ?: TableProps["actionBulking"],
75
- controlBar ?: (ControlBarOptionType | "CREATE" | "IMPORT" | "EXPORT" | "PRINT")[];
76
- responsiveControl ?: {
77
- mobile ?: boolean | {
78
- item ?: (item: Record<string, any>, key: number) => ReactNode,
79
- leftActionControl ?: Omit<SwipeActionType, "onAction"> & { onAction?: (item: Record<string, any>, key?: number) => void },
80
- rightActionControl ?: Omit<SwipeActionType, "onAction"> & { onAction?: (item: Record<string, any>, key?: number) => void },
81
- }
82
- };
83
- importControl ?: FetchControlType;
84
- };
85
-
86
-
87
-
88
- export function TableSupervisionComponent({
89
- title,
90
- id,
91
- fetchControl,
92
- columnControl,
93
- formControl,
94
- onRowClick,
95
- detailControl,
96
- actionControl,
97
- actionBulkingControl,
98
- block,
99
- controlBar,
100
- noIndex,
101
- responsiveControl,
102
- urlParam,
103
- importControl,
104
- }: TableSupervisionProps) {
105
- const { tableKey, tableControl, data, selected, setSelected, checks, setChecks, reset, focus, setFocus } = useTable(fetchControl, id, title, (urlParam || true))
106
- const { setToggle, toggle } = useToggleContext()
107
- const { isSm } = useResponsive();
108
-
109
- const toggleKey = useMemo(() => conversion.strSnake(tableKey).toUpperCase(), [tableKey])
110
-
111
-
112
- useEffect(() => {
113
- if(data?.data?.length && !toggle[`MODAL_DELETE_${toggleKey}`] && !toggle[`MODAL_DELETE_${toggleKey}`] && !toggle[`MODAL_SHOW_${toggleKey}`]) {
114
- shortcut.register("arrowdown", () => {
115
- const max = data?.data?.length - 1;
116
- setFocus(focus == null ? 0 : focus >= max ? max : (focus + 1))
117
- }, "Pilih data kebawah")
118
-
119
- shortcut.register("arrowup", () => {
120
- setFocus(focus == null ? 0 : focus <= 0 ? 0 : (focus - 1))
121
- }, "Pilih data keatas")
122
-
123
- if(focus != null) {
124
- shortcut.register("delete", () => {
125
- setSelected(data?.data?.at(focus))
126
- setToggle(`MODAL_DELETE_${toggleKey}`)
127
- }, "Delete data yang dipilih")
128
-
129
- shortcut.register(" ", () => {
130
- setSelected(data?.data?.at(focus))
131
- setToggle(`MODAL_FORM_${toggleKey}`)
132
- }, "Edit data yang dipilih")
133
-
134
- shortcut.register("enter", () => {
135
- setSelected(data?.data?.at(focus))
136
- setToggle(`MODAL_SHOW_${toggleKey}`)
137
- }, "Detail data yang dipilih")
138
-
139
- shortcut.register("escape", () => {
140
- setFocus(null)
141
- }, "Kembali")
142
- }
143
- }
144
-
145
- return () => {
146
- shortcut.unregister("arrowdown")
147
- shortcut.unregister("arrowup")
148
- shortcut.unregister("delete")
149
- shortcut.unregister(" ")
150
- shortcut.unregister("enter")
151
- shortcut.unregister("escape")
152
- }
153
- }, [data?.data, actionControl, focus, toggle[`MODAL_DELETE_${toggleKey}`], toggle[`MODAL_DELETE_${toggleKey}`], toggle[`MODAL_SHOW_${toggleKey}`]])
154
-
155
-
156
- // ============================
157
- // ## Column preparation
158
- // ============================
159
- const columns = useMemo(() => {
160
- return columnControl?.length ? columnControl.map((col) => {
161
- if (typeof col === "string") {
162
- return {
163
- selector : col,
164
- label : col,
165
- };
166
- } else {
167
- return { ...col };
168
- }
169
- })
170
- : data?.columns || data?.data?.at(0) ? Object.keys(data.data[0]).map((col) => {
171
- return {
172
- selector : col,
173
- label : col,
174
- };
175
- })
176
- : [];
177
- }, [columnControl, data]);
178
-
179
-
180
-
181
- const renderTableAction = (
182
- actions : TableSupervisionProps["actionControl"],
183
- item ?: Record<string, any>,
184
- options ?: {size?: ButtonProps['size'], className?: string}
185
- ) => {
186
- return (
187
- <>
188
- <div className={cn("flex items-center gap-2", options?.className)}>
189
- {(Array.isArray(actions) ? actions : (actions || actions == undefined) ? ['EDIT', "DELETE"] : [])?.map((action, key) => {
190
- if(action == "EDIT") {
191
- return (
192
- <ButtonComponent
193
- key={key}
194
- icon={faEdit}
195
- label={"Ubah"}
196
- variant="outline"
197
- paint="warning"
198
- size={options?.size || "xs"}
199
- rounded
200
- onClick={() => {
201
- setToggle(`MODAL_FORM_${toggleKey}`);
202
- item && setSelected?.(item);
203
- }}
204
- />
205
- )
206
- }
207
-
208
- if(action == "DELETE") {
209
- return (
210
- <ButtonComponent
211
- key={key}
212
- icon={faTrash}
213
- label={"Hapus"}
214
- variant="outline"
215
- paint="danger"
216
- size={options?.size || "xs"}
217
- rounded
218
- onClick={() => {
219
- setToggle(`MODAL_DELETE_${toggleKey}`);
220
- item && setSelected?.(item);
221
- }}
222
- />
223
- )
224
- }
225
-
226
- if(typeof action == "object") {
227
- <ButtonComponent
228
- key={`action-object-${key}`}
229
- label={action?.button?.label || action?.label}
230
- variant={action?.button?.variant || "outline"}
231
- paint={action?.button?.paint || "primary"}
232
- size={action?.button?.size || options?.size || "xs"}
233
- rounded={action?.button?.rounded || true}
234
- onClick={() => {
235
- if (action?.button?.onClick) {
236
- action?.button?.onClick(item)
237
- } else {
238
- setToggle(`MODAL_${conversion.strSnake(action?.label).toUpperCase()}_${toggleKey}`);
239
- item && setSelected?.(item);
240
- }
241
- }}
242
- {...action.button}
243
- />
244
- }
245
-
246
- if(typeof action == "function") {
247
- return (
248
- <span key={`action-fn-${key}`}>
249
- {action(item || {}, (type: "EDIT" | "DELETE") => {
250
- if(type == "EDIT") {
251
- setToggle(`MODAL_FORM_${toggleKey}`);
252
- item && setSelected?.(item);
253
- }
254
-
255
- if (type == "DELETE") {
256
- setToggle(`MODAL_DELETE_${toggleKey}`);
257
- item && setSelected?.(item);
258
- }
259
- })}
260
- </span>
261
- )
262
- }
263
-
264
- return <span key={`action-default-${key}`}></span>;
265
- })}
266
- </div>
267
- </>
268
- )
269
- }
270
-
271
-
272
- // ============================
273
- // ## Data table preparation
274
- // ============================
275
- const dataTables = useMemo(() => {
276
- return data?.data?.map((row: object) => {
277
- return {
278
- ...row,
279
- action: renderTableAction(actionControl, row),
280
- };
281
- });
282
- }, [actionControl, data]);
283
-
284
-
285
- // ============================
286
- // ## Render detail page
287
- // ============================
288
- const detailPage = useMemo(() => {
289
- return (
290
- <div className="p-4">
291
- <div className={cn(
292
- "flex flex-col gap-y-4",
293
- )}>
294
- {!!selected && (typeof detailControl === "object" && detailControl?.length ? detailControl?.map((column, key) => {
295
- if (typeof column === "string") {
296
- return (<TypographyColumnComponent
297
- key={key}
298
- title={columns?.find((c) => c.selector == column)?.label}
299
- content={selected[column]}
300
- />)
301
- } else if (typeof column === "object") {
302
- return (<TypographyColumnComponent
303
- key={key}
304
- title={column?.label}
305
- content={typeof column?.item === "string" ? selected[column?.item] : column?.item(selected)}
306
- />)
307
- } else {
308
- return column?.(selected)
309
- }
310
- }) : typeof detailControl == "function" ? detailControl(selected) : columns?.map((column, key) => (
311
- <TypographyColumnComponent
312
- key={key}
313
- title={column.label}
314
- content={selected[column.selector]}
315
- />
316
- )))}
317
- </div>
318
- </div>
319
- )
320
- }, [selected, detailControl]);
321
-
322
-
323
-
324
-
325
- // ============================
326
- // ## Form preparation
327
- // ============================
328
- const fields = useMemo(() => {
329
- return formControl?.fields?.length ? formControl?.fields.map((form) => {
330
- return typeof form === "string" ? {
331
- col : 12,
332
- type : "text",
333
- construction : {
334
- name : form,
335
- label : form,
336
- },
337
- } : { ...form };
338
- }) : data?.forms || data?.columns || columnControl?.map((col) => {
339
- return {
340
- col : 12,
341
- type : "text",
342
- construction : {
343
- name : typeof col == "string" ? col : col?.selector,
344
- label : typeof col == "string" ? col : col?.label,
345
- placeholder : `Masukkan ${ typeof col == "string" ? col : col?.label}...`,
346
- },
347
- };
348
- }) || (data?.data?.at(0) ? Object.keys(data.data[0]).map((col) => {
349
- return {
350
- col : 12,
351
- type : "text",
352
- construction : {
353
- name : col,
354
- label : col,
355
- placeholder : `Masukkan ${col}...`,
356
- },
357
- };
358
- })
359
- : []);
360
- }, [formControl, data]);
361
-
362
-
363
-
364
- // ============================
365
- // ## Render form page
366
- // ============================
367
- const formPage = useMemo(async () => {
368
- return (
369
- <FormSupervisionComponent
370
- submitControl={(fetchControl as ApiType).path ? {
371
- path: `${(fetchControl as ApiType).path}/${(selected as { id: number })?.id || "" }`,
372
- method: !(selected as { id: number })?.id ? "POST" : "PUT",
373
- } : (fetchControl as ApiType).url ? {
374
- url: `${(fetchControl as ApiType).url}/${(selected as { id: number })?.id || ""}`,
375
- method: !(selected as { id: number })?.id ? "POST" : "PUT",
376
- } : { idb: (fetchControl as ({ idb: UseResourceIdb }))?.idb }
377
- }
378
- fields={fields as FormType[]}
379
- defaultValue={formControl?.defaultValue ? await formControl?.defaultValue(selected || null) : selected}
380
- payload={formControl?.payload}
381
- onSuccess={() => {
382
- reset();
383
- setToggle(`MODAL_FORM_${toggleKey}`, false);
384
- }}
385
- />
386
- )
387
- }, [selected, fetchControl, formControl]);
388
-
389
-
390
-
391
- useEffect(() => {
392
- if(toggle[`REFRESH_${toggleKey}`] != undefined) reset();
393
- }, [toggle[`REFRESH_${toggleKey}`]]);
394
-
395
-
396
- return (
397
- <>
398
- <Suspense fallback={<div>Loading...</div>}>
399
- {title && <h1 className="text-lg lg:text-xl font-bold mb-2 lg:mb-4">{title}</h1>}
400
-
401
-
402
- <TableComponent
403
- id={tableKey}
404
- controlBar={controlBar?.map((cb) => {
405
- if (cb == "CREATE") {
406
- if (isSm) return
407
- return (
408
- <div className="pl-1.5 pr-3 mr-2 border-r" key="button-add">
409
- <ButtonComponent
410
- icon={faPlus}
411
- label="Tambah Data"
412
- size="sm"
413
- onClick={() => {
414
- setToggle(`MODAL_FORM_${toggleKey}`)
415
- setSelected(null)
416
- }}
417
- />
418
- </div>
419
- )
420
- }
421
-
422
- if (cb == "IMPORT") {
423
- return (
424
- <div className="px-1.5 rounded-md relative" key={"import"}>
425
- <ButtonComponent
426
- icon={faFileExcel}
427
- label="Import"
428
- variant="outline"
429
- className="!text-foreground"
430
- onClick={() => setToggle(`MODAL_IMPORT_${toggleKey}`)}
431
- size="sm"
432
- />
433
- </div>
434
- )
435
- }
436
-
437
- if (cb == "EXPORT") {
438
- return (
439
- <div className="px-1.5 rounded-md relative" key={"export-excel"}>
440
- <ButtonComponent
441
- icon={faFileExcel}
442
- label="Export"
443
- variant="outline"
444
- className="!text-foreground"
445
- onClick={() => setToggle(`MODAL_EXPORT_${toggleKey}`)}
446
- size="sm"
447
- />
448
- </div>
449
- )
450
- }
451
-
452
- if (cb == "PRINT") {
453
- return (
454
- <div className="px-1.5 rounded-md relative" key={"export-pdf"}>
455
- <ButtonComponent
456
- icon={faFilePdf}
457
- label="Cetak"
458
- variant="outline"
459
- className="!text-foreground"
460
- onClick={() => setToggle(`MODAL_PRINT_${toggleKey}`)}
461
- size="sm"
462
- />
463
- </div>
464
- )
465
- }
466
-
467
- return cb
468
- }) || [
469
- ...(!isSm ? [
470
- <div className="pl-1.5 pr-3 mr-2 border-r" key="button-add">
471
- <ButtonComponent
472
- icon={faPlus}
473
- label="Tambah Data"
474
- size="sm"
475
- onClick={() => {
476
- setToggle(`MODAL_FORM_${toggleKey}`)
477
- setSelected(null)
478
- }}
479
- />
480
- </div>
481
- ] : []),
482
- "SEARCH",
483
- ...(columns?.filter((c) => !!(c as { filterable?: any }).filterable)?.length ? ["FILTER"] : []),
484
- ...(columns?.filter((c) => !!(c as { sortable?: any }).sortable)?.length ? ["SORT"] : []),
485
- "SELECTABLE", "REFRESH",
486
- ]}
487
- columns={columns as TableColumnType[]}
488
- data={dataTables}
489
- onRowClick={onRowClick ? onRowClick : detailControl != false ? (e) => {
490
- setToggle(`MODAL_SHOW_${toggleKey}`)
491
- setSelected(e)
492
- } : undefined}
493
- actionBulking={actionBulkingControl}
494
- checks={checks || []}
495
- onChangeChecks={(e) => setChecks(e)}
496
- block={block}
497
- focus={focus}
498
- noIndex={noIndex}
499
- responsiveControl={responsiveControl ? {
500
- mobile: responsiveControl?.mobile == true ? {
501
- leftActionControl: (Array.isArray(actionControl) ? actionControl : (actionControl || actionControl == undefined) ? ['EDIT', "DELETE"] : []).includes('EDIT') ? {
502
- icon: faEdit,
503
- onAction: (item) => {
504
- setToggle(`MODAL_FORM_${toggleKey}`);
505
- item && setSelected?.(item);
506
- }
507
- } : undefined,
508
- rightActionControl: (Array.isArray(actionControl) ? actionControl : (actionControl || actionControl == undefined) ? ['EDIT', "DELETE"] : []).includes('DELETE') ? {
509
- icon: faTrash,
510
- onAction: (item) => {
511
- setToggle(`MODAL_DELETE_${toggleKey}`);
512
- item && setSelected?.(item);
513
- }
514
- } : undefined
515
- } : responsiveControl?.mobile || undefined,
516
- } : undefined}
517
- {...tableControl}
518
- />
519
-
520
- <IconButtonComponent
521
- icon={faPlus}
522
- className="fixed bottom-2 left-2 w-12 h-12 md:hidden"
523
- size="lg"
524
- rounded
525
- onClick={() => {
526
- setToggle(`MODAL_FORM_${toggleKey}`)
527
- setSelected(null)
528
- }}
529
- />
530
-
531
-
532
- {isSm ? (
533
- <BottomSheetComponent
534
- show={!!toggle[`MODAL_SHOW_${toggleKey}`]}
535
- onClose={() => setToggle(`MODAL_SHOW_${toggleKey}`, false)}
536
- className="bg-background"
537
- footer={renderTableAction(actionControl, undefined, {className: isSm ? "justify-end p-2 bg-background" : "justify-end", size: isSm ? "sm" : "md"})}
538
- size="98vh"
539
- >
540
- {detailPage}
541
- </BottomSheetComponent>
542
- ) : (
543
- <FloatingPageComponent
544
- show={!!toggle[`MODAL_SHOW_${toggleKey}`]}
545
- onClose={() => setToggle(`MODAL_SHOW_${toggleKey}`, false)}
546
- title="Detail"
547
- className="bg-background"
548
- footer={renderTableAction(actionControl, undefined, {className: isSm ? "justify-end p-2 bg-background" : "justify-end", size: isSm ? "sm" : "md"})}
549
- >
550
- {detailPage}
551
- </FloatingPageComponent>
552
- )}
553
-
554
-
555
- {isSm ? (
556
- <BottomSheetComponent
557
- show={!!toggle[`MODAL_FORM_${toggleKey}`]}
558
- onClose={() => setToggle(`MODAL_FORM_${toggleKey}`, false)}
559
- className={cn("bg-background", formControl?.modalControl?.className)}
560
- size="98vh"
561
- >
562
- <div className="p-4 h-[110vh]">
563
- {formPage}
564
- </div>
565
- </BottomSheetComponent>
566
- ) : (
567
- <FloatingPageComponent
568
- show={!!toggle[`MODAL_FORM_${toggleKey}`]}
569
- onClose={() => setToggle(`MODAL_FORM_${toggleKey}`, false)}
570
- title={!!selected ? "Ubah Data" : "Tambah Data"}
571
- className={cn("bg-background", formControl?.modalControl?.className)}
572
- >
573
- <div className="p-4">
574
- {formPage}
575
- </div>
576
- </FloatingPageComponent>
577
- )}
578
-
579
-
580
- {ExportExcel && (
581
- <FloatingPageComponent
582
- show={!!toggle[`MODAL_EXPORT_${toggleKey}`]}
583
- onClose={() => setToggle(`MODAL_EXPORT_${toggleKey}`, false)}
584
- title="Export Ke Excel"
585
- className="bg-background md:w-[1200px] max-w-[1200px]"
586
- >
587
- <ExportExcel
588
- fetchControl={fetchControl as FetchControlType}
589
- filename={"Export - " + title}
590
- columnControl={columns?.map((cc: TableSupervisionColumnProps) => ({
591
- label: cc.label || "",
592
- selector: cc.selector || "",
593
- status: cc.exportable === false ? "hidden" : cc.exportable === true ? "default" : typeof cc.exportable === "string" ? cc.exportable : undefined,
594
- }))}
595
- />
596
- </FloatingPageComponent>
597
- )}
598
-
599
-
600
- {ImportExcel && (
601
- <FloatingPageComponent
602
- show={!!toggle[`MODAL_IMPORT_${toggleKey}`]}
603
- onClose={() => setToggle(`MODAL_IMPORT_${toggleKey}`, false)}
604
- title="Import Dari Excel"
605
- className="bg-background md:w-[1200px] max-w-[1200px]"
606
- >
607
- <ImportExcel
608
- submitControl={importControl}
609
- fetchControl={
610
- (fetchControl as ApiType).path ? {
611
- path: (fetchControl as ApiType).path,
612
- } : undefined
613
- }
614
- columnControl={columns?.filter((cc: TableSupervisionColumnProps) => cc.importable !== false).map((cc: TableSupervisionColumnProps) => ({
615
- label: cc.label || "",
616
- selector: cc.selector || "",
617
- }))}
618
- />
619
- </FloatingPageComponent>
620
- )}
621
-
622
-
623
- {/* <FloatingPageComponent
624
- show={!!toggle[`MODAL_PRINT_${toggleKey}`]}
625
- onClose={() => setToggle(`MODAL_PRINT_${toggleKey}`, false)}
626
- title="Print PDF"
627
- className="bg-background md:w-[1200px] max-w-[1200px]"
628
- >
629
- <PrintTable
630
- fetchControl={fetchControl}
631
- columnControl={columns?.map((cc) => ({
632
- label: cc.label || "",
633
- selector: cc.selector || "",
634
- }))}
635
- title={"Print - " + title}
636
- />
637
- </FloatingPageComponent> */}
638
-
639
-
640
- <ModalConfirmComponent
641
- show={!!toggle[`MODAL_DELETE_${toggleKey}`]}
642
- onClose={() => setToggle(`MODAL_DELETE_${toggleKey}`, false)}
643
- icon={faQuestionCircle}
644
- title={`Menghapus Data?`}
645
- submitControl={{
646
- onSubmit: {
647
- ...((fetchControl as ApiType).path
648
- ? {path: `${(fetchControl as ApiType).path}/${(selected as { id: number })?.id || ""}`}
649
- : (fetchControl as ApiType).url ? {url: `${(fetchControl as ApiType).url}/${(selected as { id: number })?.id || ""}`}
650
- : { idb: { ...(fetchControl as ({ idb: UseResourceIdb }))?.idb, id: (selected as { id: number })?.id || "" }}
651
- ),
652
- method: "DELETE",
653
- },
654
- onSuccess: () => {
655
- reset();
656
- setToggle(`MODAL_DELETE_${toggleKey}`, false);
657
- },
658
- }}
659
- >
660
- {columns?.at(0)?.selector && selected ? (
661
- <p className="px-2 pb-2 text-sm text-center">Yakin menghapus <span className="font-semibold">&quot;{selected[columns?.at(0)?.selector || ""]}&quot;</span>?</p>
662
- ) : (
663
- <p className="px-2 pb-2 text-sm text-center">Yakin yang dihapus sudah benar?</p>
664
- )}
665
- </ModalConfirmComponent>
666
-
667
- {actionControl && Array.isArray(actionControl) && actionControl.filter((ac) => typeof ac == "object")?.map((ac, acKey) => {
668
- const submitControl = ac.modal?.submitControl?.onSubmit as ApiType;
669
- return (
670
- <ModalConfirmComponent
671
- key={acKey}
672
- show={!!toggle[`MODAL_${conversion.strSnake(ac.label).toUpperCase()}_${toggleKey}`]}
673
- onClose={() => setToggle(`MODAL_${conversion.strSnake(ac.label).toUpperCase()}_${toggleKey}`, false)}
674
- icon={ac?.modal?.icon || faQuestionCircle}
675
- title={ac?.modal?.title || ac.label}
676
- submitControl={{
677
- onSubmit: {
678
- ...(submitControl?.path
679
- ? {path: `${submitControl?.path}/${(selected as { id: number })?.id || ""}`}
680
- : {url: `${submitControl?.url}/${(selected as { id: number })?.id || ""}`}
681
- ),
682
- method: submitControl?.method || "POST",
683
- },
684
- onSuccess: () => {
685
- reset();
686
- setToggle(`MODAL_${conversion.strSnake(ac.label).toUpperCase()}_${conversion.strSnake(tableKey).toUpperCase()}`, false);
687
- setSelected(null)
688
- ac.modal?.submitControl?.onSuccess?.()
689
- },
690
- }}
691
- >{ac.modal?.children}</ModalConfirmComponent>
692
- )
693
- })}
694
- </Suspense>
695
- </>
696
- );
697
- }
1
+ "use client"
2
+
3
+ import { ReactNode, Suspense, useEffect, useMemo } from "react";
4
+ import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
5
+ import { faEdit, faFileExcel, faFilePdf, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
6
+ import { ApiType, cn, conversion, FetchControlType, registry, shortcut, ShortcutHandler, UseResourceIdb, UseResourceProps, useResponsive, useTable } from "@utils";
7
+ import { useToggleContext } from "@contexts";
8
+ import { FloatingPageComponent, FloatingPageProps, ButtonComponent, IconButtonComponent, TableColumnType, TableComponent, FormSupervisionComponent, FormType, ModalConfirmComponent, TypographyColumnComponent, ButtonProps, ModalConfirmProps, TableProps, ControlBarOptionType, BottomSheetComponent, SwipeActionType } from "@components";
9
+
10
+ const ExportExcel = registry.get("ExportExcel");
11
+ const ImportExcel = registry.get("ImportExcel");
12
+
13
+
14
+
15
+ export interface TableSupervisionColumnProps {
16
+ selector : string;
17
+ label ?: string;
18
+ width ?: string;
19
+ sortable ?: boolean;
20
+ searchable ?: boolean;
21
+ filterable ?: boolean | {
22
+ type : "text" | "number" | "currency" | "date";
23
+ } | {
24
+ type : "select";
25
+ options : { label: string; value: any }[];
26
+ };
27
+ accessCode ?: string;
28
+ item ?: (data: any) => string | ReactNode;
29
+ tip ?: string | ((data: any) => string);
30
+ exportable ?: boolean | "default" | "optional" | "hidden";
31
+ importable ?: boolean;
32
+ };
33
+
34
+ export interface TableSupervisionFormProps {
35
+ fields : string[] | (FormType & { visibility?: "*" | "create" | "update" })[];
36
+ defaultValue ?: (item: Record<string, any> | null) => Promise<Record<string, any>> | Record<string, any>;
37
+ payload ?: (values: any) => Promise<Record<string, any>> | object;
38
+ modalControl ?: Omit<FloatingPageProps, "show" | "onClose" | "children">;
39
+ contentType ?: "application/json" | "multipart/form-data";
40
+ };
41
+
42
+
43
+ export type TableSupervisionProps = {
44
+ fetchControl : UseResourceProps;
45
+ title ?: string;
46
+ id ?: string;
47
+ accessCode ?: number;
48
+ urlParam ?: boolean | { compressed ?: boolean }
49
+ onRowClick ?: (data: Record<string, any>) => void;
50
+ columnControl ?: string[] | TableSupervisionColumnProps[];
51
+ formControl ?: TableSupervisionFormProps;
52
+ detailControl ?: boolean
53
+ | (
54
+ | string
55
+ | { label: string, item: string | ((data: Record<string, any>) => ReactNode) }
56
+ | ((data: Record<string, any>) => ReactNode)
57
+ )[]
58
+ | ((data: Record<string, any>) => ReactNode);
59
+ actionControl ?: boolean | (
60
+ | 'EDIT' | 'DELETE' | {
61
+ label : string,
62
+ modal ?: Omit<ModalConfirmProps, "show" | "onClose">,
63
+ button ?: ButtonProps,
64
+ shortcut ?: { key: string, description: string },
65
+ } | ((
66
+ row : Record<string, any>,
67
+ setModal : (type: "EDIT" | "DELETE") => void,
68
+ setDataSelected ?: () => void,
69
+ setShortcut ?: (key: string, handler: ShortcutHandler, description?: string) => void
70
+ ) => ReactNode)
71
+ )[];
72
+ block ?: boolean,
73
+ noIndex ?: boolean;
74
+ actionBulkingControl ?: TableProps["actionBulking"],
75
+ controlBar ?: (ControlBarOptionType | "CREATE" | "IMPORT" | "EXPORT" | "PRINT")[];
76
+ responsiveControl ?: {
77
+ mobile ?: boolean | {
78
+ item ?: (item: Record<string, any>, key: number) => ReactNode,
79
+ leftActionControl ?: Omit<SwipeActionType, "onAction"> & { onAction?: (item: Record<string, any>, key?: number) => void },
80
+ rightActionControl ?: Omit<SwipeActionType, "onAction"> & { onAction?: (item: Record<string, any>, key?: number) => void },
81
+ }
82
+ };
83
+ importControl ?: FetchControlType;
84
+ };
85
+
86
+
87
+
88
+ export function TableSupervisionComponent({
89
+ title,
90
+ id,
91
+ fetchControl,
92
+ columnControl,
93
+ formControl,
94
+ onRowClick,
95
+ detailControl,
96
+ actionControl,
97
+ actionBulkingControl,
98
+ block,
99
+ controlBar,
100
+ noIndex,
101
+ responsiveControl,
102
+ urlParam,
103
+ importControl,
104
+ }: TableSupervisionProps) {
105
+ const { tableKey, tableControl, data, selected, setSelected, checks, setChecks, reset, focus, setFocus } = useTable(fetchControl, id, title, (urlParam || true))
106
+ const { setToggle, toggle } = useToggleContext()
107
+ const { isSm } = useResponsive();
108
+
109
+ const toggleKey = useMemo(() => conversion.strSnake(tableKey).toUpperCase(), [tableKey])
110
+
111
+
112
+ useEffect(() => {
113
+ if(data?.data?.length && !toggle[`MODAL_DELETE_${toggleKey}`] && !toggle[`MODAL_DELETE_${toggleKey}`] && !toggle[`MODAL_SHOW_${toggleKey}`]) {
114
+ shortcut.register("arrowdown", () => {
115
+ const max = data?.data?.length - 1;
116
+ setFocus(focus == null ? 0 : focus >= max ? max : (focus + 1))
117
+ }, "Pilih data kebawah")
118
+
119
+ shortcut.register("arrowup", () => {
120
+ setFocus(focus == null ? 0 : focus <= 0 ? 0 : (focus - 1))
121
+ }, "Pilih data keatas")
122
+
123
+ if(focus != null) {
124
+ shortcut.register("delete", () => {
125
+ setSelected(data?.data?.at(focus))
126
+ setToggle(`MODAL_DELETE_${toggleKey}`)
127
+ }, "Delete data yang dipilih")
128
+
129
+ shortcut.register(" ", () => {
130
+ setSelected(data?.data?.at(focus))
131
+ setToggle(`MODAL_FORM_${toggleKey}`)
132
+ }, "Edit data yang dipilih")
133
+
134
+ shortcut.register("enter", () => {
135
+ setSelected(data?.data?.at(focus))
136
+ setToggle(`MODAL_SHOW_${toggleKey}`)
137
+ }, "Detail data yang dipilih")
138
+
139
+ shortcut.register("escape", () => {
140
+ setFocus(null)
141
+ }, "Kembali")
142
+ }
143
+ }
144
+
145
+ return () => {
146
+ shortcut.unregister("arrowdown")
147
+ shortcut.unregister("arrowup")
148
+ shortcut.unregister("delete")
149
+ shortcut.unregister(" ")
150
+ shortcut.unregister("enter")
151
+ shortcut.unregister("escape")
152
+ }
153
+ }, [data?.data, actionControl, focus, toggle[`MODAL_DELETE_${toggleKey}`], toggle[`MODAL_DELETE_${toggleKey}`], toggle[`MODAL_SHOW_${toggleKey}`]])
154
+
155
+
156
+ // ============================
157
+ // ## Column preparation
158
+ // ============================
159
+ const columns = useMemo(() => {
160
+ return columnControl?.length ? columnControl.map((col) => {
161
+ if (typeof col === "string") {
162
+ return {
163
+ selector : col,
164
+ label : col,
165
+ };
166
+ } else {
167
+ return { ...col };
168
+ }
169
+ })
170
+ : data?.columns || data?.data?.at(0) ? Object.keys(data.data[0]).map((col) => {
171
+ return {
172
+ selector : col,
173
+ label : col,
174
+ };
175
+ })
176
+ : [];
177
+ }, [columnControl, data]);
178
+
179
+
180
+
181
+ const renderTableAction = (
182
+ actions : TableSupervisionProps["actionControl"],
183
+ item ?: Record<string, any>,
184
+ options ?: {size?: ButtonProps['size'], className?: string}
185
+ ) => {
186
+ return (
187
+ <>
188
+ <div className={cn("flex items-center gap-2", options?.className)}>
189
+ {(Array.isArray(actions) ? actions : (actions || actions == undefined) ? ['EDIT', "DELETE"] : [])?.map((action, key) => {
190
+ if(action == "EDIT") {
191
+ return (
192
+ <ButtonComponent
193
+ key={key}
194
+ icon={faEdit}
195
+ label={"Ubah"}
196
+ variant="outline"
197
+ paint="warning"
198
+ size={options?.size || "xs"}
199
+ rounded
200
+ onClick={() => {
201
+ setToggle(`MODAL_FORM_${toggleKey}`);
202
+ item && setSelected?.(item);
203
+ }}
204
+ />
205
+ )
206
+ }
207
+
208
+ if(action == "DELETE") {
209
+ return (
210
+ <ButtonComponent
211
+ key={key}
212
+ icon={faTrash}
213
+ label={"Hapus"}
214
+ variant="outline"
215
+ paint="danger"
216
+ size={options?.size || "xs"}
217
+ rounded
218
+ onClick={() => {
219
+ setToggle(`MODAL_DELETE_${toggleKey}`);
220
+ item && setSelected?.(item);
221
+ }}
222
+ />
223
+ )
224
+ }
225
+
226
+ if(typeof action == "object") {
227
+ <ButtonComponent
228
+ key={`action-object-${key}`}
229
+ label={action?.button?.label || action?.label}
230
+ variant={action?.button?.variant || "outline"}
231
+ paint={action?.button?.paint || "primary"}
232
+ size={action?.button?.size || options?.size || "xs"}
233
+ rounded={action?.button?.rounded || true}
234
+ onClick={() => {
235
+ if (action?.button?.onClick) {
236
+ action?.button?.onClick(item)
237
+ } else {
238
+ setToggle(`MODAL_${conversion.strSnake(action?.label).toUpperCase()}_${toggleKey}`);
239
+ item && setSelected?.(item);
240
+ }
241
+ }}
242
+ {...action.button}
243
+ />
244
+ }
245
+
246
+ if(typeof action == "function") {
247
+ return (
248
+ <span key={`action-fn-${key}`}>
249
+ {action(item || {}, (type: "EDIT" | "DELETE") => {
250
+ if(type == "EDIT") {
251
+ setToggle(`MODAL_FORM_${toggleKey}`);
252
+ item && setSelected?.(item);
253
+ }
254
+
255
+ if (type == "DELETE") {
256
+ setToggle(`MODAL_DELETE_${toggleKey}`);
257
+ item && setSelected?.(item);
258
+ }
259
+ })}
260
+ </span>
261
+ )
262
+ }
263
+
264
+ return <span key={`action-default-${key}`}></span>;
265
+ })}
266
+ </div>
267
+ </>
268
+ )
269
+ }
270
+
271
+
272
+ // ============================
273
+ // ## Data table preparation
274
+ // ============================
275
+ const dataTables = useMemo(() => {
276
+ return data?.data?.map((row: object) => {
277
+ return {
278
+ ...row,
279
+ action: renderTableAction(actionControl, row),
280
+ };
281
+ });
282
+ }, [actionControl, data]);
283
+
284
+
285
+ // ============================
286
+ // ## Render detail page
287
+ // ============================
288
+ const detailPage = useMemo(() => {
289
+ return (
290
+ <div className="p-4">
291
+ <div className={cn(
292
+ "flex flex-col gap-y-4",
293
+ )}>
294
+ {!!selected && (typeof detailControl === "object" && detailControl?.length ? detailControl?.map((column, key) => {
295
+ if (typeof column === "string") {
296
+ return (<TypographyColumnComponent
297
+ key={key}
298
+ title={columns?.find((c) => c.selector == column)?.label}
299
+ content={selected[column]}
300
+ />)
301
+ } else if (typeof column === "object") {
302
+ return (<TypographyColumnComponent
303
+ key={key}
304
+ title={column?.label}
305
+ content={typeof column?.item === "string" ? selected[column?.item] : column?.item(selected)}
306
+ />)
307
+ } else {
308
+ return column?.(selected)
309
+ }
310
+ }) : typeof detailControl == "function" ? detailControl(selected) : columns?.map((column, key) => (
311
+ <TypographyColumnComponent
312
+ key={key}
313
+ title={column.label}
314
+ content={selected[column.selector]}
315
+ />
316
+ )))}
317
+ </div>
318
+ </div>
319
+ )
320
+ }, [selected, detailControl]);
321
+
322
+
323
+
324
+
325
+ // ============================
326
+ // ## Form preparation
327
+ // ============================
328
+ const fields = useMemo(() => {
329
+ return formControl?.fields?.length ? formControl?.fields.map((form) => {
330
+ return typeof form === "string" ? {
331
+ col : 12,
332
+ type : "text",
333
+ construction : {
334
+ name : form,
335
+ label : form,
336
+ },
337
+ } : { ...form };
338
+ }) : data?.forms || data?.columns || columnControl?.map((col) => {
339
+ return {
340
+ col : 12,
341
+ type : "text",
342
+ construction : {
343
+ name : typeof col == "string" ? col : col?.selector,
344
+ label : typeof col == "string" ? col : col?.label,
345
+ placeholder : `Masukkan ${ typeof col == "string" ? col : col?.label}...`,
346
+ },
347
+ };
348
+ }) || (data?.data?.at(0) ? Object.keys(data.data[0]).map((col) => {
349
+ return {
350
+ col : 12,
351
+ type : "text",
352
+ construction : {
353
+ name : col,
354
+ label : col,
355
+ placeholder : `Masukkan ${col}...`,
356
+ },
357
+ };
358
+ })
359
+ : []);
360
+ }, [formControl, data]);
361
+
362
+
363
+
364
+ // ============================
365
+ // ## Render form page
366
+ // ============================
367
+ const formPage = useMemo(async () => {
368
+ return (
369
+ <FormSupervisionComponent
370
+ submitControl={(fetchControl as ApiType).path ? {
371
+ path: `${(fetchControl as ApiType).path}/${(selected as { id: number })?.id || "" }`,
372
+ method: !(selected as { id: number })?.id ? "POST" : "PUT",
373
+ } : (fetchControl as ApiType).url ? {
374
+ url: `${(fetchControl as ApiType).url}/${(selected as { id: number })?.id || ""}`,
375
+ method: !(selected as { id: number })?.id ? "POST" : "PUT",
376
+ } : { idb: (fetchControl as ({ idb: UseResourceIdb }))?.idb }
377
+ }
378
+ fields={fields as FormType[]}
379
+ defaultValue={formControl?.defaultValue ? await formControl?.defaultValue(selected || null) : selected}
380
+ payload={formControl?.payload}
381
+ onSuccess={() => {
382
+ reset();
383
+ setToggle(`MODAL_FORM_${toggleKey}`, false);
384
+ }}
385
+ />
386
+ )
387
+ }, [selected, fetchControl, formControl]);
388
+
389
+
390
+
391
+ useEffect(() => {
392
+ if(toggle[`REFRESH_${toggleKey}`] != undefined) reset();
393
+ }, [toggle[`REFRESH_${toggleKey}`]]);
394
+
395
+
396
+ return (
397
+ <>
398
+ <Suspense fallback={<div>Loading...</div>}>
399
+ {title && <h1 className="text-lg lg:text-xl font-bold mb-2 lg:mb-4">{title}</h1>}
400
+
401
+
402
+ <TableComponent
403
+ id={tableKey}
404
+ controlBar={controlBar?.map((cb) => {
405
+ if (cb == "CREATE") {
406
+ if (isSm) return
407
+ return (
408
+ <div className="pl-1.5 pr-3 mr-2 border-r" key="button-add">
409
+ <ButtonComponent
410
+ icon={faPlus}
411
+ label="Tambah Data"
412
+ size="sm"
413
+ onClick={() => {
414
+ setToggle(`MODAL_FORM_${toggleKey}`)
415
+ setSelected(null)
416
+ }}
417
+ />
418
+ </div>
419
+ )
420
+ }
421
+
422
+ if (cb == "IMPORT") {
423
+ return (
424
+ <div className="px-1.5 rounded-md relative" key={"import"}>
425
+ <ButtonComponent
426
+ icon={faFileExcel}
427
+ label="Import"
428
+ variant="outline"
429
+ className="!text-foreground"
430
+ onClick={() => setToggle(`MODAL_IMPORT_${toggleKey}`)}
431
+ size="sm"
432
+ />
433
+ </div>
434
+ )
435
+ }
436
+
437
+ if (cb == "EXPORT") {
438
+ return (
439
+ <div className="px-1.5 rounded-md relative" key={"export-excel"}>
440
+ <ButtonComponent
441
+ icon={faFileExcel}
442
+ label="Export"
443
+ variant="outline"
444
+ className="!text-foreground"
445
+ onClick={() => setToggle(`MODAL_EXPORT_${toggleKey}`)}
446
+ size="sm"
447
+ />
448
+ </div>
449
+ )
450
+ }
451
+
452
+ if (cb == "PRINT") {
453
+ return (
454
+ <div className="px-1.5 rounded-md relative" key={"export-pdf"}>
455
+ <ButtonComponent
456
+ icon={faFilePdf}
457
+ label="Cetak"
458
+ variant="outline"
459
+ className="!text-foreground"
460
+ onClick={() => setToggle(`MODAL_PRINT_${toggleKey}`)}
461
+ size="sm"
462
+ />
463
+ </div>
464
+ )
465
+ }
466
+
467
+ return cb
468
+ }) || [
469
+ ...(!isSm ? [
470
+ <div className="pl-1.5 pr-3 mr-2 border-r" key="button-add">
471
+ <ButtonComponent
472
+ icon={faPlus}
473
+ label="Tambah Data"
474
+ size="sm"
475
+ onClick={() => {
476
+ setToggle(`MODAL_FORM_${toggleKey}`)
477
+ setSelected(null)
478
+ }}
479
+ />
480
+ </div>
481
+ ] : []),
482
+ "SEARCH",
483
+ ...(columns?.filter((c) => !!(c as { filterable?: any }).filterable)?.length ? ["FILTER"] : []),
484
+ ...(columns?.filter((c) => !!(c as { sortable?: any }).sortable)?.length ? ["SORT"] : []),
485
+ "SELECTABLE", "REFRESH",
486
+ ]}
487
+ columns={columns as TableColumnType[]}
488
+ data={dataTables}
489
+ onRowClick={onRowClick ? onRowClick : detailControl != false ? (e) => {
490
+ setToggle(`MODAL_SHOW_${toggleKey}`)
491
+ setSelected(e)
492
+ } : undefined}
493
+ actionBulking={actionBulkingControl}
494
+ checks={checks || []}
495
+ onChangeChecks={(e) => setChecks(e)}
496
+ block={block}
497
+ focus={focus}
498
+ noIndex={noIndex}
499
+ responsiveControl={responsiveControl ? {
500
+ mobile: responsiveControl?.mobile == true ? {
501
+ leftActionControl: (Array.isArray(actionControl) ? actionControl : (actionControl || actionControl == undefined) ? ['EDIT', "DELETE"] : []).includes('EDIT') ? {
502
+ icon: faEdit,
503
+ onAction: (item) => {
504
+ setToggle(`MODAL_FORM_${toggleKey}`);
505
+ item && setSelected?.(item);
506
+ }
507
+ } : undefined,
508
+ rightActionControl: (Array.isArray(actionControl) ? actionControl : (actionControl || actionControl == undefined) ? ['EDIT', "DELETE"] : []).includes('DELETE') ? {
509
+ icon: faTrash,
510
+ onAction: (item) => {
511
+ setToggle(`MODAL_DELETE_${toggleKey}`);
512
+ item && setSelected?.(item);
513
+ }
514
+ } : undefined
515
+ } : responsiveControl?.mobile || undefined,
516
+ } : undefined}
517
+ {...tableControl}
518
+ />
519
+
520
+ <IconButtonComponent
521
+ icon={faPlus}
522
+ className="fixed bottom-2 left-2 w-12 h-12 md:hidden"
523
+ size="lg"
524
+ rounded
525
+ onClick={() => {
526
+ setToggle(`MODAL_FORM_${toggleKey}`)
527
+ setSelected(null)
528
+ }}
529
+ />
530
+
531
+
532
+ {isSm ? (
533
+ <BottomSheetComponent
534
+ show={!!toggle[`MODAL_SHOW_${toggleKey}`]}
535
+ onClose={() => setToggle(`MODAL_SHOW_${toggleKey}`, false)}
536
+ className="bg-background"
537
+ footer={renderTableAction(actionControl, undefined, {className: isSm ? "justify-end p-2 bg-background" : "justify-end", size: isSm ? "sm" : "md"})}
538
+ size="98vh"
539
+ >
540
+ {detailPage}
541
+ </BottomSheetComponent>
542
+ ) : (
543
+ <FloatingPageComponent
544
+ show={!!toggle[`MODAL_SHOW_${toggleKey}`]}
545
+ onClose={() => setToggle(`MODAL_SHOW_${toggleKey}`, false)}
546
+ title="Detail"
547
+ className="bg-background"
548
+ footer={renderTableAction(actionControl, undefined, {className: isSm ? "justify-end p-2 bg-background" : "justify-end", size: isSm ? "sm" : "md"})}
549
+ >
550
+ {detailPage}
551
+ </FloatingPageComponent>
552
+ )}
553
+
554
+
555
+ {isSm ? (
556
+ <BottomSheetComponent
557
+ show={!!toggle[`MODAL_FORM_${toggleKey}`]}
558
+ onClose={() => setToggle(`MODAL_FORM_${toggleKey}`, false)}
559
+ className={cn("bg-background", formControl?.modalControl?.className)}
560
+ size="98vh"
561
+ >
562
+ <div className="p-4 h-[110vh]">
563
+ {formPage}
564
+ </div>
565
+ </BottomSheetComponent>
566
+ ) : (
567
+ <FloatingPageComponent
568
+ show={!!toggle[`MODAL_FORM_${toggleKey}`]}
569
+ onClose={() => setToggle(`MODAL_FORM_${toggleKey}`, false)}
570
+ title={!!selected ? "Ubah Data" : "Tambah Data"}
571
+ className={cn("bg-background", formControl?.modalControl?.className)}
572
+ >
573
+ <div className="p-4">
574
+ {formPage}
575
+ </div>
576
+ </FloatingPageComponent>
577
+ )}
578
+
579
+
580
+ {ExportExcel && (
581
+ <FloatingPageComponent
582
+ show={!!toggle[`MODAL_EXPORT_${toggleKey}`]}
583
+ onClose={() => setToggle(`MODAL_EXPORT_${toggleKey}`, false)}
584
+ title="Export Ke Excel"
585
+ className="bg-background md:w-[1200px] max-w-[1200px]"
586
+ >
587
+ <ExportExcel
588
+ fetchControl={fetchControl as FetchControlType}
589
+ filename={"Export - " + title}
590
+ columnControl={columns?.map((cc: TableSupervisionColumnProps) => ({
591
+ label: cc.label || "",
592
+ selector: cc.selector || "",
593
+ status: cc.exportable === false ? "hidden" : cc.exportable === true ? "default" : typeof cc.exportable === "string" ? cc.exportable : undefined,
594
+ }))}
595
+ />
596
+ </FloatingPageComponent>
597
+ )}
598
+
599
+
600
+ {ImportExcel && (
601
+ <FloatingPageComponent
602
+ show={!!toggle[`MODAL_IMPORT_${toggleKey}`]}
603
+ onClose={() => setToggle(`MODAL_IMPORT_${toggleKey}`, false)}
604
+ title="Import Dari Excel"
605
+ className="bg-background md:w-[1200px] max-w-[1200px]"
606
+ >
607
+ <ImportExcel
608
+ submitControl={importControl}
609
+ fetchControl={
610
+ (fetchControl as ApiType).path ? {
611
+ path: (fetchControl as ApiType).path,
612
+ } : undefined
613
+ }
614
+ columnControl={columns?.filter((cc: TableSupervisionColumnProps) => cc.importable !== false).map((cc: TableSupervisionColumnProps) => ({
615
+ label: cc.label || "",
616
+ selector: cc.selector || "",
617
+ }))}
618
+ />
619
+ </FloatingPageComponent>
620
+ )}
621
+
622
+
623
+ {/* <FloatingPageComponent
624
+ show={!!toggle[`MODAL_PRINT_${toggleKey}`]}
625
+ onClose={() => setToggle(`MODAL_PRINT_${toggleKey}`, false)}
626
+ title="Print PDF"
627
+ className="bg-background md:w-[1200px] max-w-[1200px]"
628
+ >
629
+ <PrintTable
630
+ fetchControl={fetchControl}
631
+ columnControl={columns?.map((cc) => ({
632
+ label: cc.label || "",
633
+ selector: cc.selector || "",
634
+ }))}
635
+ title={"Print - " + title}
636
+ />
637
+ </FloatingPageComponent> */}
638
+
639
+
640
+ <ModalConfirmComponent
641
+ show={!!toggle[`MODAL_DELETE_${toggleKey}`]}
642
+ onClose={() => setToggle(`MODAL_DELETE_${toggleKey}`, false)}
643
+ icon={faQuestionCircle}
644
+ title={`Menghapus Data?`}
645
+ submitControl={{
646
+ onSubmit: {
647
+ ...((fetchControl as ApiType).path
648
+ ? {path: `${(fetchControl as ApiType).path}/${(selected as { id: number })?.id || ""}`}
649
+ : (fetchControl as ApiType).url ? {url: `${(fetchControl as ApiType).url}/${(selected as { id: number })?.id || ""}`}
650
+ : { idb: { ...(fetchControl as ({ idb: UseResourceIdb }))?.idb, id: (selected as { id: number })?.id || "" }}
651
+ ),
652
+ method: "DELETE",
653
+ },
654
+ onSuccess: () => {
655
+ reset();
656
+ setToggle(`MODAL_DELETE_${toggleKey}`, false);
657
+ },
658
+ }}
659
+ >
660
+ {columns?.at(0)?.selector && selected ? (
661
+ <p className="px-2 pb-2 text-sm text-center">Yakin menghapus <span className="font-semibold">&quot;{selected[columns?.at(0)?.selector || ""]}&quot;</span>?</p>
662
+ ) : (
663
+ <p className="px-2 pb-2 text-sm text-center">Yakin yang dihapus sudah benar?</p>
664
+ )}
665
+ </ModalConfirmComponent>
666
+
667
+ {actionControl && Array.isArray(actionControl) && actionControl.filter((ac) => typeof ac == "object")?.map((ac, acKey) => {
668
+ const submitControl = ac.modal?.submitControl?.onSubmit as ApiType;
669
+ return (
670
+ <ModalConfirmComponent
671
+ key={acKey}
672
+ show={!!toggle[`MODAL_${conversion.strSnake(ac.label).toUpperCase()}_${toggleKey}`]}
673
+ onClose={() => setToggle(`MODAL_${conversion.strSnake(ac.label).toUpperCase()}_${toggleKey}`, false)}
674
+ icon={ac?.modal?.icon || faQuestionCircle}
675
+ title={ac?.modal?.title || ac.label}
676
+ submitControl={{
677
+ onSubmit: {
678
+ ...(submitControl?.path
679
+ ? {path: `${submitControl?.path}/${(selected as { id: number })?.id || ""}`}
680
+ : {url: `${submitControl?.url}/${(selected as { id: number })?.id || ""}`}
681
+ ),
682
+ method: submitControl?.method || "POST",
683
+ },
684
+ onSuccess: () => {
685
+ reset();
686
+ setToggle(`MODAL_${conversion.strSnake(ac.label).toUpperCase()}_${conversion.strSnake(tableKey).toUpperCase()}`, false);
687
+ setSelected(null)
688
+ ac.modal?.submitControl?.onSuccess?.()
689
+ },
690
+ }}
691
+ >{ac.modal?.children}</ModalConfirmComponent>
692
+ )
693
+ })}
694
+ </Suspense>
695
+ </>
696
+ );
697
+ }