@ramathibodi/nuxt-commons 0.1.74 → 0.1.75

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 (96) hide show
  1. package/README.md +115 -115
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/components/Alert.vue +58 -58
  4. package/dist/runtime/components/BarcodeReader.vue +130 -130
  5. package/dist/runtime/components/ExportCSV.vue +110 -110
  6. package/dist/runtime/components/FileBtn.vue +79 -79
  7. package/dist/runtime/components/ImportCSV.vue +151 -151
  8. package/dist/runtime/components/MrzReader.vue +168 -168
  9. package/dist/runtime/components/SplitterPanel.vue +67 -67
  10. package/dist/runtime/components/TabsGroup.vue +39 -39
  11. package/dist/runtime/components/TextBarcode.vue +66 -66
  12. package/dist/runtime/components/device/IdCardButton.vue +95 -95
  13. package/dist/runtime/components/device/IdCardWebSocket.vue +207 -207
  14. package/dist/runtime/components/device/Scanner.vue +350 -350
  15. package/dist/runtime/components/dialog/Confirm.vue +112 -112
  16. package/dist/runtime/components/dialog/Host.vue +88 -88
  17. package/dist/runtime/components/dialog/Index.vue +84 -84
  18. package/dist/runtime/components/dialog/Loading.vue +51 -51
  19. package/dist/runtime/components/dialog/default/Confirm.vue +112 -112
  20. package/dist/runtime/components/dialog/default/Loading.vue +60 -60
  21. package/dist/runtime/components/dialog/default/Notify.vue +82 -82
  22. package/dist/runtime/components/dialog/default/Printing.vue +46 -46
  23. package/dist/runtime/components/dialog/default/VerifyUser.vue +144 -144
  24. package/dist/runtime/components/document/Form.vue +50 -50
  25. package/dist/runtime/components/document/TemplateBuilder.vue +536 -536
  26. package/dist/runtime/components/form/ActionPad.vue +156 -156
  27. package/dist/runtime/components/form/Birthdate.vue +116 -116
  28. package/dist/runtime/components/form/CheckboxGroup.vue +99 -99
  29. package/dist/runtime/components/form/CodeEditor.vue +45 -45
  30. package/dist/runtime/components/form/Date.vue +270 -270
  31. package/dist/runtime/components/form/DateTime.vue +220 -220
  32. package/dist/runtime/components/form/Dialog.vue +178 -178
  33. package/dist/runtime/components/form/EditPad.vue +157 -157
  34. package/dist/runtime/components/form/File.vue +295 -295
  35. package/dist/runtime/components/form/Hidden.vue +44 -44
  36. package/dist/runtime/components/form/Iterator.vue +538 -538
  37. package/dist/runtime/components/form/Login.vue +143 -143
  38. package/dist/runtime/components/form/Pad.vue +399 -399
  39. package/dist/runtime/components/form/SignPad.vue +226 -226
  40. package/dist/runtime/components/form/System.vue +34 -34
  41. package/dist/runtime/components/form/Table.vue +391 -391
  42. package/dist/runtime/components/form/TableData.vue +236 -236
  43. package/dist/runtime/components/form/Time.vue +177 -177
  44. package/dist/runtime/components/form/images/Capture.vue +245 -245
  45. package/dist/runtime/components/form/images/Edit.vue +133 -133
  46. package/dist/runtime/components/form/images/Field.vue +331 -331
  47. package/dist/runtime/components/form/images/Pad.vue +54 -54
  48. package/dist/runtime/components/label/Date.vue +37 -37
  49. package/dist/runtime/components/label/DateAgo.vue +102 -102
  50. package/dist/runtime/components/label/DateCount.vue +152 -152
  51. package/dist/runtime/components/label/Field.vue +111 -111
  52. package/dist/runtime/components/label/FormatMoney.vue +37 -37
  53. package/dist/runtime/components/label/Mask.vue +46 -46
  54. package/dist/runtime/components/label/Object.vue +21 -21
  55. package/dist/runtime/components/master/Autocomplete.vue +89 -89
  56. package/dist/runtime/components/master/Combobox.vue +88 -88
  57. package/dist/runtime/components/master/RadioGroup.vue +90 -90
  58. package/dist/runtime/components/master/Select.vue +70 -70
  59. package/dist/runtime/components/master/label.vue +55 -55
  60. package/dist/runtime/components/model/Autocomplete.vue +91 -91
  61. package/dist/runtime/components/model/Combobox.vue +90 -90
  62. package/dist/runtime/components/model/Pad.vue +114 -114
  63. package/dist/runtime/components/model/Select.vue +78 -84
  64. package/dist/runtime/components/model/Table.vue +370 -370
  65. package/dist/runtime/components/model/iterator.vue +497 -497
  66. package/dist/runtime/components/model/label.vue +58 -58
  67. package/dist/runtime/components/pdf/Print.vue +75 -75
  68. package/dist/runtime/components/pdf/View.vue +146 -146
  69. package/dist/runtime/composables/dialog.d.ts +1 -1
  70. package/dist/runtime/composables/graphql.d.ts +1 -1
  71. package/dist/runtime/composables/graphqlModel.d.ts +9 -9
  72. package/dist/runtime/composables/graphqlModelItem.d.ts +7 -7
  73. package/dist/runtime/composables/graphqlModelOperation.d.ts +6 -6
  74. package/dist/runtime/composables/userPermission.d.ts +1 -1
  75. package/dist/runtime/labs/Calendar.vue +99 -99
  76. package/dist/runtime/labs/form/EditMobile.vue +152 -152
  77. package/dist/runtime/labs/form/TextFieldMask.vue +43 -43
  78. package/dist/runtime/plugins/clientConfig.d.ts +1 -1
  79. package/dist/runtime/plugins/default.d.ts +1 -1
  80. package/dist/runtime/plugins/dialogManager.d.ts +1 -1
  81. package/dist/runtime/plugins/permission.d.ts +1 -1
  82. package/dist/runtime/types/alert.d.ts +11 -11
  83. package/dist/runtime/types/clientConfig.d.ts +13 -13
  84. package/dist/runtime/types/dialogManager.d.ts +35 -35
  85. package/dist/runtime/types/formDialog.d.ts +5 -5
  86. package/dist/runtime/types/graphqlOperation.d.ts +23 -23
  87. package/dist/runtime/types/menu.d.ts +31 -31
  88. package/dist/runtime/types/modules.d.ts +7 -7
  89. package/dist/runtime/types/permission.d.ts +13 -13
  90. package/package.json +131 -131
  91. package/scripts/enrich-vue-docs-from-ai.mjs +197 -197
  92. package/scripts/generate-ai-summary.mjs +321 -321
  93. package/scripts/generate-composables-md.mjs +129 -129
  94. package/scripts/postInstall.cjs +70 -70
  95. package/templates/.codegen/codegen.ts +32 -32
  96. package/templates/.codegen/plugin-schema-object.js +161 -161
@@ -1,391 +1,391 @@
1
- <script lang="ts" setup>
2
- /**
3
- * FormTable is a schema-driven form field component that binds model data, renders field UI, and emits normalized updates.
4
- * This doc block is consumed by vue-docgen for generated API documentation.
5
- */
6
- import {VDataTable} from 'vuetify/components/VDataTable'
7
- import {VInput} from 'vuetify/components/VInput'
8
- import {computed, defineOptions,defineExpose, nextTick, ref, useAttrs, watch, useTemplateRef} from 'vue'
9
- import {omit} from 'lodash-es'
10
- import {useDialog} from "../../composables/dialog"
11
- import type {FormDialogCallback} from '../../types/formDialog'
12
- import { useLocalStorageModel, type PersistSlimProps } from '../../composables/localStorageModel'
13
-
14
- defineOptions({
15
- inheritAttrs: false,
16
- })
17
-
18
- interface Props extends /* @vue-ignore */ InstanceType<typeof VDataTable['$props']> {
19
- title: string // Toolbar title and default export file name shown for this table section.
20
- noDataText?: string // Text rendered by the data table when `items` is empty after filtering/loading.
21
- modelValue?: Record<string, any>[] // Source rows from parent; component normalizes, mutates order, then emits updated rows.
22
- modelKey?: string // Unique row key used for update/delete/reorder; missing keys are auto-generated incrementally.
23
- dialogWidth?: string | number // Passed to row edit dialog width to control normal (non-fullscreen) modal size.
24
- dialogMaxWidth?: string | number // Upper width bound for the edit dialog on large screens.
25
- dialogHeight?: string | number // Passed to row edit dialog height to control vertical editing space.
26
- dialogMaxHeight?: string | number // Upper height bound for the edit dialog before internal scrolling.
27
- dialogFullscreen?: boolean // Default open mode for row editor dialog (`true` opens full screen).
28
- initialData?: Record<string, any> // Seed object merged into new rows before user input.
29
- toolbarColor?: string // Vuetify color applied to toolbar and default action buttons.
30
- importable?: boolean // Enables Import button and `@import` flow for adding multiple rows from file.
31
- exportable?: boolean // Enables Export button for current `items` dataset.
32
- insertable?: boolean // Enables Add button and creation flow for new rows.
33
- searchable?: boolean // Shows toolbar search input and binds keyword to table filtering.
34
- inputPad?: boolean // Enables embedded pad editing workflow instead of relying only on dialog.
35
- inputPadOnly?: boolean // Uses pad editor exclusively and skips dialog open/close behavior.
36
- saveAndStay?: boolean // Forwards to child editor to keep it open after save (batch editing).
37
- stringFields?: Array<string> // Dot-path fields preserved as string during import/export flattening and parsing.
38
- }
39
-
40
- /**
41
- * Public props accepted by FormTable.
42
- * Document each prop field with intent, defaults, and side effects for clear generated docs.
43
- */
44
- const props = withDefaults(defineProps<Props & PersistSlimProps>(), {
45
- noDataText: 'ไม่พบข้อมูล',
46
- dialogFullscreen: false,
47
- modelKey: 'id',
48
- toolbarColor: 'primary',
49
- importable: true,
50
- exportable: true,
51
- insertable: true,
52
- searchable: true,
53
- inputPad: false,
54
- inputPadOnly: false,
55
- saveAndStay: false,
56
- stringFields: ()=>[],
57
- })
58
-
59
- /**
60
- * Custom events emitted by FormTable.
61
- * Parents can listen to these events to react to user actions and internal state changes.
62
- */
63
- const emit = defineEmits(['update:modelValue','open:dialog','close:dialog'])
64
- const attrs = useAttrs()
65
- const plainAttrs = computed(() => {
66
- return omit(attrs, ['modelValue', 'onUpdate:modelValue'])
67
- })
68
-
69
- const inputRef = useTemplateRef<VInput>("inputRef")
70
-
71
- const items = ref<Record<string, any>[]>([])
72
- const search = ref<string>()
73
- const currentItem = ref<Record<string, any> | undefined>(undefined)
74
-
75
- useLocalStorageModel(items,props)
76
-
77
- function setSearch(keyword: string) {
78
- search.value = keyword
79
- }
80
-
81
- const isDialogOpen = ref<boolean>(false)
82
-
83
- watch(() => props.modelValue, (newValue) => {
84
- if (!Array.isArray(newValue) || !newValue.every(item => typeof item === 'object')) {
85
- items.value = []
86
- }
87
- else {
88
- let maxKey = 0
89
-
90
- newValue.forEach((item) => {
91
- if (!item.hasOwnProperty(props.modelKey)) {
92
- maxKey = Math.max(maxKey, ...newValue.map(i => i[props.modelKey] || 0))
93
- item[props.modelKey] = maxKey + 1
94
- }
95
- })
96
-
97
- items.value = newValue
98
- }
99
- }, { immediate: true })
100
-
101
- watch(items, (newValue) => {
102
- emit('update:modelValue', newValue)
103
- }, { deep: true })
104
-
105
-
106
- function createItem(item: Record<string, any>, callback?: FormDialogCallback) {
107
- if (items.value.length > 0) item[props.modelKey] = Math.max(...items.value.map(i => i[props.modelKey] || 0)) + 1
108
- else item[props.modelKey] = 1
109
-
110
- items.value.push(item)
111
-
112
- if (callback && callback.setData) callback.setData(item)
113
- if (callback) callback.done()
114
- }
115
-
116
- function importItems(importItems: Record<string, any>[], callback?: FormDialogCallback) {
117
- importItems.forEach((item) => {
118
- createItem(item)
119
- })
120
- if (callback) callback.done()
121
- }
122
-
123
- function updateItem(newItem: Record<string, any>, callback?: FormDialogCallback) {
124
- const index = items.value.findIndex(item => item[props.modelKey] === newItem[props.modelKey])
125
-
126
- if (index !== -1) {
127
- items.value[index] = newItem
128
- }
129
-
130
- if (callback && callback.setData) callback.setData(newItem)
131
- if (callback) callback.done()
132
- }
133
-
134
- function moveUpItem(currentItem: Record<string, any>, callback?: FormDialogCallback) {
135
- const index = items.value.findIndex(item => item[props.modelKey] === currentItem[props.modelKey])
136
-
137
- if (index > 0) {
138
- const temp = items.value[index - 1]
139
- items.value[index - 1] = items.value[index]
140
- items.value[index] = temp
141
- }
142
-
143
- if (callback) callback.done()
144
- }
145
-
146
- function moveDownItem(currentItem: Record<string, any>, callback?: FormDialogCallback) {
147
- const index = items.value.findIndex(item => item[props.modelKey] === currentItem[props.modelKey])
148
-
149
- if (index >= 0 && index < items.value.length - 1) {
150
- const temp = items.value[index + 1]
151
- items.value[index + 1] = items.value[index]
152
- items.value[index] = temp
153
- }
154
-
155
- if (callback) callback.done()
156
- }
157
-
158
- function moveToItem(currentItem: Record<string, any>, callback?: FormDialogCallback) {
159
- const index = items.value.findIndex(item => item[props.modelKey] === currentItem[props.modelKey]);
160
-
161
- if (index !== -1) {
162
- const newPosition = prompt("Enter the new position (0-based index):");
163
- const parsedPosition = parseInt(<string>newPosition, 10);
164
-
165
- if (isNaN(parsedPosition) || parsedPosition < 0 || parsedPosition >= items.value.length) {
166
- alert("Invalid position entered. Please enter a number between 0 and " + (items.value.length - 1));
167
- return
168
- }
169
-
170
- const [temp] = items.value.splice(index, 1);
171
-
172
- items.value.splice(parsedPosition, 0, temp);
173
- }
174
-
175
- if (callback) callback.done();
176
- }
177
-
178
- async function deleteItem(deleteItem: Record<string, any>, callback?: FormDialogCallback) {
179
- const index = items.value.findIndex(item => item[props.modelKey] === deleteItem[props.modelKey])
180
-
181
- if (index !== -1) {
182
- let confirm = await useDialog().confirm({message: "Do you want to delete record?"})
183
- if (confirm) {
184
- items.value.splice(index, 1)
185
- if (callback) callback.done()
186
- }
187
- }
188
- }
189
-
190
- function openDialog(item?: object) {
191
- if (props.inputPadOnly) inputPadRef.value?.setOriginalData(item)
192
- else {
193
- currentItem.value = item
194
- nextTick(() => {
195
- isDialogOpen.value = true
196
- emit('open:dialog',item)
197
- })
198
- }
199
- }
200
- const inputPadRef = ref()
201
-
202
- const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpItem, moveDownItem,moveToItem,setSearch })
203
-
204
- const isValid = computed(()=>{
205
- return inputRef.value?.isValid
206
- })
207
-
208
- const errorMessages = computed(()=>{
209
- return inputRef.value?.errorMessages
210
- })
211
-
212
- defineExpose({
213
- errorMessages,
214
- isValid,
215
- reset: ()=>inputRef.value?.reset(),
216
- resetValidation : ()=>inputRef.value?.resetValidation(),
217
- validate : ()=>inputRef.value?.validate(),
218
- operation,
219
- items
220
- })
221
- </script>
222
-
223
- <template>
224
- <v-input v-model="items" v-bind="plainAttrs" ref="inputRef">
225
- <template #default="{isReadonly,isDisabled}">
226
- <v-container fluid class="ma-0 pa-0">
227
- <v-card>
228
- <slot
229
- name="header"
230
- :items="items"
231
- :operation="operation"
232
- >
233
- <VToolbar :color="toolbarColor">
234
- <v-row
235
- justify="end"
236
- class="ma-1"
237
- dense
238
- no-gutters
239
- align="center"
240
- >
241
- <v-col cols="7">
242
- <VToolbarTitle class="pl-3">
243
- <slot name="title">
244
- {{ title }}
245
- </slot>
246
- </VToolbarTitle>
247
- </v-col>
248
- <v-col cols="5">
249
- <slot name="search" :items="items" :operation="operation" v-if="props.searchable">
250
- <VTextField
251
- v-model="search"
252
- class="justify-end w-100"
253
- density="compact"
254
- hide-details
255
- placeholder="ค้นหา"
256
- clearable
257
- variant="solo"
258
- />
259
- </slot>
260
- </v-col>
261
- </v-row>
262
-
263
- <VToolbarItems>
264
- <slot name="toolbarItems" :items="items" :operation="operation"/>
265
- <ImportCSV
266
- v-if="props.importable && !(isReadonly.value || isDisabled.value)"
267
- icon="mdi:mdi-file-upload"
268
- variant="flat"
269
- @import="importItems"
270
- :color="toolbarColor"
271
- :stringFields="props.stringFields"
272
- />
273
- <ExportCSV
274
- v-if="props.exportable && items.length && !(isReadonly.value || isDisabled.value)"
275
- icon="mdi:mdi-file-download"
276
- variant="flat"
277
- :file-name="title"
278
- :model-value="items"
279
- :color="toolbarColor"
280
- :stringFields="props.stringFields"
281
- />
282
- <VBtn
283
- v-if="props.insertable && !props.inputPadOnly && !(isReadonly.value || isDisabled.value)"
284
- :color="toolbarColor"
285
- prepend-icon="mdi:mdi-plus"
286
- variant="flat"
287
- @click="openDialog()"
288
- >
289
- add
290
- </VBtn>
291
- </VToolbarItems>
292
- </VToolbar>
293
- </slot>
294
- <v-data-table
295
- v-bind="plainAttrs"
296
- color="primary"
297
- :items="items"
298
- :search="search"
299
- >
300
- <!-- @ts-ignore -->
301
- <template
302
- v-for="(_, name, index) in ($slots as {})"
303
- :key="index"
304
- #[name]="slotData"
305
- >
306
- <slot
307
- :name="name"
308
- v-bind="((slotData || {}) as object)"
309
- :operation="operation"
310
- :isReadonly="isReadonly"
311
- :isDisabled="isDisabled"
312
- />
313
- </template>
314
- <template
315
- v-if="!$slots['item.operation'] && !(isReadonly.value || isDisabled.value)"
316
- #item.operation="props"
317
- >
318
- <v-icon density="compact" :disabled="props.index==0" @click="moveUpItem(props.item)">mdi:mdi-arrow-up-thick</v-icon>
319
- <v-icon density="compact" @click="moveToItem(props.item)">fa:fas fa-arrow-right-to-bracket</v-icon>
320
- <v-icon density="compact" :disabled="props.index==items.length-1" @click="moveDownItem(props.item)">mdi:mdi-arrow-down-thick</v-icon>
321
- </template>
322
- <template
323
- v-if="!$slots['item.action'] && !(isReadonly.value || isDisabled.value)"
324
- #item.action="{ item }"
325
- >
326
- <v-btn
327
- variant="flat"
328
- density="compact"
329
- icon="mdi:mdi-note-edit"
330
- @click="openDialog(item)"
331
- />
332
- <v-btn
333
- variant="flat"
334
- density="compact"
335
- icon="mdi:mdi-delete"
336
- @click="deleteItem(item)"
337
- />
338
- </template>
339
- </v-data-table>
340
- <FormDialog
341
- v-model="isDialogOpen"
342
- :title="title"
343
- :fullscreen="dialogFullscreen"
344
- :initial-data="initialData"
345
- :form-data="currentItem"
346
- @create="createItem"
347
- @update="updateItem"
348
- @afterLeave="emit('close:dialog')"
349
- :saveAndStay="saveAndStay"
350
- :width="dialogWidth"
351
- :height="dialogHeight"
352
- :max-width="dialogMaxWidth"
353
- :max-height="dialogMaxHeight"
354
- v-if="!props.inputPadOnly"
355
- >
356
- <template #default="slotData">
357
- <slot
358
- name="form"
359
- v-bind="slotData"
360
- />
361
- </template>
362
- <template #title="slotData">
363
- <slot
364
- name="formTitle"
365
- v-bind="slotData"
366
- />
367
- </template>
368
- <template #action="slotData">
369
- <slot
370
- name="formAction"
371
- v-bind="slotData"
372
- />
373
- </template>
374
- </FormDialog>
375
- </v-card>
376
- <slot name="inputPad" :operation="operation" :isReadonly="isReadonly" :isDisabled="isDisabled">
377
- <v-sheet border rounded class="mt-1" v-if="inputPad && !(isReadonly.value || isDisabled.value)">
378
- <form-action-pad ref="inputPadRef" :title="props.title" @create="createItem" @update="updateItem" :initial-data="initialData">
379
- <template #default="slotData">
380
- <slot
381
- name="form"
382
- v-bind="slotData"
383
- />
384
- </template>
385
- </form-action-pad>
386
- </v-sheet>
387
- </slot>
388
- </v-container>
389
- </template>
390
- </v-input>
391
- </template>
1
+ <script lang="ts" setup>
2
+ /**
3
+ * FormTable is a schema-driven form field component that binds model data, renders field UI, and emits normalized updates.
4
+ * This doc block is consumed by vue-docgen for generated API documentation.
5
+ */
6
+ import {VDataTable} from 'vuetify/components/VDataTable'
7
+ import {VInput} from 'vuetify/components/VInput'
8
+ import {computed, defineOptions,defineExpose, nextTick, ref, useAttrs, watch, useTemplateRef} from 'vue'
9
+ import {omit} from 'lodash-es'
10
+ import {useDialog} from "../../composables/dialog"
11
+ import type {FormDialogCallback} from '../../types/formDialog'
12
+ import { useLocalStorageModel, type PersistSlimProps } from '../../composables/localStorageModel'
13
+
14
+ defineOptions({
15
+ inheritAttrs: false,
16
+ })
17
+
18
+ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataTable['$props']> {
19
+ title: string // Toolbar title and default export file name shown for this table section.
20
+ noDataText?: string // Text rendered by the data table when `items` is empty after filtering/loading.
21
+ modelValue?: Record<string, any>[] // Source rows from parent; component normalizes, mutates order, then emits updated rows.
22
+ modelKey?: string // Unique row key used for update/delete/reorder; missing keys are auto-generated incrementally.
23
+ dialogWidth?: string | number // Passed to row edit dialog width to control normal (non-fullscreen) modal size.
24
+ dialogMaxWidth?: string | number // Upper width bound for the edit dialog on large screens.
25
+ dialogHeight?: string | number // Passed to row edit dialog height to control vertical editing space.
26
+ dialogMaxHeight?: string | number // Upper height bound for the edit dialog before internal scrolling.
27
+ dialogFullscreen?: boolean // Default open mode for row editor dialog (`true` opens full screen).
28
+ initialData?: Record<string, any> // Seed object merged into new rows before user input.
29
+ toolbarColor?: string // Vuetify color applied to toolbar and default action buttons.
30
+ importable?: boolean // Enables Import button and `@import` flow for adding multiple rows from file.
31
+ exportable?: boolean // Enables Export button for current `items` dataset.
32
+ insertable?: boolean // Enables Add button and creation flow for new rows.
33
+ searchable?: boolean // Shows toolbar search input and binds keyword to table filtering.
34
+ inputPad?: boolean // Enables embedded pad editing workflow instead of relying only on dialog.
35
+ inputPadOnly?: boolean // Uses pad editor exclusively and skips dialog open/close behavior.
36
+ saveAndStay?: boolean // Forwards to child editor to keep it open after save (batch editing).
37
+ stringFields?: Array<string> // Dot-path fields preserved as string during import/export flattening and parsing.
38
+ }
39
+
40
+ /**
41
+ * Public props accepted by FormTable.
42
+ * Document each prop field with intent, defaults, and side effects for clear generated docs.
43
+ */
44
+ const props = withDefaults(defineProps<Props & PersistSlimProps>(), {
45
+ noDataText: 'ไม่พบข้อมูล',
46
+ dialogFullscreen: false,
47
+ modelKey: 'id',
48
+ toolbarColor: 'primary',
49
+ importable: true,
50
+ exportable: true,
51
+ insertable: true,
52
+ searchable: true,
53
+ inputPad: false,
54
+ inputPadOnly: false,
55
+ saveAndStay: false,
56
+ stringFields: ()=>[],
57
+ })
58
+
59
+ /**
60
+ * Custom events emitted by FormTable.
61
+ * Parents can listen to these events to react to user actions and internal state changes.
62
+ */
63
+ const emit = defineEmits(['update:modelValue','open:dialog','close:dialog'])
64
+ const attrs = useAttrs()
65
+ const plainAttrs = computed(() => {
66
+ return omit(attrs, ['modelValue', 'onUpdate:modelValue'])
67
+ })
68
+
69
+ const inputRef = useTemplateRef<VInput>("inputRef")
70
+
71
+ const items = ref<Record<string, any>[]>([])
72
+ const search = ref<string>()
73
+ const currentItem = ref<Record<string, any> | undefined>(undefined)
74
+
75
+ useLocalStorageModel(items,props)
76
+
77
+ function setSearch(keyword: string) {
78
+ search.value = keyword
79
+ }
80
+
81
+ const isDialogOpen = ref<boolean>(false)
82
+
83
+ watch(() => props.modelValue, (newValue) => {
84
+ if (!Array.isArray(newValue) || !newValue.every(item => typeof item === 'object')) {
85
+ items.value = []
86
+ }
87
+ else {
88
+ let maxKey = 0
89
+
90
+ newValue.forEach((item) => {
91
+ if (!item.hasOwnProperty(props.modelKey)) {
92
+ maxKey = Math.max(maxKey, ...newValue.map(i => i[props.modelKey] || 0))
93
+ item[props.modelKey] = maxKey + 1
94
+ }
95
+ })
96
+
97
+ items.value = newValue
98
+ }
99
+ }, { immediate: true })
100
+
101
+ watch(items, (newValue) => {
102
+ emit('update:modelValue', newValue)
103
+ }, { deep: true })
104
+
105
+
106
+ function createItem(item: Record<string, any>, callback?: FormDialogCallback) {
107
+ if (items.value.length > 0) item[props.modelKey] = Math.max(...items.value.map(i => i[props.modelKey] || 0)) + 1
108
+ else item[props.modelKey] = 1
109
+
110
+ items.value.push(item)
111
+
112
+ if (callback && callback.setData) callback.setData(item)
113
+ if (callback) callback.done()
114
+ }
115
+
116
+ function importItems(importItems: Record<string, any>[], callback?: FormDialogCallback) {
117
+ importItems.forEach((item) => {
118
+ createItem(item)
119
+ })
120
+ if (callback) callback.done()
121
+ }
122
+
123
+ function updateItem(newItem: Record<string, any>, callback?: FormDialogCallback) {
124
+ const index = items.value.findIndex(item => item[props.modelKey] === newItem[props.modelKey])
125
+
126
+ if (index !== -1) {
127
+ items.value[index] = newItem
128
+ }
129
+
130
+ if (callback && callback.setData) callback.setData(newItem)
131
+ if (callback) callback.done()
132
+ }
133
+
134
+ function moveUpItem(currentItem: Record<string, any>, callback?: FormDialogCallback) {
135
+ const index = items.value.findIndex(item => item[props.modelKey] === currentItem[props.modelKey])
136
+
137
+ if (index > 0) {
138
+ const temp = items.value[index - 1]
139
+ items.value[index - 1] = items.value[index]
140
+ items.value[index] = temp
141
+ }
142
+
143
+ if (callback) callback.done()
144
+ }
145
+
146
+ function moveDownItem(currentItem: Record<string, any>, callback?: FormDialogCallback) {
147
+ const index = items.value.findIndex(item => item[props.modelKey] === currentItem[props.modelKey])
148
+
149
+ if (index >= 0 && index < items.value.length - 1) {
150
+ const temp = items.value[index + 1]
151
+ items.value[index + 1] = items.value[index]
152
+ items.value[index] = temp
153
+ }
154
+
155
+ if (callback) callback.done()
156
+ }
157
+
158
+ function moveToItem(currentItem: Record<string, any>, callback?: FormDialogCallback) {
159
+ const index = items.value.findIndex(item => item[props.modelKey] === currentItem[props.modelKey]);
160
+
161
+ if (index !== -1) {
162
+ const newPosition = prompt("Enter the new position (0-based index):");
163
+ const parsedPosition = parseInt(<string>newPosition, 10);
164
+
165
+ if (isNaN(parsedPosition) || parsedPosition < 0 || parsedPosition >= items.value.length) {
166
+ alert("Invalid position entered. Please enter a number between 0 and " + (items.value.length - 1));
167
+ return
168
+ }
169
+
170
+ const [temp] = items.value.splice(index, 1);
171
+
172
+ items.value.splice(parsedPosition, 0, temp);
173
+ }
174
+
175
+ if (callback) callback.done();
176
+ }
177
+
178
+ async function deleteItem(deleteItem: Record<string, any>, callback?: FormDialogCallback) {
179
+ const index = items.value.findIndex(item => item[props.modelKey] === deleteItem[props.modelKey])
180
+
181
+ if (index !== -1) {
182
+ let confirm = await useDialog().confirm({message: "Do you want to delete record?"})
183
+ if (confirm) {
184
+ items.value.splice(index, 1)
185
+ if (callback) callback.done()
186
+ }
187
+ }
188
+ }
189
+
190
+ function openDialog(item?: object) {
191
+ if (props.inputPadOnly) inputPadRef.value?.setOriginalData(item)
192
+ else {
193
+ currentItem.value = item
194
+ nextTick(() => {
195
+ isDialogOpen.value = true
196
+ emit('open:dialog',item)
197
+ })
198
+ }
199
+ }
200
+ const inputPadRef = ref()
201
+
202
+ const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpItem, moveDownItem,moveToItem,setSearch })
203
+
204
+ const isValid = computed(()=>{
205
+ return inputRef.value?.isValid
206
+ })
207
+
208
+ const errorMessages = computed(()=>{
209
+ return inputRef.value?.errorMessages
210
+ })
211
+
212
+ defineExpose({
213
+ errorMessages,
214
+ isValid,
215
+ reset: ()=>inputRef.value?.reset(),
216
+ resetValidation : ()=>inputRef.value?.resetValidation(),
217
+ validate : ()=>inputRef.value?.validate(),
218
+ operation,
219
+ items
220
+ })
221
+ </script>
222
+
223
+ <template>
224
+ <v-input v-model="items" v-bind="plainAttrs" ref="inputRef">
225
+ <template #default="{isReadonly,isDisabled}">
226
+ <v-container fluid class="ma-0 pa-0">
227
+ <v-card>
228
+ <slot
229
+ name="header"
230
+ :items="items"
231
+ :operation="operation"
232
+ >
233
+ <VToolbar :color="toolbarColor">
234
+ <v-row
235
+ justify="end"
236
+ class="ma-1"
237
+ dense
238
+ no-gutters
239
+ align="center"
240
+ >
241
+ <v-col cols="7">
242
+ <VToolbarTitle class="pl-3">
243
+ <slot name="title">
244
+ {{ title }}
245
+ </slot>
246
+ </VToolbarTitle>
247
+ </v-col>
248
+ <v-col cols="5">
249
+ <slot name="search" :items="items" :operation="operation" v-if="props.searchable">
250
+ <VTextField
251
+ v-model="search"
252
+ class="justify-end w-100"
253
+ density="compact"
254
+ hide-details
255
+ placeholder="ค้นหา"
256
+ clearable
257
+ variant="solo"
258
+ />
259
+ </slot>
260
+ </v-col>
261
+ </v-row>
262
+
263
+ <VToolbarItems>
264
+ <slot name="toolbarItems" :items="items" :operation="operation"/>
265
+ <ImportCSV
266
+ v-if="props.importable && !(isReadonly.value || isDisabled.value)"
267
+ icon="mdi:mdi-file-upload"
268
+ variant="flat"
269
+ @import="importItems"
270
+ :color="toolbarColor"
271
+ :stringFields="props.stringFields"
272
+ />
273
+ <ExportCSV
274
+ v-if="props.exportable && items.length && !(isReadonly.value || isDisabled.value)"
275
+ icon="mdi:mdi-file-download"
276
+ variant="flat"
277
+ :file-name="title"
278
+ :model-value="items"
279
+ :color="toolbarColor"
280
+ :stringFields="props.stringFields"
281
+ />
282
+ <VBtn
283
+ v-if="props.insertable && !props.inputPadOnly && !(isReadonly.value || isDisabled.value)"
284
+ :color="toolbarColor"
285
+ prepend-icon="mdi:mdi-plus"
286
+ variant="flat"
287
+ @click="openDialog()"
288
+ >
289
+ add
290
+ </VBtn>
291
+ </VToolbarItems>
292
+ </VToolbar>
293
+ </slot>
294
+ <v-data-table
295
+ v-bind="plainAttrs"
296
+ color="primary"
297
+ :items="items"
298
+ :search="search"
299
+ >
300
+ <!-- @ts-ignore -->
301
+ <template
302
+ v-for="(_, name, index) in ($slots as {})"
303
+ :key="index"
304
+ #[name]="slotData"
305
+ >
306
+ <slot
307
+ :name="name"
308
+ v-bind="((slotData || {}) as object)"
309
+ :operation="operation"
310
+ :isReadonly="isReadonly"
311
+ :isDisabled="isDisabled"
312
+ />
313
+ </template>
314
+ <template
315
+ v-if="!$slots['item.operation'] && !(isReadonly.value || isDisabled.value)"
316
+ #item.operation="props"
317
+ >
318
+ <v-icon density="compact" :disabled="props.index==0" @click="moveUpItem(props.item)">mdi:mdi-arrow-up-thick</v-icon>
319
+ <v-icon density="compact" @click="moveToItem(props.item)">fa:fas fa-arrow-right-to-bracket</v-icon>
320
+ <v-icon density="compact" :disabled="props.index==items.length-1" @click="moveDownItem(props.item)">mdi:mdi-arrow-down-thick</v-icon>
321
+ </template>
322
+ <template
323
+ v-if="!$slots['item.action'] && !(isReadonly.value || isDisabled.value)"
324
+ #item.action="{ item }"
325
+ >
326
+ <v-btn
327
+ variant="flat"
328
+ density="compact"
329
+ icon="mdi:mdi-note-edit"
330
+ @click="openDialog(item)"
331
+ />
332
+ <v-btn
333
+ variant="flat"
334
+ density="compact"
335
+ icon="mdi:mdi-delete"
336
+ @click="deleteItem(item)"
337
+ />
338
+ </template>
339
+ </v-data-table>
340
+ <FormDialog
341
+ v-model="isDialogOpen"
342
+ :title="title"
343
+ :fullscreen="dialogFullscreen"
344
+ :initial-data="initialData"
345
+ :form-data="currentItem"
346
+ @create="createItem"
347
+ @update="updateItem"
348
+ @afterLeave="emit('close:dialog')"
349
+ :saveAndStay="saveAndStay"
350
+ :width="dialogWidth"
351
+ :height="dialogHeight"
352
+ :max-width="dialogMaxWidth"
353
+ :max-height="dialogMaxHeight"
354
+ v-if="!props.inputPadOnly"
355
+ >
356
+ <template #default="slotData">
357
+ <slot
358
+ name="form"
359
+ v-bind="slotData"
360
+ />
361
+ </template>
362
+ <template #title="slotData">
363
+ <slot
364
+ name="formTitle"
365
+ v-bind="slotData"
366
+ />
367
+ </template>
368
+ <template #action="slotData">
369
+ <slot
370
+ name="formAction"
371
+ v-bind="slotData"
372
+ />
373
+ </template>
374
+ </FormDialog>
375
+ </v-card>
376
+ <slot name="inputPad" :operation="operation" :isReadonly="isReadonly" :isDisabled="isDisabled">
377
+ <v-sheet border rounded class="mt-1" v-if="inputPad && !(isReadonly.value || isDisabled.value)">
378
+ <form-action-pad ref="inputPadRef" :title="props.title" @create="createItem" @update="updateItem" :initial-data="initialData">
379
+ <template #default="slotData">
380
+ <slot
381
+ name="form"
382
+ v-bind="slotData"
383
+ />
384
+ </template>
385
+ </form-action-pad>
386
+ </v-sheet>
387
+ </slot>
388
+ </v-container>
389
+ </template>
390
+ </v-input>
391
+ </template>