@mixd-id/web-scaffold 0.1.2301231351 → 0.1.2301231353

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.2301231351",
4
+ "version": "0.1.2301231353",
5
5
  "scripts": {
6
6
  "dev": "vite serve",
7
7
  "build": "vite build",
@@ -24,6 +24,7 @@
24
24
  "@vueuse/core": "^9.0.2",
25
25
  "adm-zip": "^0.5.10",
26
26
  "axios": "^1.3.4",
27
+ "chart.js": "^4.2.1",
27
28
  "compression": "^1.7.4",
28
29
  "cookie-parser": "^1.4.6",
29
30
  "cors": "^2.8.5",
@@ -41,13 +42,13 @@
41
42
  "serve-static": "^1.15.0",
42
43
  "tailwindcss": "^3.2.4",
43
44
  "vue": "^3.2.25",
45
+ "vue-chartjs": "^5.2.0",
44
46
  "vue-router": "^4.0.14"
45
47
  },
46
48
  "devDependencies": {
47
49
  "@vitejs/plugin-vue": "^2.2.0",
48
50
  "autoprefixer": "^10.4.4",
49
51
  "postcss": "^8.4.12",
50
- "sequelize": "^6.29.0",
51
52
  "vite": "^2.8.0"
52
53
  },
53
54
  "description": "This scaffold based on express vitejs vuejs",
@@ -30,7 +30,12 @@ export default {
30
30
  default: "auto" // bottom-left, bottom-right, top-left, top-right, bottom-center, top-center default: bottom-left
31
31
  },
32
32
 
33
- bodyClass: String
33
+ bodyClass: String,
34
+
35
+ dismiss: {
36
+ type: Boolean,
37
+ default: true
38
+ }
34
39
 
35
40
  },
36
41
 
@@ -51,7 +56,8 @@ export default {
51
56
  return {
52
57
  computedStyle: null,
53
58
  isOpen: false,
54
- context: null
59
+ context: null,
60
+ closing: false
55
61
  }
56
62
  },
57
63
 
@@ -75,39 +81,39 @@ export default {
75
81
  if(typeof window === 'undefined') return
76
82
 
77
83
  const transitionEnd = () => {
78
- this.$emit('open')
79
84
  window.addEventListener('click', this.onDismiss)
80
85
  window.addEventListener('scroll', this.onDismiss)
81
86
  this.$refs.contextMenu.removeEventListener('transitionend', transitionEnd)
87
+ this.$emit('open')
82
88
  }
83
89
 
90
+ this.$refs.contextMenu.removeEventListener('transitionend', transitionEnd)
84
91
  this.$refs.contextMenu.addEventListener('transitionend', transitionEnd)
85
- window.setTimeout(() => {
92
+ this.$nextTick(() => {
86
93
  this.$refs.contextMenu.classList.add(this.$style.active)
87
- }, 77)
94
+ })
88
95
  },
89
96
 
90
- onDismiss(){
97
+ onDismiss(e){
91
98
  if(typeof window === 'undefined') return
92
99
  if(!this.$refs.contextMenu) return
93
100
 
94
- const transitionEnd = () => {
95
- window.removeEventListener('click', this.onDismiss)
96
- window.removeEventListener('scroll', this.onDismiss)
97
- if(this.$refs.contextMenu){
98
- this.$refs.contextMenu.removeEventListener('transitionend', transitionEnd)
99
- }
100
- this.computedStyle = null
101
- this.$emit('dismiss')
102
- this.isOpen = false
103
- }
101
+ if(e.target.closest('.' + this.$style.contextMenu) && !this.dismiss)
102
+ return
104
103
 
105
- this.$refs.contextMenu.addEventListener('transitionend', transitionEnd)
106
- this.$refs.contextMenu.classList.remove(this.$style.active)
104
+ this.close()
107
105
  },
108
106
 
109
107
  open(caller, context){
110
108
 
109
+ if(this.isOpen){
110
+ return this.close(() => {
111
+ window.setTimeout(() => {
112
+ this.open(caller, context)
113
+ }, 30)
114
+ })
115
+ }
116
+
111
117
  if(caller){
112
118
  this.isOpen = true
113
119
  }
@@ -185,7 +191,23 @@ export default {
185
191
  else{
186
192
  console.warn('Invalid context menu caller', this.caller)
187
193
  }
194
+ },
188
195
 
196
+ close(fn){
197
+ const transitionEnd = () => {
198
+ window.removeEventListener('click', this.onDismiss)
199
+ window.removeEventListener('scroll', this.onDismiss)
200
+ if(this.$refs.contextMenu){
201
+ this.$refs.contextMenu.removeEventListener('transitionend', transitionEnd)
202
+ }
203
+ this.computedStyle = null
204
+ this.isOpen = false
205
+ this.$emit('dismiss')
206
+ if(fn) fn()
207
+ }
208
+ this.$refs.contextMenu.removeEventListener('transitionend', transitionEnd)
209
+ this.$refs.contextMenu.addEventListener('transitionend', transitionEnd)
210
+ this.$refs.contextMenu.classList.remove(this.$style.active)
189
211
  }
190
212
 
191
213
  }
@@ -2,17 +2,17 @@
2
2
  <Suspense @resolve="patch">
3
3
  <div :class="$style.comp">
4
4
 
5
- <div class="flex-1 flex flex-col">
5
+ <div class="flex-1 flex flex-col p-6">
6
6
 
7
- <div class="p-6 pb-0 flex flex-row gap-3 items-center">
7
+ <div class="pb-0 flex flex-row gap-3 items-center mb-6">
8
8
  <slot v-if="$slots['lp-start']" name="lp-start" :preset="preset"/>
9
9
  <div v-else class="mr-6 hidden md:block">
10
10
  <h2>{{ title }}</h2>
11
11
  </div>
12
12
  <div class="flex-1"></div>
13
- <div class="overflow-hidden mr-4 hidden md:block">
13
+ <div class="overflow-hidden hidden md:block">
14
14
  <slot v-if="$slots['lp-search']" name="lp-search" :preset="preset"/>
15
- <Textbox v-else :clearable="true" placeholder="Cari..." class="w-[200px]" v-model="preset.search"
15
+ <Textbox v-else :clearable="true" :placeholder="$t('Search...')" class="w-[200px]" v-model="preset.search"
16
16
  @clear="clearSearch" @submit="load">
17
17
  <template #start>
18
18
  <div class="px-2">
@@ -36,49 +36,100 @@
36
36
  </div>
37
37
  </div>
38
38
  </Dropdown>
39
- <button class="border-[1px] border-text-200 bg-base-50 hover:border-text-300 rounded-lg p-2"
40
- @click="openPreset">
41
- <svg width="20.5" height="20.5" viewBox="0 0 24 24" class="fill-text-300 hover:fill-primary" xmlns="http://www.w3.org/2000/svg">
42
- <path fill-rule="evenodd" clip-rule="evenodd" d="M5 13.5C5.82843 13.5 6.5 12.8284 6.5 12C6.5 11.1716 5.82843 10.5 5 10.5C4.17157 10.5 3.5 11.1716 3.5 12C3.5 12.8284 4.17157 13.5 5 13.5ZM5 15C6.65685 15 8 13.6569 8 12C8 10.3431 6.65685 9 5 9C3.34315 9 2 10.3431 2 12C2 13.6569 3.34315 15 5 15Z" />
43
- <path fill-rule="evenodd" clip-rule="evenodd" d="M12 13.5C12.8284 13.5 13.5 12.8284 13.5 12C13.5 11.1716 12.8284 10.5 12 10.5C11.1716 10.5 10.5 11.1716 10.5 12C10.5 12.8284 11.1716 13.5 12 13.5ZM12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z" />
44
- <path fill-rule="evenodd" clip-rule="evenodd" d="M19 13.5C19.8284 13.5 20.5 12.8284 20.5 12C20.5 11.1716 19.8284 10.5 19 10.5C18.1716 10.5 17.5 11.1716 17.5 12C17.5 12.8284 18.1716 13.5 19 13.5ZM19 15C20.6569 15 22 13.6569 22 12C22 10.3431 20.6569 9 19 9C17.3431 9 16 10.3431 16 12C16 13.6569 17.3431 15 19 15Z" />
45
- </svg>
39
+ <button class="border-[1px] border-text-200 bg-base-50 hover:border-text-300 rounded-lg p-3"
40
+ @click="openPreset()">
41
+ <svg width="15" height="15" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="fill-text-300 hover:fill-primary"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M64 256V160H224v96H64zm0 64H224v96H64V320zm224 96V320H448v96H288zM448 256H288V160H448v96zM64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64z"/></svg>
46
42
  </button>
47
43
  </div>
48
44
  </div>
49
45
 
50
- <VirtualScroll v-if="isMobile.value" :items="items" class="flex-1" @scroll-end="loadNext">
51
- <template #item="{ item, index }">
52
- <slot v-if="$slots.mobile" name="mobile" :item="item" :index="index"></slot>
53
- <div v-else>
54
- <div class="h-[2rem] bg-base-300 rounded-lg my-2"></div>
55
- </div>
56
- </template>
57
- </VirtualScroll>
46
+ <div class="flex-1 mb-4 flex flex-col bg-base-500 border-text-50 border-[1px]" v-if="summary">
47
+ <VirtualTable v-if="preset.summary.mode === 'table'" :items="summary.items" :columns="summary.columns" class="flex-1"></VirtualTable>
48
+
49
+ <Bar v-else-if="preset.summary.mode === 'bar'" :data="chartData" :options="chartOptions" class="flex-1 w-full p-2"/>
50
+
51
+ </div>
58
52
 
59
- <VirtualTable v-else class="flex-1 m-6" :columns="presetColumns" :items="items"
53
+ <VirtualTable v-if="!hideDetails" class="flex-1" :columns="presetColumns" :items="items"
60
54
  :appearances="appearances" @scroll-end="loadNext" :pinned="pinned">
61
55
  <template v-for="column in presetColumns" #[colOf(column.key)]="{}">
62
- <div :class="getHeader(column)">
63
- <div class="flex-1 p-2 cursor-pointer" @click="$refs[column.key][0].open($event.target)">
56
+ <div :class="getHeader(column)" @click="openColumnOptions(column.key, $event.target.closest('.' + $style.header))">
57
+ <div>
64
58
  {{ column.label }}
65
59
  </div>
66
- <div>
60
+ <div class="absolute top-0 right-0 p-2 bg-base-500" v-if="sortedColumns[column.key] === 'asc'">
61
+ <svg width="19" height="19" viewBox="0 0 24 24" class="fill-text-400" xmlns="http://www.w3.org/2000/svg">
62
+ <path d="M6.75 5C6.75 4.58579 6.41421 4.25 6 4.25C5.58579 4.25 5.25 4.58579 5.25 5V17.6893L3.53033 15.9697C3.23744 15.6768 2.76256 15.6768 2.46967 15.9697C2.17678 16.2626 2.17678 16.7374 2.46967 17.0303L4.76256 19.3232C5.44598 20.0066 6.55402 20.0066 7.23744 19.3232L9.53033 17.0303C9.82322 16.7374 9.82322 16.2626 9.53033 15.9697C9.23744 15.6768 8.76256 15.6768 8.46967 15.9697L6.75 17.6893V5Z"/>
63
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M12.25 17C12.25 17.4142 12.5858 17.75 13 17.75H21C21.4142 17.75 21.75 17.4142 21.75 17C21.75 16.5858 21.4142 16.25 21 16.25H13C12.5858 16.25 12.25 16.5858 12.25 17Z"/>
64
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M12.25 12C12.25 11.5858 12.5858 11.25 13 11.25H18C18.4142 11.25 18.75 11.5858 18.75 12C18.75 12.4142 18.4142 12.75 18 12.75H13C12.5858 12.75 12.25 12.4142 12.25 12Z"/>
65
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M12.25 7C12.25 6.58579 12.5858 6.25 13 6.25H15C15.4142 6.25 15.75 6.58579 15.75 7C15.75 7.41421 15.4142 7.75 15 7.75H13C12.5858 7.75 12.25 7.41421 12.25 7Z"/>
66
+ </svg>
67
+ </div>
68
+ <div class="absolute top-0 right-0 p-2 bg-base-500" v-else-if="sortedColumns[column.key] === 'desc'">
69
+ <svg width="19" height="19" viewBox="0 0 24 24" class="fill-text-400" xmlns="http://www.w3.org/2000/svg">
70
+ <path d="M6.75 5C6.75 4.58579 6.41421 4.25 6 4.25C5.58579 4.25 5.25 4.58579 5.25 5V17.6893L3.53033 15.9697C3.23744 15.6768 2.76256 15.6768 2.46967 15.9697C2.17678 16.2626 2.17678 16.7374 2.46967 17.0303L4.76256 19.3232C5.44598 20.0066 6.55402 20.0066 7.23744 19.3232L9.53033 17.0303C9.82322 16.7374 9.82322 16.2626 9.53033 15.9697C9.23744 15.6768 8.76256 15.6768 8.46967 15.9697L6.75 17.6893V5Z"/>
71
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M12.25 7C12.25 6.58579 12.5858 6.25 13 6.25H21C21.4142 6.25 21.75 6.58579 21.75 7C21.75 7.41421 21.4142 7.75 21 7.75H13C12.5858 7.75 12.25 7.41421 12.25 7Z"/>
72
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M12.25 12C12.25 12.4142 12.5858 12.75 13 12.75H18C18.4142 12.75 18.75 12.4142 18.75 12C18.75 11.5858 18.4142 11.25 18 11.25H13C12.5858 11.25 12.25 11.5858 12.25 12Z"/>
73
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M12.25 17C12.25 17.4142 12.5858 17.75 13 17.75H15C15.4142 17.75 15.75 17.4142 15.75 17C15.75 16.5858 15.4142 16.25 15 16.25H13C12.5858 16.25 12.25 16.5858 12.25 17Z"/>
74
+ </svg>
67
75
  </div>
68
76
  </div>
69
77
  </template>
70
- <template v-for="(_, slot) in $slots" #[slot]="{ item, index }">
78
+ <template v-for="(_, slot) in headerSlots" #[slot]="{ item, index }">
79
+ <div :class="getHeader(slot.replace('col-', ''))" @click="openColumnOptions(slot.replace('col-', ''), $event.target.closest('.' + $style.header))">
80
+ <slot :name="slot" :item="item" :index="index"></slot>
81
+ </div>
82
+ </template>
83
+ <template v-for="(_, slot) in contentSlots" #[slot]="{ item, index }">
71
84
  <slot :name="slot" :item="item" :index="index"></slot>
72
85
  </template>
73
86
  </VirtualTable>
74
87
 
75
- <ContextMenu v-for="column in presetColumns" :ref="column.key">
76
- <div class="p-4 min-w-[200px] max-w-[300px] min-h-[200px] flex flex-col gap-1">
77
- <h5>{{ column.label }}</h5>
78
- <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec nec metus nisi. Etiam faucibus tortor ut faucibus scelerisque. In vel odio mollis, facilisis arcu et, tristique dolor. Morbi sapien velit, porttitor ut neque id, facilisis tristique mi. Donec sed odio felis. Mauris id urna tempor, sagittis libero ornare, pulvinar nulla. </p>
88
+ <ContextMenu ref="columnMenu" :dismiss="false">
89
+ <div class="flex-1 flex flex-col w-[270px] p-3">
90
+ <div class="flex flex-col">
91
+ <div class="flex flex-row gap-2 items-center">
92
+ <div class="p-2 text-text-300 flex-1">Sort By</div>
93
+ <div class="text-primary cursor-pointer text-sm" @click="openPreset('sort');$refs.columnMenu.close()">Sort Options</div>
94
+ </div>
95
+ <div class="p-2 cursor-pointer" :class="$style.hoverable" @click="setSortCurrent(1)">Sort Ascending</div>
96
+ <div class="p-2 cursor-pointer" :class="$style.hoverable" @click="setSortCurrent(2)">Sort Descending</div>
97
+ </div>
98
+ <div class="h-[1px] bg-text-50 my-2"></div>
99
+ <div class="flex flex-col">
100
+ <div class="p-2 cursor-pointer" :class="$style.hoverable" @click="hide">Hide</div>
101
+ <div class="p-2 cursor-pointer" :class="$style.hoverable" @click="openPreset();$refs.columnMenu.close()">Column Options</div>
102
+ </div>
103
+ <div class="h-[1px] bg-text-50 my-2"></div>
104
+ <div class="flex flex-col">
105
+ <div class="flex flex-row gap-2 items-center">
106
+ <div class="p-2 text-text-300 flex-1">Filters</div>
107
+ <div class="text-primary cursor-pointer text-sm" @click="openPreset('filter');$refs.columnMenu.close()">Filter Options</div>
108
+ </div>
109
+ <div v-if="currentFilters.length > 0">
110
+ <ListPage1Filter v-if="preset.filters" v-for="filter in currentFilters"
111
+ :filter="filter" :column="columns[filter.key]"
112
+ @remove="removeFilter(filter)" @change="load" />
113
+ </div>
114
+ <div v-else>
115
+ <div class="p-2 cursor-pointer" :class="$style.hoverable" @click.stop="addCurrentFilter">Add Filter</div>
116
+ </div>
117
+ </div>
79
118
  </div>
80
119
  </ContextMenu>
81
120
 
121
+ <div v-if="count > 0 && !summaryTeleport" class="flex flex-row bg-base-500 divide-x divide-text-50 border-[1px] border-text-50">
122
+ <div class="p-2 px-3 text-text-400">
123
+ Count: {{ count }}
124
+ </div>
125
+ <div></div>
126
+ </div>
127
+ <Teleport v-else-if="count > 0" to=".footer-wrap">
128
+ <div>
129
+ Count: {{ count }}
130
+ </div>
131
+ </Teleport>
132
+
82
133
  </div>
83
134
 
84
135
  <div v-if="layout.presetOpen" class="w-[360px] border-l-[1px] border-text-50 bg-base-500 flex flex-col">
@@ -98,8 +149,8 @@
98
149
  :caller="layout.presetContextMenu ? layout.presetContextMenu.caller : null"
99
150
  @dismiss="layout.presetContextMenu = null" position="bottom-right">
100
151
  <div class="min-w-[200px]">
101
- <button class="w-full p-3 hover:bg-text-50" @click="copyPreset">Copy this Preset</button>
102
- <button class="w-full p-3 text-red-500 hover:text-red-700" @click="removePreset" v-if="presets.length > 1">Remove</button>
152
+ <button type="button" class="w-full p-3 hover:bg-text-50 text-left" @click="copyPreset">Copy this Preset</button>
153
+ <button type="button" class="w-full p-3 text-red-600 hover:text-red-500 text-left" @click="removePreset" v-if="presets.length > 1">Remove</button>
103
154
  </div>
104
155
  </ContextMenu>
105
156
  <button @click="closePreset">
@@ -110,7 +161,7 @@
110
161
  </div>
111
162
 
112
163
  <div class="flex justify-center">
113
- <Tabs v-model="layout.presetTab" :items="[{text:'Kolom',value:'column'},{text:'Filter',value:'filter'},{text:'Urutan',value:'sort'}]" />
164
+ <Tabs v-model="layout.presetTab" :items="tabItems" />
114
165
  </div>
115
166
  </div>
116
167
 
@@ -134,15 +185,14 @@
134
185
  </ListItem>
135
186
  </div>
136
187
 
137
- <div class="flex flex-col" v-if="layout.presetTab === 'filter'">
188
+ <div class="flex flex-col" v-else-if="layout.presetTab === 'filter'">
138
189
  <div v-if="filterableColumns.length > 0">
139
190
  <ListPage1Filter v-if="preset.filters" v-for="filter in preset.filters"
140
191
  :filter="filter" :column="columns[filter.key]"
141
192
  @remove="removeFilter(filter)" @change="load"/>
142
-
143
193
  <div class="py-8">
144
194
  <Dropdown @change="addFilter" v-model="layout.presetFilterSelector">
145
- <option value="" disabled selected>Tambah Filter</option>
195
+ <option value="" disabled selected>{{ $t('Add Filter')}}</option>
146
196
  <option v-for="column in filterableColumns" :value="column.key">{{ column.label }}</option>
147
197
  </Dropdown>
148
198
  </div>
@@ -154,13 +204,13 @@
154
204
  </div>
155
205
  </div>
156
206
 
157
- <div class="flex flex-col" v-if="layout.presetTab === 'sort'">
207
+ <div class="flex flex-col" v-else-if="layout.presetTab === 'sort'">
158
208
  <div v-if="sortableColumns.length > 0" class="my-8">
159
209
  <div v-for="sort in preset.sorts" class="py-4">
160
210
  <div class="flex flex-row items-center gap-2">
161
211
  <div class="flex-1">
162
212
  <Dropdown v-model="sort.key" @change="load">
163
- <option value="" disabled selected>Tambah Urutan</option>
213
+ <option value="" disabled selected>{{ $t('Add Sort') }}</option>
164
214
  <option v-for="column in sortableColumns" :value="column.key">{{ column.label }}</option>
165
215
  </Dropdown>
166
216
  </div>
@@ -177,12 +227,10 @@
177
227
  </button>
178
228
  </div>
179
229
  </div>
180
-
181
230
  <div v-if="preset.sorts && preset.sorts.length > 0" class="h-[1px] my-4 bg-text-100"></div>
182
-
183
231
  <div class="py-4">
184
232
  <Dropdown @change="addSort" v-model="layout.presetSortSelector">
185
- <option value="" disabled selected>Tambah Urutan</option>
233
+ <option value="" disabled selected>{{ $t('Add Sort') }}</option>
186
234
  <option v-for="column in sortableColumns" :value="column.key">{{ column.label }}</option>
187
235
  </Dropdown>
188
236
  </div>
@@ -194,6 +242,72 @@
194
242
  </div>
195
243
  </div>
196
244
 
245
+ <div class="flex flex-col gap-3 py-8" v-else-if="layout.presetTab === 'summary'">
246
+
247
+ <div class="flex flex-row items-center p-3">
248
+ <label class="text-text-400 flex-1">Enabled</label>
249
+ <Checkbox v-model="preset.summary.enabled" @change="load"/>
250
+ </div>
251
+
252
+ <div class="flex flex-row items-center p-3">
253
+ <label class="text-text-400 flex-1">Hide Details</label>
254
+ <Checkbox v-model="preset.summary.hideDetails" @change="load"/>
255
+ </div>
256
+
257
+ <div class="p-3">
258
+ <label class="text-text-400 flex-1">Row</label>
259
+ <div class="flex flex-row mt-2 gap-2">
260
+ <Dropdown v-model="preset.summary.rows[0].key" class="flex-1" @change="load">
261
+ <option value="" disabled selected>{{ $t('Add Column') }}</option>
262
+ <option v-for="column in filterableColumns" :value="column.key">{{ column.label }}</option>
263
+ </Dropdown>
264
+ <Dropdown v-model="preset.summary.rows[0].format" class="w-[100px]" @change="load">
265
+ <option value="">{{ $t('Default') }}</option>
266
+ <option value="date">{{ $t('Date') }}</option>
267
+ </Dropdown>
268
+ </div>
269
+ </div>
270
+
271
+ <div class="p-3">
272
+ <label class="text-text-400 flex-1">Column</label>
273
+ <div class="flex flex-row mt-2 gap-2">
274
+ <Dropdown v-model="preset.summary.columns[0].key" class="flex-1" @change="load">
275
+ <option value="" disabled selected>{{ $t('Add Column') }}</option>
276
+ <option v-for="column in filterableColumns" :value="column.key">{{ column.label }}</option>
277
+ </Dropdown>
278
+ <Dropdown v-model="preset.summary.columns[0].format" class="w-[100px]" @change="load">
279
+ <option value="">{{ $t('Default') }}</option>
280
+ <option value="date">{{ $t('Date') }}</option>
281
+ </Dropdown>
282
+ </div>
283
+ </div>
284
+
285
+ <div class="p-3">
286
+ <label class="text-text-400 flex-1">Values</label>
287
+ <div class="flex flex-row mt-2 gap-2">
288
+ <Dropdown v-model="preset.summary.values[0].key" class="flex-1" @change="load">
289
+ <option value="" disabled selected>{{ $t('Add Column') }}</option>
290
+ <option v-for="column in filterableColumns" :value="column.key">{{ column.label }}</option>
291
+ </Dropdown>
292
+ <Dropdown v-model="preset.summary.values[0].aggregrate" class="w-[100px]" @change="load">
293
+ <option value="">{{ $t('Default') }}</option>
294
+ <option value="count">{{ $t('Count') }}</option>
295
+ </Dropdown>
296
+ </div>
297
+ </div>
298
+
299
+ <div class="p-3">
300
+ <label class="text-text-400 flex-1">Type</label>
301
+ <div class="mt-2">
302
+ <Dropdown v-model="preset.summary.mode" class="flex-1" @change="load">
303
+ <option value="table">{{ $t('Table') }}</option>
304
+ <option value="bar">{{ $t('Bar Chart') }}</option>
305
+ </Dropdown>
306
+ </div>
307
+ </div>
308
+
309
+ </div>
310
+
197
311
  </div>
198
312
 
199
313
  <div class="px-6 py-3 flex flex-row gap-2"></div>
@@ -211,9 +325,15 @@
211
325
 
212
326
  import throttle from "lodash/throttle";
213
327
  import {urlQuery} from "../utils/helpers.mjs";
328
+ import { Bar } from 'vue-chartjs'
329
+ import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale } from 'chart.js'
330
+
331
+ ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
214
332
 
215
333
  export default{
216
334
 
335
+ components: { Bar },
336
+
217
337
  inject: [ 'socket', 'confirm', 'alert', 'socketEmit', 'toast' ],
218
338
 
219
339
  props:{
@@ -224,7 +344,9 @@ export default{
224
344
 
225
345
  title: String,
226
346
 
227
- pinned: Function
347
+ pinned: Function,
348
+
349
+ summaryTeleport: String
228
350
 
229
351
  },
230
352
 
@@ -235,8 +357,9 @@ export default{
235
357
  },
236
358
 
237
359
  presetColumns(){
360
+ if(!this.preset.columns) return []
238
361
 
239
- for(let i = 0 ; i < (this.preset.columns ?? []).length ; i++){
362
+ for(let i = 0 ; i < (this.preset.columns).length ; i++){
240
363
  const presetColumn = this.preset.columns[i]
241
364
  const column = this.columns[presetColumn.key] ?? {}
242
365
  for(let key in column){
@@ -246,7 +369,7 @@ export default{
246
369
  }
247
370
  }
248
371
 
249
- return this.preset.columns ?? []
372
+ return this.preset.columns
250
373
  },
251
374
 
252
375
  appearances(){
@@ -261,14 +384,121 @@ export default{
261
384
  return Object.values(this.columns).filter((_) => _.sortable)
262
385
  },
263
386
 
387
+ sortedColumns(){
388
+ const c = {};
389
+ (this.preset.sorts ?? []).forEach((_) => c[_.key] = _.type);
390
+ return c
391
+ },
392
+
264
393
  computedDataSource(){
265
394
  return this.datasource ?? this.model
395
+ },
396
+
397
+ headerSlots(){
398
+ const slots = {}
399
+ for(let key in this.$slots){
400
+ if(key.startsWith('col-'))
401
+ slots[key] = this.$slots[key]
402
+ }
403
+ return slots
404
+ },
405
+
406
+ contentSlots(){
407
+ const slots = {}
408
+ for(let key in this.$slots){
409
+ if(!key.startsWith('col-'))
410
+ slots[key] = this.$slots[key]
411
+ }
412
+ return slots
413
+ },
414
+
415
+ currentFilters(){
416
+ if(!this.preset.filters) return []
417
+ return this.preset.filters.filter((_) => _.key === this.selectedColumn)
418
+ },
419
+
420
+ chartOptions(){
421
+
422
+ var style = getComputedStyle(document.body)
423
+ var gridColor = style.getPropertyValue('--text-50')
424
+
425
+ return {
426
+ responsive: true,
427
+ maintainAspectRatio: false,
428
+ plugins: {
429
+ legend: true
430
+ },
431
+ scales: {
432
+ x: {
433
+ grid: {
434
+ color: `rgb(${gridColor})`
435
+ }
436
+ },
437
+ y: {
438
+ grid: {
439
+ color: `rgb(${gridColor})`
440
+ }
441
+ }
442
+ }
443
+ }
444
+ },
445
+
446
+ chartData(){
447
+
448
+ //console.log(this.summary)
449
+
450
+ const labels = []
451
+ this.summary.items.forEach((item) => {
452
+ labels.push(item.dfDate)
453
+ })
454
+
455
+ const datasets = []
456
+ this.summary.columns.forEach((column) => {
457
+ if(column.key === 'dfDate') return
458
+
459
+ const data = []
460
+ this.summary.items.forEach((item) => {
461
+ data.push(parseInt(item[column.key]))
462
+ })
463
+
464
+ const dataset = {
465
+ label: column.key,
466
+ data,
467
+ backgroundColor: this.chartOpt.backgroundColors[datasets.length % 9],
468
+ borderColor: this.chartOpt.borderColors[datasets.length % 9]
469
+ }
470
+
471
+ datasets.push(dataset)
472
+ })
473
+
474
+ //console.log(labels)
475
+ //console.log(datasets)
476
+
477
+ return {
478
+ labels,
479
+ datasets
480
+ }
481
+
482
+ /*return {
483
+ labels: [ 'January', 'February', 'March' ],
484
+ datasets: [ { data: [40, 20, 12] } ]
485
+ }*/
486
+ },
487
+
488
+ hideDetails(){
489
+ if(this.preset && this.preset.summary && this.preset.summary.hideDetails)
490
+ return this.preset.summary.hideDetails
491
+ return false;
266
492
  }
267
493
 
268
494
  },
269
495
 
270
496
  data(){
271
497
  return {
498
+ chartOpt: {
499
+ backgroundColors: [ '#5D9CEC', '#4FC1E9', '#48CFAD', '#A0D468', '#FFCE54', '#FC6E51', '#ED5565', '#AC92EC', '#EC87C0' ],
500
+ borderColors: [ '#4A89DC', '#3BAFDA', '#37BC9B', '#8CC152', '#F6BB42', '#E9573F', '#DA4453', '#967ADC', '#D770AD' ],
501
+ },
272
502
  layout: {
273
503
  presetOpen: false,
274
504
  presetTab: 'column',
@@ -276,11 +506,20 @@ export default{
276
506
  presetFilterSelector: null,
277
507
  presetSortSelector: null,
278
508
  },
509
+ tabItems: [
510
+ { text:'Columns', value:'column' },
511
+ { text:'Filters', value:'filter' },
512
+ { text:'Sorts', value:'sort' },
513
+ { text:'Summary', value:'summary' },
514
+ ],
279
515
  name: null,
280
516
  columns: [],
517
+ selectedColumn: null,
281
518
  presets: [],
282
519
  presetIdx: -1,
283
520
  items: [],
521
+ summary: null,
522
+ count: null,
284
523
  hasNext: false,
285
524
  modal: null
286
525
  }
@@ -294,6 +533,9 @@ export default{
294
533
  unmounted() {
295
534
  window.removeEventListener('focus', this.onFocus)
296
535
  this.socket.offAny(this.onHooks)
536
+
537
+ if(this.name)
538
+ this.socketEmit(this.name + '.unsubscribe', { name:this.name })
297
539
  },
298
540
 
299
541
  methods:{
@@ -303,15 +545,22 @@ export default{
303
545
  this.socketEmit(`${this.computedDataSource}.patch`, {
304
546
  ...(urlQuery().reset ? { reset:1 } : {})
305
547
  }, (res) => {
306
- this.name = res.name
307
- this.columns = res.columns
308
- this.presets = res.presets
309
- this.presetIdx = res.presetIdx
548
+ const data = res.data ? res.data : res
549
+ //console.log('patch', data)
550
+ this.name = data.name
551
+ this.columns = data.columns
552
+ this.presets = data.presets
553
+ this.presetIdx = data.presetIdx
310
554
  this.patchPresets()
311
555
  this.$nextTick(() => {
312
- this.items = res.items
313
- this.hasNext = res.hasNext
556
+ this.items = data.items
557
+ this.summary = data.summary
558
+ this.hasNext = data.hasNext
559
+ this.count = data.count
314
560
  })
561
+
562
+ if(this.name)
563
+ this.socketEmit(this.name + '.subscribe', { name:this.name })
315
564
  })
316
565
  },
317
566
 
@@ -355,12 +604,14 @@ export default{
355
604
  },
356
605
 
357
606
  load(){
358
- console.log('load')
359
607
  this.socketEmit(`${this.computedDataSource}.load`, {
360
608
  preset: this.preset
361
609
  }, (res) => {
362
- this.items = res.items
363
- this.hasNext = res.hasNext
610
+ const data = res.data ? res.data : res
611
+ this.items = data.items
612
+ this.summary = data.summary
613
+ this.hasNext = data.hasNext
614
+ this.count = data.count
364
615
  })
365
616
  },
366
617
 
@@ -397,10 +648,12 @@ export default{
397
648
 
398
649
  selectPreset(preset){
399
650
  this.presetIdx = this.presets.findIndex((_) => _ === preset)
651
+ this.load()
400
652
  },
401
653
 
402
- openPreset(){
654
+ openPreset(presetTab = 'column'){
403
655
  this.layout.presetOpen = true
656
+ this.layout.presetTab = presetTab
404
657
  },
405
658
 
406
659
  closePreset(){
@@ -486,6 +739,7 @@ export default{
486
739
  }
487
740
 
488
741
  this.preset.filters.push({
742
+ enabled: true,
489
743
  key: column.key,
490
744
  label: column.label,
491
745
  type: column.type,
@@ -496,6 +750,10 @@ export default{
496
750
  this.layout.presetFilterSelector = null
497
751
  },
498
752
 
753
+ addCurrentFilter(){
754
+ this.addFilter(this.selectedColumn)
755
+ },
756
+
499
757
  addSort(key){
500
758
 
501
759
  const column = this.preset.columns.filter((_) => _.key === key).pop()
@@ -535,10 +793,33 @@ export default{
535
793
 
536
794
  return [
537
795
  this.$style.header,
538
- (this.preset.filters ?? []).findIndex((_) => _.key === column.key) >= 0 ?
796
+ (this.preset.filters ?? []).findIndex((_) => _.key === column.key && _.enabled) >= 0 ?
539
797
  this.$style.headerSelected : ''
540
798
  ]
541
799
  .join(' ')
800
+ },
801
+
802
+ openColumnOptions(key, target){
803
+ this.selectedColumn = key
804
+ this.$refs.columnMenu.open(target)
805
+ },
806
+
807
+ setSortCurrent(sortType){
808
+ this.preset.sorts = [
809
+ {
810
+ key: this.selectedColumn,
811
+ label: this.columns[this.selectedColumn].label,
812
+ type: sortType === 2 ? 'desc' : 'asc'
813
+ }
814
+ ]
815
+ this.load()
816
+ this.$refs.columnMenu.close()
817
+ },
818
+
819
+ hide(){
820
+ const idx = this.preset.columns.findIndex((_) => _.key === this.selectedColumn)
821
+ this.preset.columns[idx].visible = false
822
+ this.$refs.columnMenu.close()
542
823
  }
543
824
 
544
825
  },
@@ -566,11 +847,18 @@ export default{
566
847
  }
567
848
 
568
849
  .header{
569
- @apply flex flex-row;
850
+ @apply p-2 cursor-pointer border-b-[2px] border-transparent overflow-hidden;
851
+ }
852
+ .header>*:first-child{
853
+ @apply text-ellipsis whitespace-nowrap overflow-x-hidden;
570
854
  }
571
855
 
572
856
  .headerSelected{
573
- @apply bg-primary-700 border-[1px] border-primary-700;
857
+ @apply border-primary;
858
+ }
859
+
860
+ .hoverable{
861
+ @apply hover:bg-primary hover:text-white;
574
862
  }
575
863
 
576
864
  </style>
@@ -1,52 +1,49 @@
1
1
  <template>
2
- <div class="py-8">
3
- <div class="flex flex-row items-center gap-2 py-2">
2
+ <div :class="$style.comp">
3
+ <div class="flex flex-row items-center gap-2">
4
+ <Checkbox v-model="filter.enabled" @change="onFilterChange(item)"/>
4
5
  <div class="flex-1">
5
- <label>{{ filter.label }}</label>
6
+ <strong class="block text-ellipsis overflow-x-hidden whitespace-nowrap">{{ filter.label }}</strong>
6
7
  </div>
7
- <button @click="removeFilter(filter)" class="text-primary">
8
+ <div class="w-[60px]">
9
+ <Dropdown v-model="filter.operand" @change="onFilterChange(item)">
10
+ <option value="">And</option>
11
+ <option value="or">Or</option>
12
+ </Dropdown>
13
+ </div>
14
+ <button @click.stop="removeFilter(filter)" class="text-primary">
8
15
  <svg width="21" height="21" class="fill-text-100 hover:fill-text-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
9
16
  <path class="secondary" fill-rule="evenodd" d="M15.78 14.36a1 1 0 0 1-1.42 1.42l-2.82-2.83-2.83 2.83a1 1 0 1 1-1.42-1.42l2.83-2.82L7.3 8.7a1 1 0 0 1 1.42-1.42l2.83 2.83 2.82-2.83a1 1 0 0 1 1.42 1.42l-2.83 2.83 2.83 2.82z"/>
10
17
  </svg>
11
18
  </button>
12
19
  </div>
13
- <div>
20
+ <div class="flex flex-col gap-3 py-3">
14
21
 
15
22
  <div v-for="(item, idx) in filter.filters">
16
23
 
17
24
  <div v-if="column && column.type === 'date'">
18
- <div class="flex flex-row items-start gap-2 my-2">
19
- <button v-if="filter.filters.length > 1" @click="filter.filters.splice(filter.filters.indexOf(item), 1)"
25
+
26
+ <div class="flex flex-row items-start gap-2">
27
+ <Dropdown v-model="item.operator" @change="onFilterChange(item)" class="flex-1 min-w-[100px]">
28
+ <option value="">Select</option>
29
+ <option value="today">Today</option>
30
+ <option value="thisWeek">This Week</option>
31
+ <option value="thisMonth">This Month</option>
32
+ <option value="lastMonth">Last Month</option>
33
+ <option value="thisMonth">This Year</option>
34
+ <option value="lastYear">Last Year</option>
35
+ <option value="between">Between</option>
36
+ </Dropdown>
37
+ <Datepicker v-if="item.operator === 'between'" v-model="item.value[0]" mode="popup" @change="onFilterChange(item)"/>
38
+ <Datepicker v-if="item.operator === 'between'" v-model="item.value[1]" mode="popup" @change="onFilterChange(item)"/>
39
+ <button v-if="filter.filters.length > 1" @click.stop="filter.filters.splice(filter.filters.indexOf(item), 1);onFilterChange(item)"
20
40
  class="py-2">
21
41
  <svg width="21" height="21" class="fill-text-100 hover:fill-text-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
22
42
  <path class="secondary" fill-rule="evenodd" d="M15.78 14.36a1 1 0 0 1-1.42 1.42l-2.82-2.83-2.83 2.83a1 1 0 1 1-1.42-1.42l2.83-2.82L7.3 8.7a1 1 0 0 1 1.42-1.42l2.83 2.83 2.82-2.83a1 1 0 0 1 1.42 1.42l-2.83 2.83 2.83 2.82z"/>
23
43
  </svg>
24
44
  </button>
25
- <div class="w-[60px]">
26
- <Dropdown v-if="idx === 0" v-model="item.operand" @change="onFilterChange(item)">
27
- <option value="">And</option>
28
- <option value="or">Or</option>
29
- </Dropdown>
30
- </div>
31
- <div class="flex-1 flex flex-col gap-2">
32
- <Dropdown v-model="item.operator" @change="onFilterChange(item)">
33
- <option value="">Select</option>
34
- <option value="today">Today</option>
35
- <option value="thisWeek">This Week</option>
36
- <option value="thisMonth">This Month</option>
37
- <option value="lastMonth">Last Month</option>
38
- <option value="thisMonth">This Year</option>
39
- <option value="lastYear">Last Year</option>
40
- <option value="between">Between</option>
41
- </Dropdown>
42
-
43
- <Datepicker v-if="item.operator === 'between'" v-model="item.value[0]" mode="popup" @change="onFilterChange(item)"/>
44
-
45
- <Datepicker v-if="item.operator === 'between'" v-model="item.value[1]" mode="popup" @change="onFilterChange(item)"/>
46
-
47
- <button v-if="idx === filter.filters.length - 1" @click="addMore" class="text-primary">Add More</button>
48
- </div>
49
45
  </div>
46
+
50
47
  </div>
51
48
 
52
49
  <div v-else-if="column && column.type === 'enum'" class="flex flex-col">
@@ -57,70 +54,57 @@
57
54
  </div>
58
55
 
59
56
  <div v-else-if="column && [ 'currency', 'number' ].includes(column.type)">
60
- <div class="flex flex-row items-start gap-2 my-2">
61
- <button v-if="filter.filters.length > 1" @click="filter.filters.splice(filter.filters.indexOf(item), 1)"
57
+
58
+ <div class="flex flex-row items-start gap-2">
59
+ <Dropdown v-model="item.operator" @change="onFilterChange(item)">
60
+ <option value="">Select</option>
61
+ <option value="=">=</option>
62
+ <option value=">">&gt;</option>
63
+ <option value=">=">&gt;=</option>
64
+ <option value="<">&lt;</option>
65
+ <option value="<=">&lt;=</option>
66
+ </Dropdown>
67
+ <Textbox class="flex-1" v-model="item.value" @keyup.enter="onFilterChange(item)"/>
68
+ <button v-if="filter.filters.length > 1" @click.stop="filter.filters.splice(filter.filters.indexOf(item), 1);onFilterChange(item)"
62
69
  class="py-2">
63
70
  <svg width="21" height="21" class="fill-text-100 hover:fill-text-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
64
71
  <path class="secondary" fill-rule="evenodd" d="M15.78 14.36a1 1 0 0 1-1.42 1.42l-2.82-2.83-2.83 2.83a1 1 0 1 1-1.42-1.42l2.83-2.82L7.3 8.7a1 1 0 0 1 1.42-1.42l2.83 2.83 2.82-2.83a1 1 0 0 1 1.42 1.42l-2.83 2.83 2.83 2.82z"/>
65
72
  </svg>
66
73
  </button>
67
- <div class="w-[60px]">
68
- <Dropdown v-if="idx === 0" v-model="item.operand" @change="onFilterChange(item)">
69
- <option value="">And</option>
70
- <option value="or">Or</option>
71
- </Dropdown>
72
- </div>
73
- <div class="flex-1 flex flex-col gap-2">
74
- <Dropdown v-model="item.operator" @change="onFilterChange(item)">
75
- <option value="">Select</option>
76
- <option value="=">=</option>
77
- <option value=">">&gt;</option>
78
- <option value=">=">&gt;=</option>
79
- <option value="<">&lt;</option>
80
- <option value="<=">&lt;=</option>
81
- </Dropdown>
82
-
83
- <Textbox v-model="item.value" />
84
-
85
- <button v-if="idx === filter.filters.length - 1" @click="addMore" class="text-primary">Add More</button>
86
- </div>
87
74
  </div>
75
+
88
76
  </div>
89
77
 
90
78
  <div v-else>
91
- <div class="flex flex-row items-start gap-2 my-2">
92
- <button v-if="filter.filters.length > 1" @click="filter.filters.splice(filter.filters.indexOf(item), 1);onFilterChange(item)"
93
- class="py-2">
79
+
80
+ <div class="flex flex-row items-start gap-2">
81
+ <Dropdown v-model="item.operator" @change="onFilterChange(item)" class="flex-1 min-w-[100px]">
82
+ <option value="">{{ $t('Select') }}</option>
83
+ <option value="startsWith">{{ $t('Starts with') }}</option>
84
+ <option value="endsWith">{{ $t('Ends with') }}</option>
85
+ <option value="contains">{{ $t('Contains') }}</option>
86
+ <option value="=">{{ $t('Equal') }}</option>
87
+ <option value="empty">{{ $t('Empty')}}</option>
88
+ <option value="notEmpty">{{ $t('Not empty')}}</option>
89
+ <option value="regex">{{ $t('Regexp')}}</option>
90
+ </Dropdown>
91
+ <Textbox v-if="![ 'empty', 'notEmpty' ].includes(item.operator)" class="w-[150px]" v-model="item.value" @keyup.enter="onFilterChange(item)"/>
92
+ <button v-if="filter.filters.length > 1" @click.stop="filter.filters.splice(filter.filters.indexOf(item), 1);onFilterChange(item)"
93
+ class="py-2">
94
94
  <svg width="21" height="21" class="fill-text-100 hover:fill-text-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
95
95
  <path class="secondary" fill-rule="evenodd" d="M15.78 14.36a1 1 0 0 1-1.42 1.42l-2.82-2.83-2.83 2.83a1 1 0 1 1-1.42-1.42l2.83-2.82L7.3 8.7a1 1 0 0 1 1.42-1.42l2.83 2.83 2.82-2.83a1 1 0 0 1 1.42 1.42l-2.83 2.83 2.83 2.82z"/>
96
96
  </svg>
97
97
  </button>
98
- <div class="w-[60px]">
99
- <Dropdown v-if="idx === 0" v-model="item.operand" @change="onFilterChange(item)">
100
- <option value="">And</option>
101
- <option value="or">Or</option>
102
- </Dropdown>
103
- </div>
104
- <div class="flex-1 flex flex-col gap-2">
105
- <Dropdown v-model="item.operator" @change="onFilterChange(item)">
106
- <option value="">Select</option>
107
- <option value="startsWith">Dimulai</option>
108
- <option value="endsWith">Diakhiri</option>
109
- <option value="contains">Berisi</option>
110
- <option value="=">=</option>
111
- </Dropdown>
112
-
113
- <Textbox v-model="item.value" @keyup.enter="onFilterChange(item)"/>
114
-
115
- <button v-if="idx === filter.filters.length - 1" @click="addMore" class="text-primary">Add More</button>
116
- </div>
117
98
  </div>
118
99
 
119
-
120
100
  </div>
121
101
 
122
102
  </div>
123
103
 
104
+ <div class="text-center">
105
+ <button @click="addMore" class="text-primary">{{ $t('Add More') }}</button>
106
+ </div>
107
+
124
108
  </div>
125
109
 
126
110
  </div>
@@ -145,14 +129,12 @@ export default{
145
129
  case 'date':
146
130
  case 'enum':
147
131
  this.filter.filters.push({
148
- operand: 'or',
149
132
  value: []
150
133
  })
151
134
  break
152
135
 
153
136
  default:
154
137
  this.filter.filters.push({
155
- operand: 'or'
156
138
  })
157
139
  break
158
140
  }
@@ -180,7 +162,7 @@ export default{
180
162
  <style module>
181
163
 
182
164
  .comp{
183
-
165
+ @apply py-4;
184
166
  }
185
167
 
186
168
  </style>
@@ -262,7 +262,10 @@ export default{
262
262
  const x2 = e.touches ? e.touches[0].clientX : e.clientX
263
263
  const d = x2 - x1
264
264
  x1 = x2
265
- this.columns[idx].width = parseInt(this.columns[idx].width ?? _DEFAULT_COLUMN_WIDTH) + d
265
+
266
+ let width = parseInt(this.columns[idx].width ?? _DEFAULT_COLUMN_WIDTH) + d
267
+ if(width < 20) width = 20
268
+ this.columns[idx].width = width
266
269
  }
267
270
 
268
271
  const onMouseUp = (e) => {
@@ -1,6 +1,7 @@
1
- const { Op } = require('sequelize')
2
- const util = require("util");
1
+ const { Op } = module.parent.require('sequelize')
2
+ const Sequelize = module.parent.require('sequelize')
3
3
  const dayjs = require("dayjs");
4
+ const util = require("util");
4
5
 
5
6
  let ListPage1 = {
6
7
 
@@ -9,7 +10,7 @@ let ListPage1 = {
9
10
  model: '',
10
11
  channel: '',
11
12
 
12
- async patch(params){
13
+ async patch(params, socket){
13
14
 
14
15
  const { reset } = params
15
16
 
@@ -80,22 +81,14 @@ let ListPage1 = {
80
81
  ]
81
82
 
82
83
  let attributes = { id:1, updatedAt:1 }
83
- let modelAttributes = this.model.getAttributes()
84
- preset.columns.forEach((column) => {
85
- if(column.visible && modelAttributes[column.key]){
86
- switch(column.key){
87
-
88
- case 'item':
89
- case 'option':
90
- // Ignored
91
- break
92
-
93
- default:
94
- attributes[column.key] = 1
95
- break
84
+ if(preset.columns){
85
+ let modelAttributes = this.model.getAttributes();
86
+ preset.columns.forEach((column) => {
87
+ if(column.visible && modelAttributes[column.key]){
88
+ attributes[column.key] = 1
96
89
  }
97
- }
98
- })
90
+ })
91
+ }
99
92
  attributes = Object.keys(attributes)
100
93
 
101
94
  if(preset.filters){
@@ -129,12 +122,21 @@ let ListPage1 = {
129
122
  }
130
123
  }
131
124
 
132
- const items = await this.model.findAll({
125
+ let attributeIncludes
126
+ if(this.getAttributeIncludes){
127
+ attributeIncludes = this.getAttributeIncludes(preset)
128
+ }
129
+
130
+ const { rows:items, count } = await this.model.findAndCountAll({
133
131
  where,
134
- attributes,
132
+ attributes: {
133
+ ...attributes,
134
+ include: attributeIncludes
135
+ },
135
136
  order,
136
137
  limit: itemsPerPage + 1,
137
138
  replacements,
139
+ include: this.getModelIncludes ? this.getModelIncludes(preset) : undefined
138
140
  })
139
141
 
140
142
  const hasNext = items.length > itemsPerPage
@@ -142,13 +144,37 @@ let ListPage1 = {
142
144
  items.splice(items.length - 1, 1)
143
145
  }
144
146
 
147
+ let summary
148
+ if(!afterItem && preset.summary && preset.summary.enabled){
149
+ summary = await this.loadSummary(preset, {
150
+ where,
151
+ replacements
152
+ })
153
+ }
154
+
145
155
  if(this.socket){
146
156
  this.socket.joinWithArgs(this.channel, { preset })
147
157
  }
148
158
 
149
159
  return {
150
160
  items,
151
- hasNext
161
+ hasNext,
162
+ count,
163
+ summary
164
+ }
165
+ },
166
+
167
+ async subscribe(params, socket){
168
+ const { name } = params
169
+ if(name){
170
+ socket.join(this.name)
171
+ }
172
+ },
173
+
174
+ async unsubscribe(params, socket){
175
+ const { name } = params
176
+ if(name){
177
+ socket.leave(this.name)
152
178
  }
153
179
  },
154
180
 
@@ -163,17 +189,20 @@ let ListPage1 = {
163
189
 
164
190
  preset.filters.forEach((filter) => {
165
191
 
192
+ if(!filter.enabled) return
193
+
166
194
  const key = filter.key
167
195
  const type = filter.type
196
+ const operand = filter.operand === 'or' ? 'or' : 'and'
168
197
  const items = filter.filters ?? []
169
- const operand = items[0] && items[0].operand && items[0].operand === 'or' && items.length > 1 ?
170
- 'or' : 'and'
171
198
 
172
199
  let whereValue = []
173
200
  switch(type){
174
201
 
175
202
  case 'date':
176
203
  items.forEach((item) => {
204
+ if(!('operator' in item) || (item.operator === 'between' && !('value' in item))) return
205
+
177
206
  const operator = item.operator
178
207
  const value = item.value
179
208
 
@@ -238,6 +267,8 @@ let ListPage1 = {
238
267
 
239
268
  case 'enum':
240
269
  items.forEach((item) => {
270
+ if(!('value' in item)) return
271
+
241
272
  const value = item.value ?? []
242
273
  whereValue = {
243
274
  ...whereValue,
@@ -249,6 +280,7 @@ let ListPage1 = {
249
280
  case 'number':
250
281
  case 'currency':
251
282
  items.forEach((item) => {
283
+ if(!('operator' in item) || !('value' in item)) return
252
284
 
253
285
  const operator = item.operator
254
286
  const value = item.value
@@ -295,6 +327,9 @@ let ListPage1 = {
295
327
 
296
328
  default:
297
329
  items.forEach((item) => {
330
+ if(!('operator' in item) ||
331
+ ([ 'startsWith', 'endsWith', 'contains', '=' ].includes(item.operator) && !'value' in item))
332
+ return
298
333
 
299
334
  const operator = item.operator
300
335
  const value = item.value
@@ -324,6 +359,33 @@ let ListPage1 = {
324
359
  [Op.eq]: value
325
360
  })
326
361
  break
362
+
363
+ case 'empty':
364
+ whereValue.push({
365
+ [Op.or]: [
366
+ { [Op.eq]: null },
367
+ { [Op.eq]: '' },
368
+ ]
369
+ })
370
+ break
371
+
372
+ case 'notEmpty':
373
+ whereValue.push({
374
+ [Op.and]: [
375
+ { [Op.ne]: null },
376
+ { [Op.ne]: '' },
377
+ ]
378
+ })
379
+ break
380
+
381
+ case 'length':
382
+ break
383
+
384
+ case 'regex':
385
+ whereValue.push({
386
+ [Op.regexp]: value
387
+ })
388
+ break
327
389
  }
328
390
  })
329
391
  break
@@ -352,6 +414,8 @@ let ListPage1 = {
352
414
  })
353
415
  }
354
416
 
417
+ //console.log(util.inspect(where, false, null, true /* enable colors */))
418
+
355
419
  /*console.log(util.inspect(preset.filters, false, null, true /!* enable colors *!/))
356
420
  console.log(where)
357
421
  console.log(replacements)*/
@@ -416,28 +480,15 @@ let ListPage1 = {
416
480
  return sortWhere
417
481
  },
418
482
 
419
- async onHooks(model, event, items, io){
483
+ async onHooks(model, event, items, socket){
420
484
 
421
485
  switch(event){
422
-
423
486
  case 'destroy':
424
487
  case 'remove':
425
- io.to(model).emit(model, event, items)
426
- break
427
-
428
488
  default:
429
- const instances = await this.model.findAll({
430
- where: {
431
- id: {
432
- [Op.in]: items.map((_) => _.id)
433
- }
434
- }
435
- })
436
- io.to(model).emit(`${model}${event}`, instances)
489
+ socket.emit(model, event, items)
437
490
  break
438
-
439
491
  }
440
-
441
492
  }
442
493
 
443
494
  }