@ramathibodi/nuxt-commons 0.1.73 → 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 (111) hide show
  1. package/README.md +115 -96
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +1 -0
  4. package/dist/runtime/components/Alert.vue +58 -54
  5. package/dist/runtime/components/BarcodeReader.vue +130 -122
  6. package/dist/runtime/components/ExportCSV.vue +110 -102
  7. package/dist/runtime/components/FileBtn.vue +79 -67
  8. package/dist/runtime/components/ImportCSV.vue +151 -139
  9. package/dist/runtime/components/MrzReader.vue +168 -0
  10. package/dist/runtime/components/SplitterPanel.vue +67 -59
  11. package/dist/runtime/components/TabsGroup.vue +39 -31
  12. package/dist/runtime/components/TextBarcode.vue +66 -54
  13. package/dist/runtime/components/device/IdCardButton.vue +95 -83
  14. package/dist/runtime/components/device/IdCardWebSocket.vue +207 -195
  15. package/dist/runtime/components/device/Scanner.vue +350 -338
  16. package/dist/runtime/components/dialog/Confirm.vue +112 -100
  17. package/dist/runtime/components/dialog/Host.vue +88 -84
  18. package/dist/runtime/components/dialog/Index.vue +84 -72
  19. package/dist/runtime/components/dialog/Loading.vue +51 -39
  20. package/dist/runtime/components/dialog/default/Confirm.vue +112 -100
  21. package/dist/runtime/components/dialog/default/Loading.vue +60 -48
  22. package/dist/runtime/components/dialog/default/Notify.vue +82 -70
  23. package/dist/runtime/components/dialog/default/Printing.vue +46 -34
  24. package/dist/runtime/components/dialog/default/VerifyUser.vue +144 -132
  25. package/dist/runtime/components/document/Form.vue +50 -42
  26. package/dist/runtime/components/document/TemplateBuilder.vue +536 -524
  27. package/dist/runtime/components/form/ActionPad.vue +156 -144
  28. package/dist/runtime/components/form/Birthdate.vue +116 -104
  29. package/dist/runtime/components/form/CheckboxGroup.vue +99 -87
  30. package/dist/runtime/components/form/CodeEditor.vue +45 -37
  31. package/dist/runtime/components/form/Date.vue +270 -258
  32. package/dist/runtime/components/form/DateTime.vue +220 -208
  33. package/dist/runtime/components/form/Dialog.vue +178 -166
  34. package/dist/runtime/components/form/EditPad.vue +157 -145
  35. package/dist/runtime/components/form/File.vue +295 -283
  36. package/dist/runtime/components/form/Hidden.vue +44 -32
  37. package/dist/runtime/components/form/Iterator.vue +538 -526
  38. package/dist/runtime/components/form/Login.vue +143 -131
  39. package/dist/runtime/components/form/Pad.vue +399 -387
  40. package/dist/runtime/components/form/SignPad.vue +226 -218
  41. package/dist/runtime/components/form/System.vue +34 -26
  42. package/dist/runtime/components/form/Table.vue +391 -379
  43. package/dist/runtime/components/form/TableData.vue +236 -224
  44. package/dist/runtime/components/form/Time.vue +177 -165
  45. package/dist/runtime/components/form/images/Capture.vue +245 -237
  46. package/dist/runtime/components/form/images/Edit.vue +133 -121
  47. package/dist/runtime/components/form/images/Field.vue +331 -320
  48. package/dist/runtime/components/form/images/Pad.vue +54 -42
  49. package/dist/runtime/components/label/Date.vue +37 -29
  50. package/dist/runtime/components/label/DateAgo.vue +102 -94
  51. package/dist/runtime/components/label/DateCount.vue +152 -144
  52. package/dist/runtime/components/label/Field.vue +111 -103
  53. package/dist/runtime/components/label/FormatMoney.vue +37 -29
  54. package/dist/runtime/components/label/Mask.vue +46 -38
  55. package/dist/runtime/components/label/Object.vue +21 -13
  56. package/dist/runtime/components/master/Autocomplete.vue +89 -81
  57. package/dist/runtime/components/master/Combobox.vue +88 -80
  58. package/dist/runtime/components/master/RadioGroup.vue +90 -78
  59. package/dist/runtime/components/master/Select.vue +70 -62
  60. package/dist/runtime/components/master/label.vue +55 -47
  61. package/dist/runtime/components/model/Autocomplete.vue +91 -79
  62. package/dist/runtime/components/model/Combobox.vue +90 -78
  63. package/dist/runtime/components/model/Pad.vue +114 -102
  64. package/dist/runtime/components/model/Select.vue +78 -72
  65. package/dist/runtime/components/model/Table.vue +370 -358
  66. package/dist/runtime/components/model/iterator.vue +497 -489
  67. package/dist/runtime/components/model/label.vue +58 -50
  68. package/dist/runtime/components/pdf/Print.vue +75 -63
  69. package/dist/runtime/components/pdf/View.vue +146 -134
  70. package/dist/runtime/composables/alert.d.ts +4 -0
  71. package/dist/runtime/composables/api.d.ts +4 -0
  72. package/dist/runtime/composables/dialog.d.ts +1 -1
  73. package/dist/runtime/composables/document/templateFormHidden.d.ts +4 -0
  74. package/dist/runtime/composables/graphql.d.ts +1 -1
  75. package/dist/runtime/composables/graphqlModel.d.ts +9 -9
  76. package/dist/runtime/composables/graphqlModelItem.d.ts +7 -7
  77. package/dist/runtime/composables/graphqlModelOperation.d.ts +6 -6
  78. package/dist/runtime/composables/localStorageModel.d.ts +4 -0
  79. package/dist/runtime/composables/lookupList.d.ts +4 -0
  80. package/dist/runtime/composables/menu.d.ts +4 -0
  81. package/dist/runtime/composables/useMrzReader.d.ts +48 -0
  82. package/dist/runtime/composables/useMrzReader.js +423 -0
  83. package/dist/runtime/composables/useTesseract.d.ts +16 -0
  84. package/dist/runtime/composables/useTesseract.js +45 -0
  85. package/dist/runtime/composables/userPermission.d.ts +1 -1
  86. package/dist/runtime/labs/Calendar.vue +99 -99
  87. package/dist/runtime/labs/form/EditMobile.vue +152 -152
  88. package/dist/runtime/labs/form/TextFieldMask.vue +43 -43
  89. package/dist/runtime/plugins/clientConfig.d.ts +1 -1
  90. package/dist/runtime/plugins/default.d.ts +1 -1
  91. package/dist/runtime/plugins/dialogManager.d.ts +1 -1
  92. package/dist/runtime/plugins/permission.d.ts +1 -1
  93. package/dist/runtime/types/alert.d.ts +11 -11
  94. package/dist/runtime/types/clientConfig.d.ts +13 -13
  95. package/dist/runtime/types/dialogManager.d.ts +35 -35
  96. package/dist/runtime/types/formDialog.d.ts +5 -5
  97. package/dist/runtime/types/graphqlOperation.d.ts +23 -23
  98. package/dist/runtime/types/menu.d.ts +31 -31
  99. package/dist/runtime/types/modules.d.ts +7 -7
  100. package/dist/runtime/types/permission.d.ts +13 -13
  101. package/dist/runtime/utils/asset.d.ts +2 -0
  102. package/dist/runtime/utils/asset.js +49 -0
  103. package/package.json +131 -122
  104. package/scripts/enrich-vue-docs-from-ai.mjs +197 -0
  105. package/scripts/generate-ai-summary.mjs +321 -0
  106. package/scripts/generate-composables-md.mjs +129 -0
  107. package/scripts/postInstall.cjs +70 -70
  108. package/templates/.codegen/codegen.ts +32 -32
  109. package/templates/.codegen/plugin-schema-object.js +161 -161
  110. package/templates/public/tesseract/mrz.traineddata.gz +0 -0
  111. package/templates/public/tesseract/ocrb.traineddata.gz +0 -0
@@ -1,102 +1,110 @@
1
- <script lang="ts" setup>
2
- import { ref } from 'vue'
3
- import * as XLSX from 'xlsx'
4
- import { VBtn } from 'vuetify/components/VBtn'
5
- import { useAlert } from '../composables/alert'
6
-
7
- interface ExportButtonProps extends /* @vue-ignore */ InstanceType<typeof VBtn['$props']> {
8
- fileName?: string
9
- sheetName?: string
10
- modelValue?: object[]
11
- stringFields?: Array<string>
12
- tooltip?: string | Record<string,any> | undefined
13
- }
14
-
15
- const props = withDefaults(defineProps<ExportButtonProps>(), {
16
- fileName: 'download',
17
- sheetName: 'Sheet1',
18
- stringFields: ()=>[],
19
- tooltip: ()=>({text: 'Export', location: 'bottom'}),
20
- })
21
-
22
- const alert = useAlert()
23
- const loading = ref(false)
24
-
25
- /**
26
- * Triggers file export
27
- */
28
- function exportFile() {
29
- if (props.modelValue && Array.isArray(props.modelValue) && props.modelValue.length > 0) {
30
- loading.value = true
31
-
32
- try {
33
- const workbook = XLSX.utils.book_new()
34
- const worksheet = XLSX.utils.json_to_sheet(flattenNestedFields(props.modelValue))
35
- const fileName = `${props.fileName}.xlsx`
36
-
37
- XLSX.utils.book_append_sheet(workbook, worksheet, props.sheetName)
38
- XLSX.writeFile(workbook, fileName)
39
- } catch (error: any) {
40
- alert?.addAlert({ message: `Export failed: ${error.message}`, alertType: 'error' })
41
- } finally {
42
- loading.value = false
43
- }
44
- } else {
45
- alert?.addAlert({ message: 'Invalid or no data to export', alertType: 'error' })
46
- }
47
- }
48
-
49
- /**
50
- * Recursively flattens nested fields for export
51
- * @param items - Array of objects to flatten
52
- */
53
- function flattenNestedFields(items: any[]) {
54
- return items.map((item: any) => {
55
- return flattenObject(item)
56
- })
57
- }
58
-
59
- /**
60
- * Recursively flattens an object, converting nested keys into dot-separated keys
61
- * @param obj - Object to flatten
62
- * @param parentKey - Parent key (for recursion)
63
- * @param separator - Separator for nested keys
64
- */
65
- function flattenObject(obj: any, parentKey = '', separator = '.') {
66
- return Object.keys(obj).reduce((acc: any, key: string) => {
67
- const newKey = parentKey ? `${parentKey}${separator}${key}` : key
68
- const value = obj[key]
69
-
70
- if (value && typeof value === 'object' && !Array.isArray(value) && !props.stringFields.includes(newKey)) {
71
- Object.assign(acc, flattenObject(value, newKey, separator))
72
- } else {
73
- acc[newKey] = typeof value === 'object' ? JSON.stringify(value) : value
74
- }
75
-
76
- return acc
77
- }, {})
78
- }
79
- </script>
80
-
81
- <template>
82
- <VBtn
83
- v-bind="$attrs"
84
- color="primary"
85
- :loading="loading"
86
- :disabled="loading"
87
- text="Export CSV"
88
- @click="exportFile"
89
- v-tooltip="props.tooltip"
90
- >
91
- <template
92
- v-for="(_, name, index) in ($slots as {})"
93
- :key="index"
94
- #[name]="slotData"
95
- >
96
- <slot
97
- :name="name"
98
- v-bind="((slotData || {}) as object)"
99
- />
100
- </template>
101
- </VBtn>
102
- </template>
1
+ <script lang="ts" setup>
2
+ /**
3
+ * ExportCSV exports array data to spreadsheet files, flattening nested fields before file generation.
4
+ * This doc block is consumed by vue-docgen for generated API documentation.
5
+ */
6
+ import { ref } from 'vue'
7
+ import * as XLSX from 'xlsx'
8
+ import { VBtn } from 'vuetify/components/VBtn'
9
+ import { useAlert } from '../composables/alert'
10
+
11
+ interface ExportButtonProps extends /* @vue-ignore */ InstanceType<typeof VBtn['$props']> {
12
+ fileName?: string // File name used when downloading or printing generated files.
13
+ sheetName?: string // Configuration option used by ExportCSV.
14
+ modelValue?: object[] // Bound value for v-model synchronization with the parent component.
15
+ stringFields?: Array<string> // Field paths that must stay as string values (no nested object conversion).
16
+ tooltip?: string | Record<string,any> | undefined // Tooltip text or config object shown for the action control.
17
+ }
18
+
19
+ /**
20
+ * Public props accepted by ExportCSV.
21
+ * Document each prop field with intent, defaults, and side effects for clear generated docs.
22
+ */
23
+ const props = withDefaults(defineProps<ExportButtonProps>(), {
24
+ fileName: 'download',
25
+ sheetName: 'Sheet1',
26
+ stringFields: ()=>[],
27
+ tooltip: ()=>({text: 'Export', location: 'bottom'}),
28
+ })
29
+
30
+ const alert = useAlert()
31
+ const loading = ref(false)
32
+
33
+ /**
34
+ * Triggers file export
35
+ */
36
+ function exportFile() {
37
+ if (props.modelValue && Array.isArray(props.modelValue) && props.modelValue.length > 0) {
38
+ loading.value = true
39
+
40
+ try {
41
+ const workbook = XLSX.utils.book_new()
42
+ const worksheet = XLSX.utils.json_to_sheet(flattenNestedFields(props.modelValue))
43
+ const fileName = `${props.fileName}.xlsx`
44
+
45
+ XLSX.utils.book_append_sheet(workbook, worksheet, props.sheetName)
46
+ XLSX.writeFile(workbook, fileName)
47
+ } catch (error: any) {
48
+ alert?.addAlert({ message: `Export failed: ${error.message}`, alertType: 'error' })
49
+ } finally {
50
+ loading.value = false
51
+ }
52
+ } else {
53
+ alert?.addAlert({ message: 'Invalid or no data to export', alertType: 'error' })
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Recursively flattens nested fields for export
59
+ * @param items - Array of objects to flatten
60
+ */
61
+ function flattenNestedFields(items: any[]) {
62
+ return items.map((item: any) => {
63
+ return flattenObject(item)
64
+ })
65
+ }
66
+
67
+ /**
68
+ * Recursively flattens an object, converting nested keys into dot-separated keys
69
+ * @param obj - Object to flatten
70
+ * @param parentKey - Parent key (for recursion)
71
+ * @param separator - Separator for nested keys
72
+ */
73
+ function flattenObject(obj: any, parentKey = '', separator = '.') {
74
+ return Object.keys(obj).reduce((acc: any, key: string) => {
75
+ const newKey = parentKey ? `${parentKey}${separator}${key}` : key
76
+ const value = obj[key]
77
+
78
+ if (value && typeof value === 'object' && !Array.isArray(value) && !props.stringFields.includes(newKey)) {
79
+ Object.assign(acc, flattenObject(value, newKey, separator))
80
+ } else {
81
+ acc[newKey] = typeof value === 'object' ? JSON.stringify(value) : value
82
+ }
83
+
84
+ return acc
85
+ }, {})
86
+ }
87
+ </script>
88
+
89
+ <template>
90
+ <VBtn
91
+ v-bind="$attrs"
92
+ color="primary"
93
+ :loading="loading"
94
+ :disabled="loading"
95
+ text="Export CSV"
96
+ @click="exportFile"
97
+ v-tooltip="props.tooltip"
98
+ >
99
+ <template
100
+ v-for="(_, name, index) in ($slots as {})"
101
+ :key="index"
102
+ #[name]="slotData"
103
+ >
104
+ <slot
105
+ :name="name"
106
+ v-bind="((slotData || {}) as object)"
107
+ />
108
+ </template>
109
+ </VBtn>
110
+ </template>
@@ -1,67 +1,79 @@
1
- <script lang="ts" setup>
2
- import {ref} from 'vue'
3
- import {VBtn} from 'vuetify/components/VBtn'
4
-
5
- interface Props extends /* @vue-ignore */ InstanceType<typeof VBtn['$props']> {
6
- accept?: string
7
- multiple?: boolean
8
- iconOnly?: boolean
9
- modelValue?: File | File[] | undefined
10
- tooltip?: string | Record<string,any> | undefined
11
- }
12
-
13
- const props = withDefaults(defineProps<Props>(), {
14
- multiple: false,
15
- accept: '*',
16
- tooltip: 'Upload File',
17
- })
18
-
19
- const emit = defineEmits<{
20
- (event: 'update:modelValue', value: File | File[] | undefined): void
21
- }>()
22
-
23
- const fileInput = ref<HTMLInputElement>()
24
- const files = ref<File | File[]>()
25
-
26
- const openFileInput = () => {
27
- fileInput.value?.click()
28
- }
29
-
30
- const reset = () => {
31
- files.value = undefined
32
- }
33
-
34
- const emitFiles = () => {
35
- emit('update:modelValue', files.value)
36
- files.value = []
37
- }
38
-
39
- defineExpose({ reset })
40
- </script>
41
-
42
- <template>
43
- <v-btn
44
- v-bind="$attrs"
45
- @click="openFileInput"
46
- v-tooltip="tooltip"
47
- >
48
- <template
49
- v-for="(_, name, index) in ($slots as {})"
50
- :key="index"
51
- #[name]="slotData"
52
- >
53
- <slot
54
- :name="name"
55
- v-bind="((slotData || {}) as object)"
56
- />
57
- </template>
58
- </v-btn>
59
- <v-file-input
60
- ref="fileInput"
61
- v-model="files"
62
- @update:modelValue="emitFiles"
63
- :accept="props.accept"
64
- :multiple="props.multiple"
65
- style="display: none"
66
- />
67
- </template>
1
+ <script lang="ts" setup>
2
+ /**
3
+ * FileBtn wraps file selection behavior into a reusable button interface that works with form and upload flows.
4
+ * This doc block is consumed by vue-docgen for generated API documentation.
5
+ */
6
+ import {ref} from 'vue'
7
+ import {VBtn} from 'vuetify/components/VBtn'
8
+
9
+ interface Props extends /* @vue-ignore */ InstanceType<typeof VBtn['$props']> {
10
+ accept?: string // Accepted file MIME types or extensions for file selection.
11
+ multiple?: boolean // Allows selecting or uploading more than one file.
12
+ iconOnly?: boolean // icon name/class used in UI rendering
13
+ modelValue?: File | File[] | undefined // Bound value for v-model synchronization with the parent component.
14
+ tooltip?: string | Record<string,any> | undefined // Tooltip text or config object shown for the action control.
15
+ }
16
+
17
+ /**
18
+ * Public props accepted by FileBtn.
19
+ * Document each prop field with intent, defaults, and side effects for clear generated docs.
20
+ */
21
+ const props = withDefaults(defineProps<Props>(), {
22
+ multiple: false,
23
+ accept: '*',
24
+ tooltip: 'Upload File',
25
+ })
26
+
27
+ /**
28
+ * Custom events emitted by FileBtn.
29
+ * Parents can listen to these events to react to user actions and internal state changes.
30
+ */
31
+ const emit = defineEmits<{
32
+ (event: 'update:modelValue', value: File | File[] | undefined): void
33
+ }>()
34
+
35
+ const fileInput = ref<HTMLInputElement>()
36
+ const files = ref<File | File[]>()
37
+
38
+ const openFileInput = () => {
39
+ fileInput.value?.click()
40
+ }
41
+
42
+ const reset = () => {
43
+ files.value = undefined
44
+ }
45
+
46
+ const emitFiles = () => {
47
+ emit('update:modelValue', files.value)
48
+ files.value = []
49
+ }
50
+
51
+ defineExpose({ reset })
52
+ </script>
53
+
54
+ <template>
55
+ <v-btn
56
+ v-bind="$attrs"
57
+ @click="openFileInput"
58
+ v-tooltip="tooltip"
59
+ >
60
+ <template
61
+ v-for="(_, name, index) in ($slots as {})"
62
+ :key="index"
63
+ #[name]="slotData"
64
+ >
65
+ <slot
66
+ :name="name"
67
+ v-bind="((slotData || {}) as object)"
68
+ />
69
+ </template>
70
+ </v-btn>
71
+ <v-file-input
72
+ ref="fileInput"
73
+ v-model="files"
74
+ @update:modelValue="emitFiles"
75
+ :accept="props.accept"
76
+ :multiple="props.multiple"
77
+ style="display: none"
78
+ />
79
+ </template>
@@ -1,139 +1,151 @@
1
- <script lang="ts" setup>
2
- import * as XLSX from 'xlsx'
3
- import { ref } from 'vue'
4
- import { useAlert } from '../composables/alert'
5
- import { VBtn } from 'vuetify/components/VBtn'
6
-
7
- interface ImportButtonProps extends /* @vue-ignore */ InstanceType<typeof VBtn['$props']> {
8
- stringFields?: Array<string>
9
- tooltip?: string | Record<string,any> | undefined
10
- }
11
-
12
- const props = withDefaults(defineProps<ImportButtonProps>(), {
13
- stringFields: ()=>[],
14
- tooltip: ()=>({text: 'Import', location: 'bottom'}),
15
- })
16
-
17
- const alert = useAlert()
18
- const emit = defineEmits<{
19
- (e: 'import', value: object[]): void
20
- }>()
21
-
22
- const loading = ref(false)
23
- const fileBtnRef = ref()
24
-
25
- function uploadedFile(files: File[] | File | undefined) {
26
- if (!files) return
27
-
28
- if (Array.isArray(files) && files.length != 1) {
29
- alert?.addAlert({ message: 'Please select a single file for import', alertType: 'error' })
30
- return
31
- }
32
-
33
- if (Array.isArray(files)) files = files[0]
34
-
35
- const fileExtension = files.name.slice(files.name.lastIndexOf('.')).toLowerCase()
36
- if (!['.xlsx', '.csv'].includes(fileExtension)) {
37
- alert?.addAlert({ message: `Please upload a file with .csv or .xlsx extension only (${files.name})`, alertType: 'error' })
38
- return
39
- }
40
-
41
- const reader = new FileReader()
42
- reader.onload = (e: ProgressEvent<FileReader>) => {
43
- const workbook = XLSX.read(e.target?.result)
44
- const parsedData = parseAndAggregateColumns(
45
- XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]])
46
- )
47
- emit('import', parsedData)
48
- loading.value = false
49
- fileBtnRef.value.reset()
50
- }
51
- loading.value = true
52
- reader.readAsArrayBuffer(files)
53
- }
54
-
55
- /**
56
- * Recursively aggregates nested columns and properties.
57
- * @param items - Array of data from the Excel sheet
58
- */
59
- const parseAndAggregateColumns = (items: any[]) => {
60
- return items.map((item: any) => {
61
- const aggregatedItem: any = {}
62
-
63
- for (const key in item) {
64
- if (key.includes('.')) {
65
- // Extract root key and subKey
66
- const [rootKey, ...subKeys] = key.split('.')
67
-
68
- // Recursively aggregate subKeys
69
- aggregatedItem[rootKey] = aggregatedItem[rootKey] || {}
70
- assignNestedValue(aggregatedItem[rootKey], subKeys, parseIfJson(item[key]))
71
- } else {
72
- // Directly assign root-level properties
73
- if (props.stringFields.includes(key)) aggregatedItem[key] = item[key]
74
- else aggregatedItem[key] = parseIfJson(item[key])
75
- }
76
- }
77
-
78
- return aggregatedItem
79
- })
80
- }
81
-
82
- /**
83
- * Recursively assigns a value to a nested key structure.
84
- * @param obj - The object to assign to
85
- * @param keys - Array of keys leading to the final property
86
- * @param value - The value to assign
87
- */
88
- const assignNestedValue = (obj: any, keys: string[], value: any) => {
89
- const [currentKey, ...remainingKeys] = keys
90
- if (remainingKeys.length === 0) {
91
- obj[currentKey] = value
92
- } else {
93
- obj[currentKey] = obj[currentKey] || {}
94
- assignNestedValue(obj[currentKey], remainingKeys, value)
95
- }
96
- }
97
-
98
- /**
99
- * Attempt to parse a value as JSON or array.
100
- * @param value - The value to parse
101
- */
102
- const parseIfJson = (value: any) => {
103
- if (typeof value === 'string') {
104
- try {
105
- let parsedValue = JSON.parse(value)
106
- return (parsedValue==value) ? value : parsedValue
107
- } catch {
108
- // If parsing fails, return the original value
109
- return value
110
- }
111
- }
112
- return value
113
- }
114
- </script>
115
-
116
- <template>
117
- <FileBtn
118
- ref="fileBtnRef"
119
- v-bind="$attrs"
120
- color="primary"
121
- :loading="loading"
122
- text="Import CSV"
123
- accept=".csv, .xlsx"
124
- :multiple="false"
125
- @update:model-value="uploadedFile"
126
- :tooltip="props.tooltip"
127
- >
128
- <template
129
- v-for="(_, name, index) in ($slots as {})"
130
- :key="index"
131
- #[name]="slotData"
132
- >
133
- <slot
134
- :name="name"
135
- v-bind="((slotData || {}) as object)"
136
- />
137
- </template>
138
- </FileBtn>
139
- </template>
1
+ <script lang="ts" setup>
2
+ /**
3
+ * ImportCSV imports spreadsheet rows, maps nested object keys, and emits normalized records for downstream processing.
4
+ * This doc block is consumed by vue-docgen for generated API documentation.
5
+ */
6
+ import * as XLSX from 'xlsx'
7
+ import { ref } from 'vue'
8
+ import { useAlert } from '../composables/alert'
9
+ import { VBtn } from 'vuetify/components/VBtn'
10
+
11
+ interface ImportButtonProps extends /* @vue-ignore */ InstanceType<typeof VBtn['$props']> {
12
+ stringFields?: Array<string> // Field paths that must stay as string values (no nested object conversion).
13
+ tooltip?: string | Record<string,any> | undefined // Tooltip text or config object shown for the action control.
14
+ }
15
+
16
+ /**
17
+ * Public props accepted by ImportCSV.
18
+ * Document each prop field with intent, defaults, and side effects for clear generated docs.
19
+ */
20
+ const props = withDefaults(defineProps<ImportButtonProps>(), {
21
+ stringFields: ()=>[],
22
+ tooltip: ()=>({text: 'Import', location: 'bottom'}),
23
+ })
24
+
25
+ const alert = useAlert()
26
+ /**
27
+ * Custom events emitted by ImportCSV.
28
+ * Parents can listen to these events to react to user actions and internal state changes.
29
+ */
30
+ const emit = defineEmits<{
31
+ (e: 'import', value: object[]): void
32
+ }>()
33
+
34
+ const loading = ref(false)
35
+ const fileBtnRef = ref()
36
+
37
+ function uploadedFile(files: File[] | File | undefined) {
38
+ if (!files) return
39
+
40
+ if (Array.isArray(files) && files.length != 1) {
41
+ alert?.addAlert({ message: 'Please select a single file for import', alertType: 'error' })
42
+ return
43
+ }
44
+
45
+ if (Array.isArray(files)) files = files[0]
46
+
47
+ const fileExtension = files.name.slice(files.name.lastIndexOf('.')).toLowerCase()
48
+ if (!['.xlsx', '.csv'].includes(fileExtension)) {
49
+ alert?.addAlert({ message: `Please upload a file with .csv or .xlsx extension only (${files.name})`, alertType: 'error' })
50
+ return
51
+ }
52
+
53
+ const reader = new FileReader()
54
+ reader.onload = (e: ProgressEvent<FileReader>) => {
55
+ const workbook = XLSX.read(e.target?.result)
56
+ const parsedData = parseAndAggregateColumns(
57
+ XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]])
58
+ )
59
+ emit('import', parsedData)
60
+ loading.value = false
61
+ fileBtnRef.value.reset()
62
+ }
63
+ loading.value = true
64
+ reader.readAsArrayBuffer(files)
65
+ }
66
+
67
+ /**
68
+ * Recursively aggregates nested columns and properties.
69
+ * @param items - Array of data from the Excel sheet
70
+ */
71
+ const parseAndAggregateColumns = (items: any[]) => {
72
+ return items.map((item: any) => {
73
+ const aggregatedItem: any = {}
74
+
75
+ for (const key in item) {
76
+ if (key.includes('.')) {
77
+ // Extract root key and subKey
78
+ const [rootKey, ...subKeys] = key.split('.')
79
+
80
+ // Recursively aggregate subKeys
81
+ aggregatedItem[rootKey] = aggregatedItem[rootKey] || {}
82
+ assignNestedValue(aggregatedItem[rootKey], subKeys, parseIfJson(item[key]))
83
+ } else {
84
+ // Directly assign root-level properties
85
+ if (props.stringFields.includes(key)) aggregatedItem[key] = item[key]
86
+ else aggregatedItem[key] = parseIfJson(item[key])
87
+ }
88
+ }
89
+
90
+ return aggregatedItem
91
+ })
92
+ }
93
+
94
+ /**
95
+ * Recursively assigns a value to a nested key structure.
96
+ * @param obj - The object to assign to
97
+ * @param keys - Array of keys leading to the final property
98
+ * @param value - The value to assign
99
+ */
100
+ const assignNestedValue = (obj: any, keys: string[], value: any) => {
101
+ const [currentKey, ...remainingKeys] = keys
102
+ if (remainingKeys.length === 0) {
103
+ obj[currentKey] = value
104
+ } else {
105
+ obj[currentKey] = obj[currentKey] || {}
106
+ assignNestedValue(obj[currentKey], remainingKeys, value)
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Attempt to parse a value as JSON or array.
112
+ * @param value - The value to parse
113
+ */
114
+ const parseIfJson = (value: any) => {
115
+ if (typeof value === 'string') {
116
+ try {
117
+ let parsedValue = JSON.parse(value)
118
+ return (parsedValue==value) ? value : parsedValue
119
+ } catch {
120
+ // If parsing fails, return the original value
121
+ return value
122
+ }
123
+ }
124
+ return value
125
+ }
126
+ </script>
127
+
128
+ <template>
129
+ <FileBtn
130
+ ref="fileBtnRef"
131
+ v-bind="$attrs"
132
+ color="primary"
133
+ :loading="loading"
134
+ text="Import CSV"
135
+ accept=".csv, .xlsx"
136
+ :multiple="false"
137
+ @update:model-value="uploadedFile"
138
+ :tooltip="props.tooltip"
139
+ >
140
+ <template
141
+ v-for="(_, name, index) in ($slots as {})"
142
+ :key="index"
143
+ #[name]="slotData"
144
+ >
145
+ <slot
146
+ :name="name"
147
+ v-bind="((slotData || {}) as object)"
148
+ />
149
+ </template>
150
+ </FileBtn>
151
+ </template>