@mixd-id/web-scaffold 0.1.2301231326 → 0.1.2301231328

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mixd-id/web-scaffold",
3
3
  "private": false,
4
- "version": "0.1.2301231326",
4
+ "version": "0.1.2301231328",
5
5
  "scripts": {
6
6
  "dev": "vite serve",
7
7
  "build": "vite build",
@@ -11,23 +11,30 @@
11
11
  "exports": {
12
12
  ".": "./src/index.js",
13
13
  "./themes/default": "./src/themes/default/index.js",
14
- "./themes/kliknss": "./src/themes/kliknss/index.js",
15
14
  "./UI.vue": "./src/UI.vue",
16
- "./helpers.mjs": "./src/helpers.mjs"
15
+ "./helpers": {
16
+ "require": "./src/utils/helpers.js",
17
+ "import": "./src/utils/helpers.mjs"
18
+ },
19
+ "./importer": "./src/utils/importer.js"
17
20
  },
18
21
  "dependencies": {
19
22
  "@faker-js/faker": "^7.3.0",
20
23
  "@tailwindcss/line-clamp": "^0.4.0",
21
24
  "@vueuse/core": "^9.0.2",
25
+ "adm-zip": "^0.5.10",
22
26
  "axios": "^0.27.2",
23
27
  "compression": "^1.7.4",
24
28
  "cookie-parser": "^1.4.6",
25
29
  "cors": "^2.8.5",
26
30
  "daisyui": "^2.19.0",
27
31
  "dayjs": "^1.11.2",
32
+ "exceljs": "^4.3.0",
28
33
  "express": "^4.18.1",
34
+ "file-type": "^18.2.1",
29
35
  "glob": "^8.0.3",
30
- "lodash-es": "^4.17.21",
36
+ "lodash": "^4.17.21",
37
+ "md5": "^2.3.0",
31
38
  "nprogress": "^0.2.0",
32
39
  "pinia": "^2.0.14",
33
40
  "prismjs": "^1.28.0",
@@ -46,7 +46,7 @@
46
46
 
47
47
  <script>
48
48
 
49
- import { parseBoolean } from "../helper";
49
+ import { parseBoolean } from "../utils/helpers.mjs";
50
50
 
51
51
  export default{
52
52
 
@@ -16,7 +16,7 @@
16
16
 
17
17
  <script>
18
18
 
19
- import {parseBoolean} from "../helper";
19
+ import { parseBoolean } from "../utils/helpers.mjs";
20
20
 
21
21
  export default{
22
22
 
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <Teleport to=".Y29u">
3
- <div v-if="!!(state)" :class="$style.contextMenu" :style="computedStyle" ref="contextMenu">
3
+ <div v-if="!!(computedState)" :class="$style.contextMenu" :style="computedStyle" ref="contextMenu">
4
4
  <div :class="bodyClass">
5
5
  <slot></slot>
6
6
  </div>
@@ -41,12 +41,16 @@ export default {
41
41
 
42
42
  computed:{
43
43
 
44
+ computedState(){
45
+ return this.state || this.isOpen
46
+ }
47
+
44
48
  },
45
49
 
46
50
  data(){
47
51
  return {
48
52
  computedStyle: null,
49
- isActive: false
53
+ isOpen: false
50
54
  }
51
55
  },
52
56
 
@@ -54,75 +58,7 @@ export default {
54
58
 
55
59
  state(to){
56
60
  if(to){
57
- if(this.caller){
58
-
59
- const caller = this.caller.$el ? this.caller.$el :
60
- this.caller instanceof HTMLElement ? this.caller : null
61
-
62
- if(caller){
63
-
64
- this.$nextTick(() => {
65
- const rect = caller.getBoundingClientRect()
66
-
67
- let maxHeight, bottom
68
-
69
- switch(this.position){
70
-
71
- case 'bottom-right':
72
- this.computedStyle = {
73
- right: (window.innerWidth - (Math.round(rect.x) + rect.width)) + 'px',
74
- top: Math.round(rect.y + rect.height + 8) + 'px',
75
- maxHeight: (window.innerHeight - Math.round(rect.y + rect.height + 16)) + 'px',
76
- transformOrigin: 'top right'
77
- }
78
- break
79
-
80
- case 'top-left':
81
-
82
- maxHeight = Math.round(rect.top - 16)
83
- bottom = window.innerHeight - rect.top + 8
84
-
85
- this.computedStyle = {
86
- left: Math.round(rect.x) + 'px',
87
- bottom: bottom + 'px',
88
- maxHeight: maxHeight + 'px',
89
- transformOrigin: 'bottom left'
90
- }
91
- break
92
-
93
- case 'top-right':
94
-
95
- maxHeight = Math.round(rect.top - 16)
96
- bottom = window.innerHeight - rect.top + 8
97
-
98
- this.computedStyle = {
99
- right: (window.innerWidth - (Math.round(rect.x) + rect.width)) + 'px',
100
- bottom: bottom + 'px',
101
- maxHeight: maxHeight + 'px',
102
- transformOrigin: 'bottom right'
103
- }
104
- break
105
-
106
- default:
107
- this.computedStyle = {
108
- left: Math.round(rect.x) + 'px',
109
- top: Math.round(rect.y + rect.height + 8) + 'px',
110
- maxHeight: (window.innerHeight - Math.round(rect.y + rect.height + 16)) + 'px',
111
- transformOrigin: 'top left'
112
- }
113
- }
114
-
115
- this.onOpen()
116
- })
117
-
118
- }
119
- else{
120
- console.error('Undefined caller for context menu')
121
- }
122
- }
123
- else{
124
- console.warn('Invalid context menu caller', this.caller)
125
- }
61
+ this.open()
126
62
  }
127
63
  else{
128
64
  window.removeEventListener('click', this.onDismiss)
@@ -162,10 +98,91 @@ export default {
162
98
  }
163
99
  this.computedStyle = null
164
100
  this.$emit('dismiss')
101
+ this.isOpen = false
165
102
  }
166
103
 
167
104
  this.$refs.contextMenu.addEventListener('transitionend', transitionEnd)
168
105
  this.$refs.contextMenu.classList.remove(this.$style.active)
106
+ },
107
+
108
+ open(caller){
109
+
110
+ if(caller){
111
+ this.isOpen = true
112
+ }
113
+
114
+ caller = caller ?? this.caller
115
+
116
+ if(caller){
117
+
118
+ caller = caller.$el ? caller.$el :
119
+ caller instanceof HTMLElement ? caller : null
120
+
121
+ if(caller){
122
+
123
+ this.$nextTick(() => {
124
+ const rect = caller.getBoundingClientRect()
125
+
126
+ let maxHeight, bottom
127
+
128
+ switch(this.position){
129
+
130
+ case 'bottom-right':
131
+ this.computedStyle = {
132
+ right: (window.innerWidth - (Math.round(rect.x) + rect.width)) + 'px',
133
+ top: Math.round(rect.y + rect.height + 8) + 'px',
134
+ maxHeight: (window.innerHeight - Math.round(rect.y + rect.height + 16)) + 'px',
135
+ transformOrigin: 'top right'
136
+ }
137
+ break
138
+
139
+ case 'top-left':
140
+
141
+ maxHeight = Math.round(rect.top - 16)
142
+ bottom = window.innerHeight - rect.top + 8
143
+
144
+ this.computedStyle = {
145
+ left: Math.round(rect.x) + 'px',
146
+ bottom: bottom + 'px',
147
+ maxHeight: maxHeight + 'px',
148
+ transformOrigin: 'bottom left'
149
+ }
150
+ break
151
+
152
+ case 'top-right':
153
+
154
+ maxHeight = Math.round(rect.top - 16)
155
+ bottom = window.innerHeight - rect.top + 8
156
+
157
+ this.computedStyle = {
158
+ right: (window.innerWidth - (Math.round(rect.x) + rect.width)) + 'px',
159
+ bottom: bottom + 'px',
160
+ maxHeight: maxHeight + 'px',
161
+ transformOrigin: 'bottom right'
162
+ }
163
+ break
164
+
165
+ default:
166
+ this.computedStyle = {
167
+ left: Math.round(rect.x) + 'px',
168
+ top: Math.round(rect.y + rect.height + 8) + 'px',
169
+ maxHeight: (window.innerHeight - Math.round(rect.y + rect.height + 16)) + 'px',
170
+ transformOrigin: 'top left'
171
+ }
172
+ }
173
+
174
+ this.onOpen()
175
+ })
176
+
177
+ }
178
+ else{
179
+ console.error('Undefined caller for context menu')
180
+ }
181
+ }
182
+ else{
183
+ console.warn('Invalid context menu caller', this.caller)
184
+ }
185
+
169
186
  }
170
187
 
171
188
  }
@@ -0,0 +1,240 @@
1
+ <template>
2
+ <Modal :state="isOpen" width="480" height="560">
3
+ <template v-slot:head>
4
+ <div class="relative p-5">
5
+ <div v-if="step !== 3">
6
+ <h3>{{ title }}</h3>
7
+ <p v-if="step === 1" class="mt-1 text-text-400" v-html="description"></p>
8
+ <p v-else-if="step === 2" class="mt-1 text-text-400" v-html="description2"></p>
9
+ </div>
10
+ <div class="absolute top-0 right-0 p-2">
11
+ <button type="button" class="p-2" @click="isOpen = false">
12
+ <svg width="24" height="24" viewBox="0 0 24 24" class="fill-text-300 hover:fill-red-500" xmlns="http://www.w3.org/2000/svg">
13
+ <path d="M6.53034 5.46965C6.23745 5.17676 5.76257 5.17676 5.46968 5.46965C5.17679 5.76255 5.17679 6.23742 5.46968 6.53031L10.9393 12L5.46967 17.4697C5.17678 17.7626 5.17678 18.2374 5.46967 18.5303C5.76256 18.8232 6.23744 18.8232 6.53033 18.5303L12 13.0606L17.4697 18.5303C17.7626 18.8232 18.2375 18.8232 18.5303 18.5303C18.8232 18.2374 18.8232 17.7626 18.5303 17.4697L13.0607 12L18.5303 6.53032C18.8232 6.23743 18.8232 5.76256 18.5303 5.46966C18.2374 5.17677 17.7626 5.17677 17.4697 5.46966L12 10.9393L6.53034 5.46965Z"/>
14
+ </svg>
15
+ </button>
16
+ </div>
17
+ </div>
18
+ </template>
19
+ <template v-slot:foot>
20
+ <div class="p-5" v-if="step === 2">
21
+ <Button type="button" class="w-[120px]" :disabled="!canImport" :state="isLoading ? 2 : 1" @click="proceedImport">Import</Button>
22
+ </div>
23
+ </template>
24
+
25
+ <div class="flex-1 p-5 flex flex-col items-center justify-center" v-if="step === 1">
26
+ <button type="button" @click="$refs.uploader.click()" class="rounded-3xl w-[200px] bg-base-300 px-6 aspect-square flex items-center justify-center text-center border-dashed border-[3px] border-text-100">
27
+ <div class="text-xl" v-if="!isLoading">
28
+ Klik untuk <br />
29
+ <span class="text-primary text-lg">Upload</span>
30
+ file
31
+ </div>
32
+ <div v-else class="flex items-center justify-center">
33
+ <svg class="animate-spin aspect-square w-[48px] text-primary" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
34
+ </div>
35
+ </button>
36
+ <input type="file" :accept="accept" ref="uploader" class="hidden" @change="onFileUploaded"/>
37
+ <br />
38
+ <br />
39
+ <br />
40
+ <br />
41
+ <div v-if="templateFiles.length > 0" class="flex flex-col gap-2">
42
+ <a v-for="(file, index) in templateFiles" :href="file" class="text-primary">
43
+ Download Sampel {{ templateFiles.length > 1 ? index + 1 : '' }}
44
+ </a>
45
+ </div>
46
+ </div>
47
+
48
+ <div class="flex-1 p-5 flex flex-col gap-3" v-else-if="step === 2">
49
+ <div v-for="(key, index) in importData.keys" class="flex flex-row items-center gap-2">
50
+ <div class="w-[120px]">
51
+ <strong v-if="key.required">{{ key.text }}*</strong>
52
+ <label v-else>{{ key.text }}</label>
53
+ </div>
54
+ <div class="flex-1">
55
+ <Dropdown v-model="importData.keys[index].value">
56
+ <option disabled selected>Pilih Kolom</option>
57
+ <option v-for="column in importData.columns" :value="column">{{ column }}</option>
58
+ </Dropdown>
59
+ </div>
60
+ </div>
61
+ </div>
62
+
63
+ <div class="flex-1 flex items-center justify-center" v-else-if="step === 3">
64
+ <div class="flex flex-col items-center justify-center gap-1">
65
+ <svg width="96" height="96" viewBox="0 0 24 24" class="fill-green-600" xmlns="http://www.w3.org/2000/svg">
66
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12ZM20.5 12C20.5 16.6944 16.6944 20.5 12 20.5C7.30558 20.5 3.5 16.6944 3.5 12C3.5 7.30558 7.30558 3.5 12 3.5C14.5863 3.5 16.9028 4.65509 18.4618 6.47753L11.1768 13.7626C11.0791 13.8602 10.9209 13.8602 10.8232 13.7626L8.03033 10.9697C7.73744 10.6768 7.26256 10.6768 6.96967 10.9697C6.67678 11.2626 6.67678 11.7374 6.96967 12.0303L9.76256 14.8232C10.446 15.5066 11.554 15.5066 12.2374 14.8232L19.3437 7.717C20.0787 8.97466 20.5 10.4381 20.5 12Z" />
67
+ </svg>
68
+ <h3>{{ importData.result.title ?? 'Data berhasil diimport' }}</h3>
69
+ <p class="mb-4 text-text-400 text-center" v-html="importData.result.description"></p>
70
+ <div class="p-2 text-sm text-green-500 border-green-500 border-[1px] rounded-lg">
71
+ Total Data: {{ importData.result.count }}
72
+ </div>
73
+ <div class="px-6 my-4">
74
+ <VirtualTable v-if="warnings.length > 0" :columns="warningColumns" :items="warnings" class="h-[200px]"></VirtualTable>
75
+ </div>
76
+
77
+ <div class="flex flex-row gap-3">
78
+ <Button class="w-full font-bold w-[160px]" @click="close">OK</Button>
79
+ <Button v-if="warnings.length > 0" variant="outline" class="w-full font-bold w-[60px]" @click="downloadWarning">
80
+ <svg width="21" height="21" viewBox="0 0 24 24" class="fill-text-300 mr-1" xmlns="http://www.w3.org/2000/svg">
81
+ <path d="M12.7519 4.99959C12.7519 4.58537 12.4161 4.24959 12.0019 4.24959C11.5877 4.24959 11.2519 4.58537 11.2519 4.99959V13.6894L8.03216 10.4697C7.73927 10.1768 7.26439 10.1768 6.9715 10.4697C6.67861 10.7626 6.67861 11.2374 6.9715 11.5303L10.7644 15.3233C11.4479 16.0067 12.5559 16.0067 13.2393 15.3233L17.0303 11.5323C17.3232 11.2394 17.3232 10.7645 17.0303 10.4716C16.7374 10.1787 16.2625 10.1787 15.9696 10.4716L12.7519 13.6893V4.99959Z" />
82
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M4 14.25C4.41421 14.25 4.75 14.5858 4.75 15V17C4.75 17.6904 5.30964 18.25 6 18.25H18C18.6904 18.25 19.25 17.6904 19.25 17V15C19.25 14.5858 19.5858 14.25 20 14.25C20.4142 14.25 20.75 14.5858 20.75 15V17C20.75 18.5188 19.5188 19.75 18 19.75H6C4.48122 19.75 3.25 18.5188 3.25 17V15C3.25 14.5858 3.58579 14.25 4 14.25Z" />
83
+ </svg>
84
+ </Button>
85
+ </div>
86
+ </div>
87
+ </div>
88
+
89
+ </Modal>
90
+ </template>
91
+
92
+ <script>
93
+
94
+ import axios from "axios";
95
+
96
+ export default{
97
+
98
+ inject: [ 'socketEmit', 'socket', 'toast' ],
99
+
100
+ props: {
101
+
102
+ accept: { type: String, default: ".csv,.xlsx" },
103
+
104
+ title: { type: String, default: "Untitled Import" },
105
+
106
+ description: { type: String, default: "Import csv or xlsx file" },
107
+
108
+ description2: { type: String, default: "Map column to fields" },
109
+
110
+ urlAnalyse: { type: String },
111
+
112
+ urlImport: { type: String },
113
+
114
+ completeDescription: { type: String, default: "" },
115
+
116
+ warningFile: { type: String, default: "warning.txt" },
117
+
118
+ templateFile: String
119
+
120
+ },
121
+
122
+ computed: {
123
+
124
+ canImport(){
125
+ return (this.importData.keys ?? []).filter((_) => _.required && !_.value).length < 1
126
+ },
127
+
128
+ warnings(){
129
+ return (this.importData.result ?? {}).warnings ?? []
130
+ },
131
+
132
+ templateFiles(){
133
+ return (this.templateFile ?? '').split(',')
134
+ }
135
+
136
+ },
137
+
138
+ data(){
139
+ return {
140
+ isOpen: false,
141
+ isLoading: false,
142
+ step: 1,
143
+ importData: null,
144
+ warningColumns: [
145
+ { key:"text", label:"Warning", width:"420px", visible:true }
146
+ ]
147
+ }
148
+ },
149
+
150
+ methods: {
151
+
152
+ onFileUploaded(){
153
+
154
+ const data = new FormData()
155
+ data.append('file', this.$refs.uploader.files[0])
156
+ data.append('filename', this.$refs.uploader.files[0].name)
157
+
158
+ this.isLoading = true
159
+ axios({
160
+ url: import.meta.env.VITE_API_HOST + this.urlAnalyse,
161
+ method: 'POST',
162
+ data
163
+ })
164
+ .then(({ data:res }) => {
165
+
166
+ this.step = 2
167
+ this.importData = res
168
+
169
+ this.importData.keys.forEach((key) => {
170
+
171
+ const labels = [
172
+ ...(key.labels ?? []),
173
+ key.text
174
+ ]
175
+
176
+ const value = res.columns.filter((_) => labels.includes(_)).pop()
177
+ if(value) key.value = value
178
+ })
179
+ })
180
+ .catch((e) => {
181
+ this.toast(e.response.data)
182
+ })
183
+ .finally(() => {
184
+ this.isLoading = false
185
+ })
186
+ },
187
+
188
+ open(){
189
+ this.reset()
190
+ this.isOpen = true
191
+ },
192
+
193
+ close(){
194
+ this.isOpen = false
195
+ this.$emit('dismiss')
196
+ },
197
+
198
+ reset(){
199
+ this.step = 1
200
+ this.importData = {}
201
+ },
202
+
203
+ proceedImport(){
204
+
205
+ this.isLoading = true
206
+ axios({
207
+ url: import.meta.env.VITE_API_HOST + this.urlImport,
208
+ method: 'POST',
209
+ data: this.importData
210
+ })
211
+ .then(({ data:res }) => {
212
+ this.importData.result = res
213
+ this.step = 3
214
+ })
215
+ .catch((e) => {
216
+ this.toast(e.response.data)
217
+ })
218
+ .finally(() => {
219
+ this.isLoading = false
220
+ })
221
+ },
222
+
223
+ downloadWarning(){
224
+ const warningText = this.importData.result.warnings.map((_) => _.text).join("\n")
225
+ this.$download("data:attachment/text," + encodeURI(warningText), this.warningFile)
226
+ }
227
+
228
+ }
229
+
230
+ }
231
+
232
+ </script>
233
+
234
+ <style module>
235
+
236
+ .comp{
237
+
238
+ }
239
+
240
+ </style>
@@ -189,7 +189,7 @@
189
189
 
190
190
  <script>
191
191
 
192
- import throttle from "lodash-es/throttle";
192
+ import throttle from "lodash/throttle";
193
193
 
194
194
  export default{
195
195
 
@@ -180,8 +180,8 @@
180
180
 
181
181
  <script>
182
182
 
183
- import throttle from "lodash-es/throttle";
184
- import {parseBoolean} from "../helper";
183
+ import throttle from "lodash/throttle";
184
+ import { parseBoolean } from "../utils/helpers.mjs";
185
185
 
186
186
  export default{
187
187
 
@@ -14,7 +14,7 @@
14
14
 
15
15
  <script>
16
16
 
17
- import {parseBoolean} from "../helper";
17
+ import { parseBoolean } from "../utils/helpers.mjs";
18
18
 
19
19
  export default{
20
20
 
@@ -8,7 +8,7 @@
8
8
 
9
9
  <script>
10
10
 
11
- import {parseBoolean, uid} from "../helper.js"
11
+ import { parseBoolean, uid } from "../utils/helpers.mjs"
12
12
 
13
13
  export default{
14
14
 
@@ -21,7 +21,7 @@
21
21
 
22
22
  <script>
23
23
 
24
- import throttle from "lodash-es/throttle";
24
+ import throttle from "lodash/throttle";
25
25
 
26
26
  export default{
27
27
 
@@ -66,7 +66,7 @@
66
66
 
67
67
  <script>
68
68
 
69
- import throttle from "lodash-es/throttle";
69
+ import throttle from "lodash/throttle";
70
70
  import dayjs from "dayjs";
71
71
 
72
72
  const _DEFAULT_COLUMN_WIDTH = '100px'
package/src/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import {defineAsyncComponent, ref} from "vue"
2
- import {observeInit} from './helper.js'
3
- import throttle from "lodash-es/throttle";
4
- //import './index.css'
2
+ import {observeInit} from './utils/helpers.mjs'
3
+ import throttle from "lodash/throttle"
5
4
 
6
5
  const calculateIsMobile = () => {
7
6
  if(typeof window === 'undefined') return false
@@ -63,6 +62,7 @@ export default{
63
62
  app.component('Image', defineAsyncComponent(() => import("./components/Image.vue")))
64
63
  app.component('ImagePreview', defineAsyncComponent(() => import("./components/ImagePreview.vue")))
65
64
  app.component('ImageFullScreen', defineAsyncComponent(() => import("./components/ImageFullScreen.vue")))
65
+ app.component('ImportModal', defineAsyncComponent(() => import("./components/ImportModal.vue")))
66
66
  app.component('ListPage1', defineAsyncComponent(() => import("./components/ListPage1.vue")))
67
67
  app.component('ListPage2', defineAsyncComponent(() => import("./components/ListPage2.vue")))
68
68
  app.component('ListPage1Filter', defineAsyncComponent(() => import("./components/ListPage1Filter.vue")))
@@ -0,0 +1,22 @@
1
+ const md5 = require("md5");
2
+ const fs = require("fs");
3
+
4
+ const saveBuffer = async(buffer, dir = '/storage/files/images') => {
5
+
6
+ const { fileTypeFromBuffer } = await import('file-type')
7
+
8
+ const type = await fileTypeFromBuffer(buffer)
9
+
10
+ const storagePath = process.env.ROOT_PATH + (dir)
11
+ const ext = type && type.ext ? '.' + type.ext : ''
12
+ const filename = md5(buffer) + ext
13
+ const to = storagePath + '/' + filename
14
+
15
+ fs.writeFileSync(to, buffer)
16
+
17
+ return filename
18
+ }
19
+
20
+ module.exports = {
21
+ saveBuffer
22
+ }
@@ -1,36 +1,36 @@
1
+
1
2
  let __uid = new Date().getTime()
2
3
 
3
4
  const uid = function(prefix){
4
5
  return (prefix ?? '') + __uid++;
5
6
  }
6
7
 
7
- const debug = function(text){
8
- console.log(text)
9
- }
8
+ const csvToArray = (str, delimiter = null) => {
10
9
 
11
- const observerPlugins = {
12
- install: (app, options) => {
13
- if('IntersectionObserver' in window) {
14
- app.config.globalProperties.$observe = observeInit()
15
- debug('observer init done')
16
- }
10
+ if(!delimiter){
11
+ delimiter = str.indexOf(';') >= 0 ? ';' : ','
17
12
  }
18
- }
19
13
 
20
- const rPrefix = function(){
14
+ str = str.trim()
15
+
16
+ const headers = str.slice(0, str.indexOf("\n")).split(delimiter);
21
17
 
22
- let size = 'sm'
23
- const windowWidth = window.innerWidth
24
- if(windowWidth >= 1536)
25
- size = '2xl';
26
- else if(windowWidth >= 1280)
27
- size = 'xl';
28
- else if(windowWidth >= 1024)
29
- size = 'lg';
30
- else if(windowWidth >= 768)
31
- size = 'md';
18
+ const rows = str.slice(str.indexOf("\n") + 1).split("\n");
32
19
 
33
- return size
20
+ const arr = rows.map(function (row) {
21
+ const values = row.split(delimiter);
22
+ let el = {}
23
+ if(values.length === headers.length){
24
+ el = headers.reduce(function (object, header, index) {
25
+ object[header] = values[index];
26
+ return object;
27
+ }, {});
28
+ }
29
+ return el;
30
+ })
31
+ .filter((_) => Object.keys(_).length > 0);
32
+
33
+ return arr;
34
34
  }
35
35
 
36
36
  const parseBoolean = function(exp){
@@ -39,11 +39,9 @@ const parseBoolean = function(exp){
39
39
  }
40
40
 
41
41
  export {
42
- observerPlugins,
43
- observeInit,
44
- debug,
45
42
  uid,
46
- rPrefix,
43
+ observeInit,
44
+ csvToArray,
47
45
  parseBoolean
48
46
  }
49
47
 
@@ -0,0 +1,151 @@
1
+ const md5 = require("md5");
2
+ const fs = require("fs");
3
+ const AdmZip = require("adm-zip");
4
+ const glob = require("glob");
5
+ const Exceljs = require("exceljs")
6
+ const { saveBuffer } = require('./helpers.js')
7
+
8
+ const analyseRequest = async(req) => {
9
+
10
+ const file = req.files[0]
11
+ const ext = file.originalname.split('.').slice(-1).pop()
12
+
13
+ let folderName = md5('motor-import-analyse' + new Date().getTime())
14
+ let xlsxFileType, xlsxFile
15
+
16
+ fs.mkdirSync(process.env.ROOT_PATH + '/storage/files/temp/' + folderName)
17
+
18
+ if(ext === 'zip'){
19
+
20
+ const zip = new AdmZip(file.buffer)
21
+ zip.extractAllTo(process.env.ROOT_PATH + '/storage/files/temp/' + folderName, true)
22
+
23
+ const res = glob.sync(process.env.ROOT_PATH + '/storage/files/temp/' + folderName + '/**/*(*.xlsx|*.csv)')
24
+
25
+ if(res.length < 1)
26
+ throw new ValidationError({ file: [ 'File excel atau csv tidak ditemukan' ]})
27
+ else if(res.length > 1)
28
+ throw new ValidationError({ file: [ 'File excel atau csv tidak melebihi 1' ]})
29
+ else{
30
+ xlsxFile = res[0]
31
+ xlsxFileType = xlsxFile.split('.').slice(-1).pop()
32
+ }
33
+ }
34
+
35
+ else{
36
+ xlsxFileType = ext
37
+ const filename = await saveBuffer(file.buffer, '/storage/files/temp/' + folderName)
38
+ xlsxFile = process.env.ROOT_PATH + '/storage/files/temp/' + folderName + '/' + filename
39
+ }
40
+
41
+ const workbook = new Exceljs.Workbook();
42
+ switch(xlsxFileType){
43
+
44
+ case 'xlsx':
45
+ await workbook.xlsx.readFile(xlsxFile)
46
+ break
47
+
48
+ case 'csv':
49
+ await workbook.csv.readFile(xlsxFile, {
50
+ parserOptions: {
51
+ delimiter: ';'
52
+ }
53
+ });
54
+ break
55
+ }
56
+
57
+ const worksheet = workbook.worksheets.length > 0 ? workbook.getWorksheet(1) : undefined
58
+ if(!worksheet)
59
+ throw new ValidationError({ worksheet:[ 'File tidak dapat dibaca' ] })
60
+
61
+ const columns = []
62
+ const header = worksheet.getRow(1)
63
+ if(!header)
64
+ throw new ValidationError({ worksheet:[ 'File tidak dapat dibaca' ] })
65
+ header.eachCell((cell) => {
66
+ columns.push(cell.value)
67
+ })
68
+
69
+ return {
70
+ columns,
71
+ folderName,
72
+ xlsxFileType,
73
+ xlsxFile
74
+ }
75
+ }
76
+
77
+ const importRequest = async(req) => {
78
+
79
+ const { columns, folderName, xlsxFileType, xlsxFile, keys } = req.body
80
+
81
+ keys.forEach((key) => {
82
+ if (key.required && !key.value)
83
+ throw new ValidationError({name: ['Kolom ' + key.text + ' harus diisi']})
84
+ })
85
+
86
+ const rows = []
87
+ const cols = {}
88
+ const workbook = new Exceljs.Workbook();
89
+ switch(xlsxFileType){
90
+
91
+ case 'xlsx':
92
+ await workbook.xlsx.readFile(xlsxFile)
93
+ break
94
+
95
+ case 'csv':
96
+ await workbook.csv.readFile(xlsxFile, {
97
+ parserOptions: {
98
+ delimiter: ';'
99
+ }
100
+ });
101
+ break
102
+ }
103
+
104
+ const worksheet = workbook.getWorksheet(1)
105
+ worksheet.eachRow((row, index) => {
106
+ if(index === 1){
107
+ row.eachCell((cell, index) => {
108
+
109
+ let mapped = false
110
+ keys.forEach((key) => {
111
+ if(key.value === cell.value){
112
+ cols[key.key] = index
113
+ mapped = true
114
+ }
115
+ })
116
+
117
+ if(!mapped){
118
+ cols[cell.value] = index
119
+ }
120
+ })
121
+ }
122
+ else{
123
+ const obj = {}
124
+
125
+ for(let key in cols){
126
+ const cell = row.getCell(cols[key])
127
+ obj[key] = cell.formulaType === 1 ? cell.result : cell.value
128
+ }
129
+
130
+ rows.push(obj)
131
+ }
132
+ })
133
+
134
+ //console.log(JSON.stringify(rows, null, 2))
135
+
136
+ const images = glob.sync(process.env.ROOT_PATH + '/storage/files/temp/' + folderName +
137
+ '/**/*(*.jpg|*.jpeg|*.png|*.webp)')
138
+
139
+ return {
140
+ rows,
141
+ images,
142
+ columns,
143
+ keys
144
+ }
145
+ }
146
+
147
+
148
+ module.exports = {
149
+ analyseRequest,
150
+ importRequest
151
+ }
package/src/helpers.mjs DELETED
@@ -1,31 +0,0 @@
1
- const csvToArray = (str, delimiter = null) => {
2
-
3
- if(!delimiter){
4
- delimiter = str.indexOf(';') >= 0 ? ';' : ','
5
- }
6
-
7
- str = str.trim()
8
-
9
- const headers = str.slice(0, str.indexOf("\n")).split(delimiter);
10
-
11
- const rows = str.slice(str.indexOf("\n") + 1).split("\n");
12
-
13
- const arr = rows.map(function (row) {
14
- const values = row.split(delimiter);
15
- let el = {}
16
- if(values.length === headers.length){
17
- el = headers.reduce(function (object, header, index) {
18
- object[header] = values[index];
19
- return object;
20
- }, {});
21
- }
22
- return el;
23
- })
24
- .filter((_) => Object.keys(_).length > 0);
25
-
26
- return arr;
27
- }
28
-
29
- export {
30
- csvToArray
31
- }