@ramathibodi/nuxt-commons 0.0.1

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 (49) hide show
  1. package/README.md +81 -0
  2. package/dist/module.cjs +5 -0
  3. package/dist/module.d.mts +7 -0
  4. package/dist/module.d.ts +7 -0
  5. package/dist/module.json +8 -0
  6. package/dist/module.mjs +34 -0
  7. package/dist/runtime/components/Alert.vue +52 -0
  8. package/dist/runtime/components/BarcodeReader.vue +98 -0
  9. package/dist/runtime/components/Calendar.vue +99 -0
  10. package/dist/runtime/components/Camera.vue +116 -0
  11. package/dist/runtime/components/ExportCSV.vue +55 -0
  12. package/dist/runtime/components/FileBtn.vue +56 -0
  13. package/dist/runtime/components/ImportCSV.vue +64 -0
  14. package/dist/runtime/components/Pdf/Print.vue +63 -0
  15. package/dist/runtime/components/Pdf/View.vue +70 -0
  16. package/dist/runtime/components/TabsGroup.vue +28 -0
  17. package/dist/runtime/components/TextBarcode.vue +52 -0
  18. package/dist/runtime/components/dialog/Confirm.vue +100 -0
  19. package/dist/runtime/components/dialog/Index.vue +72 -0
  20. package/dist/runtime/components/dialog/Loading.vue +34 -0
  21. package/dist/runtime/components/form/Date.vue +163 -0
  22. package/dist/runtime/components/form/DateTime.vue +107 -0
  23. package/dist/runtime/components/form/File.vue +187 -0
  24. package/dist/runtime/components/form/Login.vue +131 -0
  25. package/dist/runtime/components/form/Pad.vue +179 -0
  26. package/dist/runtime/components/form/SignPad.vue +186 -0
  27. package/dist/runtime/components/form/Time.vue +158 -0
  28. package/dist/runtime/components/form/images/CameraCrop.vue +58 -0
  29. package/dist/runtime/components/form/images/Edit.vue +143 -0
  30. package/dist/runtime/components/form/images/Preview.vue +48 -0
  31. package/dist/runtime/components/label/Date.vue +29 -0
  32. package/dist/runtime/components/label/FormatMoney.vue +29 -0
  33. package/dist/runtime/composables/alert.d.ts +13 -0
  34. package/dist/runtime/composables/alert.mjs +44 -0
  35. package/dist/runtime/composables/utils/validation.d.ts +32 -0
  36. package/dist/runtime/composables/utils/validation.mjs +36 -0
  37. package/dist/runtime/labs/form/EditMobile.vue +153 -0
  38. package/dist/runtime/labs/form/TextFieldMask.vue +43 -0
  39. package/dist/runtime/plugins/vueSignaturePad.d.ts +2 -0
  40. package/dist/runtime/plugins/vueSignaturePad.mjs +5 -0
  41. package/dist/runtime/types/alert.d.ts +11 -0
  42. package/dist/runtime/types/modules.d.ts +5 -0
  43. package/dist/runtime/utils/datetime.d.ts +25 -0
  44. package/dist/runtime/utils/datetime.mjs +166 -0
  45. package/dist/runtime/utils/object.d.ts +8 -0
  46. package/dist/runtime/utils/object.mjs +28 -0
  47. package/dist/types.d.mts +16 -0
  48. package/dist/types.d.ts +16 -0
  49. package/package.json +90 -0
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ <!--
2
+ Get your module up and running quickly.
3
+
4
+ Find and replace all on all files (CMD+SHIFT+F):
5
+ - Name: Ramahis Common Component
6
+ - Package name: @ramahis/common-components
7
+ -->
8
+
9
+ # Ramahis Common Component
10
+
11
+ [![npm version][npm-version-src]][npm-version-href]
12
+ [![npm downloads][npm-downloads-src]][npm-downloads-href]
13
+ [![License][license-src]][license-href]
14
+ [![Nuxt][nuxt-src]][nuxt-href]
15
+
16
+
17
+ ## Quick Setup
18
+
19
+ 1. Add `@deverloprama/common-components` dependency to your project
20
+
21
+ ```bash
22
+ # Using pnpm
23
+ pnpm add -D @deverloprama/common-components
24
+
25
+ # Using yarn
26
+ yarn add --dev @deverloprama/common-components
27
+
28
+ # Using npm
29
+ npm install --save-dev @deverloprama/common-components
30
+ ```
31
+
32
+ 2. Add `my-module` to the `modules` section of `nuxt.config.ts`
33
+
34
+ ```js
35
+ export default defineNuxtConfig({
36
+ modules: [
37
+ '@deverloprama/common-components'
38
+ ]
39
+ })
40
+ ```
41
+
42
+ That's it! You can now use My Module in your Nuxt app ✨
43
+
44
+ ## Development
45
+
46
+ ```bash
47
+ # Install dependencies
48
+ npm install
49
+
50
+ # Generate type stubs
51
+ npm run dev:prepare
52
+
53
+ # Develop with the playground
54
+ npm run dev
55
+
56
+ # Build the playground
57
+ npm run dev:build
58
+
59
+ # Run ESLint
60
+ npm run lint
61
+
62
+ # Run Vitest
63
+ npm run test
64
+ npm run test:watch
65
+
66
+ # Release new version
67
+ npm run release
68
+ ```
69
+
70
+ <!-- Badges -->
71
+ [npm-version-src]: https://img.shields.io/npm/v/my-module/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
72
+ [npm-version-href]: https://npmjs.com/package/my-module
73
+
74
+ [npm-downloads-src]: https://img.shields.io/npm/dm/my-module.svg?style=flat&colorA=18181B&colorB=28CF8D
75
+ [npm-downloads-href]: https://npmjs.com/package/my-module
76
+
77
+ [license-src]: https://img.shields.io/npm/l/my-module.svg?style=flat&colorA=18181B&colorB=28CF8D
78
+ [license-href]: https://npmjs.com/package/my-module
79
+
80
+ [nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js
81
+ [nuxt-href]: https://nuxt.com
@@ -0,0 +1,5 @@
1
+ module.exports = function(...args) {
2
+ return import('./module.mjs').then(m => m.default.call(this, ...args))
3
+ }
4
+ const _meta = module.exports.meta = require('./module.json')
5
+ module.exports.getMeta = () => Promise.resolve(_meta)
@@ -0,0 +1,7 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ interface ModuleOptions {
4
+ }
5
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions>;
6
+
7
+ export { type ModuleOptions, _default as default };
@@ -0,0 +1,7 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ interface ModuleOptions {
4
+ }
5
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions>;
6
+
7
+ export { type ModuleOptions, _default as default };
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "@ramathibodi/nuxt-commons",
3
+ "configKey": "nuxt-commons",
4
+ "compatibility": {
5
+ "nuxt": "^3.0.0"
6
+ },
7
+ "version": "0.0.1"
8
+ }
@@ -0,0 +1,34 @@
1
+ import { defineNuxtModule, createResolver, addComponentsDir, addImportsDir, addTypeTemplate } from '@nuxt/kit';
2
+
3
+ const module = defineNuxtModule({
4
+ meta: {
5
+ name: "@ramathibodi/nuxt-commons",
6
+ configKey: "nuxt-commons",
7
+ compatibility: {
8
+ nuxt: "^3.0.0"
9
+ }
10
+ },
11
+ // Default configuration options of the Nuxt module
12
+ defaults: {},
13
+ async setup(_options, _nuxt) {
14
+ const resolver = createResolver(import.meta.url);
15
+ await addComponentsDir({
16
+ path: resolver.resolve("runtime/components"),
17
+ pathPrefix: true,
18
+ global: true
19
+ });
20
+ addImportsDir(resolver.resolve("runtime/composables"));
21
+ addTypeTemplate({
22
+ filename: "types/modules.d.ts",
23
+ getContents: () => `
24
+ declare module 'painterro';
25
+ declare module "@zxing/browser"
26
+ declare module "vue-signature-pad"
27
+ declare module "accounting"
28
+ declare module "pdf-vue3"
29
+ `
30
+ });
31
+ }
32
+ });
33
+
34
+ export { module as default };
@@ -0,0 +1,52 @@
1
+ <script lang="ts" setup>
2
+ import { ref, watch } from 'vue'
3
+ import { useAlert } from '../composables/alert'
4
+ import type { AlertItem } from '../types/alert'
5
+
6
+ const isAlertOpen = ref(false)
7
+ const timeout = ref(3000)
8
+ const alert = useAlert()
9
+ const currentItem = ref<AlertItem | undefined>()
10
+
11
+ const renewAlert = () => {
12
+ if (alert?.hasAlert()) {
13
+ currentItem.value = alert?.takeAlert()
14
+ isAlertOpen.value = true
15
+ }
16
+ else {
17
+ currentItem.value = undefined
18
+ isAlertOpen.value = false
19
+ }
20
+ }
21
+
22
+ watch(() => alert?.hasAlert(), (hasAlert) => {
23
+ if (hasAlert) {
24
+ renewAlert()
25
+ }
26
+ })
27
+ </script>
28
+
29
+ <template>
30
+ <VSnackbar
31
+ v-if="currentItem"
32
+ v-model="isAlertOpen"
33
+ :timeout="timeout"
34
+ location="center"
35
+ multi-line
36
+ variant="text"
37
+ @update:model-value="renewAlert()"
38
+ >
39
+ <!-- @vue-expected-error Type conversion problem -->
40
+ <VAlert
41
+ v-model="isAlertOpen"
42
+ closable
43
+ :type="currentItem.alertType"
44
+ elevation="2"
45
+ theme="dark"
46
+ variant="flat"
47
+ @click:close="renewAlert()"
48
+ >
49
+ {{ currentItem.statusCode ? currentItem.statusCode + ' ' : '' }}{{ currentItem.message }}{{ currentItem.data ? ' ' + currentItem.data : '' }}
50
+ </VAlert>
51
+ </VSnackbar>
52
+ </template>
@@ -0,0 +1,98 @@
1
+ <script lang="ts" setup>
2
+ import { BrowserMultiFormatReader } from '@zxing/browser'
3
+ import { type IScannerControls } from '@zxing/browser/esm'
4
+ import type { Exception, Result } from '@zxing/library'
5
+ import { ref, onMounted } from 'vue'
6
+ import { useAlert } from '../composables/alert'
7
+
8
+ const videoElementRef = ref<HTMLVideoElement | null>(null)
9
+ const barcodeReader = new BrowserMultiFormatReader()
10
+ const alert = useAlert()
11
+ const hasCameraAvailable = ref(false)
12
+
13
+ const emit = defineEmits<{
14
+ (event: 'decode', barcodeValue: string): void
15
+ (event: 'error', error: string | unknown): void
16
+ }>()
17
+
18
+ async function checkCameraAvailability() {
19
+ const devices = await navigator.mediaDevices.enumerateDevices()
20
+ hasCameraAvailable.value = devices.some(device => device.kind === 'videoinput')
21
+ }
22
+
23
+ async function startCamera() {
24
+ try {
25
+ const videoInputDevices = await BrowserMultiFormatReader.listVideoInputDevices()
26
+ if (videoInputDevices.length === 0) {
27
+ alert?.addAlert({ message: 'No camera devices found.', alertType: 'error' })
28
+ return
29
+ }
30
+
31
+ const selectedDeviceId = videoInputDevices[0].deviceId
32
+ barcodeReader.decodeFromVideoDevice(selectedDeviceId, videoElementRef.value, (result: Result | undefined, error: Exception | undefined, controls: IScannerControls) => {
33
+ if (result) {
34
+ emit('decode', result.getText())
35
+ controls.stop()
36
+ }
37
+ })
38
+ }
39
+ catch (error) {
40
+ emit('error', error)
41
+ alert?.addAlert({ message: 'Error starting camera.', alertType: 'error' })
42
+ }
43
+ }
44
+
45
+ function scanImageFile(selectedFile: File | File[] | undefined) {
46
+ if (!selectedFile) {
47
+ alert?.addAlert({ message: 'No file selected.', alertType: 'error' })
48
+ return
49
+ }
50
+
51
+ const reader = new FileReader()
52
+ reader.onload = async (event) => {
53
+ try {
54
+ const result = await barcodeReader.decodeFromImageUrl(event.target?.result as string)
55
+ emit('decode', result.getText())
56
+ }
57
+ catch (error) {
58
+ alert?.addAlert({ message: 'Unable to read barcode from image.', alertType: 'error' })
59
+ }
60
+ }
61
+ const scanImageSingleFile: File = Array.isArray(selectedFile) ? selectedFile[0] : selectedFile
62
+ reader.readAsDataURL(scanImageSingleFile)
63
+ }
64
+
65
+ onMounted(async () => {
66
+ await checkCameraAvailability()
67
+ if (hasCameraAvailable.value) await startCamera()
68
+ })
69
+ </script>
70
+
71
+ <template>
72
+ <v-card flat>
73
+ <v-card-item>
74
+ <v-col v-if="hasCameraAvailable">
75
+ <video
76
+ ref="videoElementRef"
77
+ autoplay
78
+ style="max-width: 1024px"
79
+ />
80
+ <div style="z-index: 2000; position: relative; bottom: 84px; left: 550px">
81
+ <FileBtn
82
+ accept="image/*"
83
+ icon="mdi mdi-image-plus"
84
+ icon-only
85
+ @update:model-value="scanImageFile"
86
+ />
87
+ </div>
88
+ </v-col>
89
+ <v-col v-else>
90
+ <FileBtn
91
+ accept="image/*"
92
+ text="Upload Image"
93
+ @update:model-value="scanImageFile"
94
+ />
95
+ </v-col>
96
+ </v-card-item>
97
+ </v-card>
98
+ </template>
@@ -0,0 +1,99 @@
1
+ <script lang="ts" setup>
2
+ import FullCalendar from '@fullcalendar/vue3'
3
+ import dayGridPlugin from '@fullcalendar/daygrid'
4
+ import timeGridPlugin from '@fullcalendar/timegrid'
5
+ import listPlugin from '@fullcalendar/list'
6
+ import interactionPlugin from '@fullcalendar/interaction'
7
+ import { ref, computed, withDefaults } from 'vue'
8
+ import { type CalendarOptions } from '@fullcalendar/core'
9
+
10
+ type Event = {
11
+ id: string | undefined
12
+ title: string
13
+ start: string // YYYY-MM-DD | YYYY-MM-DD HH:mm:ss
14
+ end: string // YYYY-MM-DD
15
+ color: string
16
+ detail?: string
17
+ props: Record<string, any> | any[]
18
+ }
19
+
20
+ interface CalendarProps {
21
+ modelValue: Event[]
22
+ locale?: 'th' | 'en'
23
+ toolBarLeft?: string
24
+ toolBarCenter?: string
25
+ toolBarRight?: string
26
+ height?: string | number
27
+ selectViewDay?: string
28
+ mode: 'dayGridMonth' | 'timeGridDay'
29
+ }
30
+
31
+ const props = withDefaults(defineProps<CalendarProps>(), {
32
+ locale: 'th',
33
+ height: 1200,
34
+ toolBarLeft: 'today prev,next',
35
+ toolBarCenter: 'title',
36
+ toolBarRight: 'timeGridDay,timeGridWeek,dayGridMonth',
37
+ mode: 'dayGridMonth',
38
+ })
39
+
40
+ const emit = defineEmits(['select', 'dialog'])
41
+
42
+ const buttonText = computed(() => {
43
+ return {
44
+ today: props.locale === 'th' ? 'วันนี้' : 'today',
45
+ month: props.locale === 'th' ? 'เดือน' : 'month',
46
+ week: props.locale === 'th' ? 'สัปดาห์' : 'week',
47
+ day: props.locale === 'th' ? 'วัน' : 'day',
48
+ list: props.locale === 'th' ? 'รายการ' : 'list',
49
+ year: props.locale === 'th' ? 'ปี' : 'year',
50
+ }
51
+ })
52
+
53
+ const calendarRef = ref<InstanceType<typeof FullCalendar>>()
54
+ const dateViewDay = ref({ date: '', props: {} })
55
+
56
+ const handleDateClick = (info: any) => {
57
+ emit('select', info.event)
58
+ }
59
+
60
+ const handleEventClick = (info: any) => {
61
+ emit('dialog', true)
62
+ emit('select', info.event)
63
+ }
64
+
65
+ const eventContentDay = (arg: any) => {
66
+ const list = document.createElement('div')
67
+ list.innerHTML = document.getElementById(arg.event.id.toString())?.innerHTML || ''
68
+ return { domNodes: [list] }
69
+ }
70
+
71
+ // @ts-ignore
72
+ const calendarOptions = ref<CalendarOptions>({
73
+ plugins: [dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin],
74
+ initialView: props.mode,
75
+ locale: props.locale,
76
+ dayMaxEvents: true,
77
+ timeZone: 'UTC',
78
+ headerToolbar: {
79
+ left: props.toolBarLeft,
80
+ center: props.toolBarCenter,
81
+ right: props.toolBarRight,
82
+ },
83
+ buttonText: buttonText.value,
84
+ events: props.modelValue,
85
+ initialDate: props.selectViewDay,
86
+ dateClick: handleDateClick,
87
+ eventClick: handleEventClick,
88
+ nowIndicator: true,
89
+ height: props.height,
90
+ eventColor: '#378006',
91
+ })
92
+ </script>
93
+
94
+ <template>
95
+ <FullCalendar
96
+ ref="calendarRef"
97
+ :options="calendarOptions as CalendarOptions"
98
+ />
99
+ </template>
@@ -0,0 +1,116 @@
1
+ <script lang="ts" setup>
2
+ import { ref, onMounted, watchEffect } from 'vue'
3
+ import { useDevicesList, useUserMedia } from '@vueuse/core'
4
+ import { useAlert } from '../composables/alert'
5
+
6
+ interface Props {
7
+ modelValue?: string
8
+ }
9
+ const props = defineProps<Props>()
10
+ const emit = defineEmits(['update:modelValue', 'closeDialog'])
11
+
12
+ const alert = useAlert()
13
+ const videoRef = ref<HTMLVideoElement>()
14
+ const capturedPhoto = ref<string | null>(null)
15
+ const currentCameraId = ref<ConstrainDOMString | undefined>()
16
+
17
+ const { videoInputs: cameras } = useDevicesList({
18
+ requestPermissions: true,
19
+ constraints: { audio: false, video: true },
20
+ onUpdated() {
21
+ if (!cameras.value.find(camera => camera.deviceId === currentCameraId.value))
22
+ currentCameraId.value = cameras.value[0]?.deviceId
23
+ },
24
+ })
25
+
26
+ const { stream, enabled } = useUserMedia({
27
+ constraints: { video: { deviceId: currentCameraId.value } },
28
+ })
29
+
30
+ watchEffect(() => {
31
+ if (videoRef.value) (videoRef.value as HTMLVideoElement).srcObject = stream.value!
32
+ })
33
+
34
+ const captureImage = () => {
35
+ if (videoRef.value) {
36
+ const canvas = document.createElement('canvas')
37
+ canvas.width = (videoRef.value as HTMLVideoElement).videoWidth
38
+ canvas.height = (videoRef.value as HTMLVideoElement).videoHeight
39
+ const context = canvas.getContext('2d')
40
+ if (context) {
41
+ context.drawImage(videoRef.value as HTMLVideoElement, 0, 0, canvas.width, canvas.height)
42
+ capturedPhoto.value = canvas.toDataURL('image/jpeg')
43
+
44
+ enabled.value = false
45
+ emit('update:modelValue', capturedPhoto.value)
46
+ }
47
+ }
48
+ }
49
+
50
+ function loadImageFile(selectedFile: File | File[] | undefined) {
51
+ if (!selectedFile) {
52
+ alert?.addAlert({ message: 'No file selected.', alertType: 'error' })
53
+ return
54
+ }
55
+
56
+ const reader = new FileReader()
57
+ reader.onload = (event) => {
58
+ capturedPhoto.value = event.target?.result as string
59
+
60
+ enabled.value = false
61
+ emit('update:modelValue', capturedPhoto.value)
62
+ }
63
+ const scanImageSingleFile: File = Array.isArray(selectedFile) ? selectedFile[0] : selectedFile
64
+ reader.readAsDataURL(scanImageSingleFile)
65
+ }
66
+
67
+ const openCamera = () => {
68
+ enabled.value = true
69
+ }
70
+
71
+ const closeDialog = () => {
72
+ enabled.value = false
73
+ emit('closeDialog')
74
+ }
75
+
76
+ defineExpose({
77
+ openCamera,
78
+ })
79
+
80
+ onMounted(() => {
81
+ enabled.value = true
82
+ })
83
+ </script>
84
+
85
+ <template>
86
+ <v-card>
87
+ <v-card-title class="d-flex justify-end">
88
+ <v-btn
89
+ icon="fa:fa-solid fa-xmark"
90
+ variant="text"
91
+ @click="closeDialog"
92
+ />
93
+ </v-card-title>
94
+ <v-card-text class="d-flex justify-center">
95
+ <video
96
+ ref="videoRef"
97
+ autoplay
98
+ style="max-width: 1024px"
99
+ />
100
+ <div style="z-index: 2000; position: relative; bottom: -410px; left: -80px; height: 50px">
101
+ <FileBtn
102
+ accept="image/*"
103
+ icon="mdi mdi-image-plus"
104
+ icon-only
105
+ @update:model-value="loadImageFile"
106
+ />
107
+ </div>
108
+ </v-card-text>
109
+ <v-card-actions class="d-flex justify-center">
110
+ <v-btn icon size="x-large" variant="tonal" @click="captureImage()">
111
+ <v-icon icon="fa:fa-solid fa-circle" size="x-large"></v-icon>
112
+ <v-tooltip activator="parent" location="top">ถ่ายภาพ</v-tooltip>
113
+ </v-btn>
114
+ </v-card-actions>
115
+ </v-card>
116
+ </template>
@@ -0,0 +1,55 @@
1
+ <script lang="ts" setup>
2
+ import { ref, withDefaults } 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
+ modelValue?: object[]
10
+ }
11
+
12
+ const props = withDefaults(defineProps<ExportButtonProps>(), {
13
+ fileName: 'download',
14
+ })
15
+
16
+ const alert = useAlert()
17
+ const loading = ref(false)
18
+
19
+ function exportFile() {
20
+ if (props.modelValue && Array.isArray(props.modelValue)) {
21
+ loading.value = true
22
+ const workbook = XLSX.utils.book_new()
23
+ const worksheet = XLSX.utils.json_to_sheet(props.modelValue)
24
+ const fileName = `${props.fileName}.xlsx`
25
+ XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
26
+ XLSX.writeFile(workbook, fileName)
27
+ loading.value = false
28
+ }
29
+ else {
30
+ alert?.addAlert({ message: 'Invalid or no data to export', alertType: 'error' })
31
+ }
32
+ }
33
+ </script>
34
+
35
+ <template>
36
+ <VBtn
37
+ v-bind="$attrs"
38
+ color="primary"
39
+ :loading="loading"
40
+ :disabled="loading"
41
+ text="Export CSV"
42
+ @click="exportFile"
43
+ >
44
+ <template
45
+ v-for="(_, slot, index) in ($slots as {})"
46
+ :key="index"
47
+ #[slot]="scope"
48
+ >
49
+ <slot
50
+ :name="slot"
51
+ v-bind="{ scope }"
52
+ />
53
+ </template>
54
+ </VBtn>
55
+ </template>
@@ -0,0 +1,56 @@
1
+ <script lang="ts" setup>
2
+ import { ref, watch } 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
+ }
11
+
12
+ const props = withDefaults(defineProps<Props>(), {
13
+ multiple: false,
14
+ accept: '*',
15
+ })
16
+
17
+ const emit = defineEmits<{
18
+ (event: 'update:modelValue', value: File | File[] | undefined): void
19
+ }>()
20
+
21
+ const fileInput = ref<HTMLInputElement>()
22
+ const files = ref<File | File[]>()
23
+
24
+ const openFileInput = () => {
25
+ fileInput.value?.click()
26
+ }
27
+
28
+ watch(files, () => {
29
+ emit('update:modelValue', files.value)
30
+ }, { deep: true })
31
+ </script>
32
+
33
+ <template>
34
+ <v-btn
35
+ v-bind="$attrs"
36
+ @click="openFileInput"
37
+ >
38
+ <template
39
+ v-for="(_, slot, index) in ($slots as {})"
40
+ :key="index"
41
+ #[slot]="scope"
42
+ >
43
+ <slot
44
+ :name="slot"
45
+ v-bind="{ scope }"
46
+ />
47
+ </template>
48
+ </v-btn>
49
+ <v-file-input
50
+ ref="fileInput"
51
+ v-model="files"
52
+ :accept="props.accept"
53
+ :multiple="props.multiple"
54
+ style="display: none"
55
+ />
56
+ </template>
@@ -0,0 +1,64 @@
1
+ <script lang="ts" setup>
2
+ import * as XLSX from 'xlsx'
3
+ import { ref } from 'vue'
4
+ import { useAlert } from '../composables/alert'
5
+
6
+ const alert = useAlert()
7
+ const emit = defineEmits<{
8
+ (e: 'import', value: object[]): void
9
+ }>()
10
+
11
+ const loading = ref(false)
12
+
13
+ function uploadedFile(files: File[] | File | undefined) {
14
+ if (!files) {
15
+ alert?.addAlert({ message: 'Please upload a file again', alertType: 'error' })
16
+ return
17
+ }
18
+
19
+ if (Array.isArray(files) && files.length != 1) {
20
+ alert?.addAlert({ message: 'Please select a single file for import', alertType: 'error' })
21
+ return
22
+ }
23
+
24
+ if (Array.isArray(files)) files = files[0]
25
+
26
+ const fileExtension = files.name.slice(files.name.lastIndexOf('.')).toLowerCase()
27
+ if (!['.xlsx', '.csv'].includes(fileExtension)) {
28
+ alert?.addAlert({ message: 'Please upload a file with .csv or .xlsx extension only (' + files.name + ')', alertType: 'error' })
29
+ return
30
+ }
31
+
32
+ const reader = new FileReader()
33
+ reader.onload = (e: ProgressEvent<FileReader>) => {
34
+ const workbook = XLSX.read(e.target?.result)
35
+ emit('import', XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]]))
36
+ loading.value = false
37
+ }
38
+ loading.value = true
39
+ reader.readAsArrayBuffer(files)
40
+ }
41
+ </script>
42
+
43
+ <template>
44
+ <FileBtn
45
+ v-bind="$attrs"
46
+ color="primary"
47
+ :loading="loading"
48
+ text="Import CSV"
49
+ accept=".csv, .xlsx"
50
+ :multiple="false"
51
+ @update:model-value="uploadedFile"
52
+ >
53
+ <template
54
+ v-for="(_, slot, index) in ($slots as {})"
55
+ :key="index"
56
+ #[slot]="scope"
57
+ >
58
+ <slot
59
+ :name="slot"
60
+ v-bind="{ scope }"
61
+ />
62
+ </template>
63
+ </FileBtn>
64
+ </template>