@mixd-id/web-scaffold 0.1.230406001

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 (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/package.json +71 -0
  4. package/public/images/mixd-logo2.png +0 -0
  5. package/src/App.vue +17 -0
  6. package/src/components/Ahref.vue +34 -0
  7. package/src/components/Alert.vue +160 -0
  8. package/src/components/Button.vue +253 -0
  9. package/src/components/ButtonGroup.vue +101 -0
  10. package/src/components/Carousel.vue +293 -0
  11. package/src/components/ChatTyping.vue +69 -0
  12. package/src/components/Checkbox.vue +152 -0
  13. package/src/components/ContextMenu.vue +261 -0
  14. package/src/components/CopyToClipboard.vue +59 -0
  15. package/src/components/Countdown.vue +213 -0
  16. package/src/components/Datepicker.vue +312 -0
  17. package/src/components/Dropdown.vue +198 -0
  18. package/src/components/DynamicTemplate.vue +44 -0
  19. package/src/components/ErrorText.vue +36 -0
  20. package/src/components/Feed.vue +118 -0
  21. package/src/components/Gmaps.vue +227 -0
  22. package/src/components/Grid.vue +29 -0
  23. package/src/components/GridColumn.vue +31 -0
  24. package/src/components/HTMLEditor.vue +396 -0
  25. package/src/components/Image.vue +207 -0
  26. package/src/components/Image360.vue +140 -0
  27. package/src/components/ImageFullScreen.vue +101 -0
  28. package/src/components/ImagePreview.vue +71 -0
  29. package/src/components/ImportModal.vue +247 -0
  30. package/src/components/ListItem.vue +147 -0
  31. package/src/components/ListPage1.vue +1331 -0
  32. package/src/components/ListPage1Filter.vue +170 -0
  33. package/src/components/Modal.vue +253 -0
  34. package/src/components/OTPField.vue +126 -0
  35. package/src/components/Radio.vue +134 -0
  36. package/src/components/SearchButton.vue +57 -0
  37. package/src/components/Slider.vue +285 -0
  38. package/src/components/SplitPane.vue +129 -0
  39. package/src/components/Switch.vue +89 -0
  40. package/src/components/TabView.vue +106 -0
  41. package/src/components/TableView.vue +201 -0
  42. package/src/components/TableViewHead.vue +159 -0
  43. package/src/components/Tabs.vue +74 -0
  44. package/src/components/TextEditor.vue +85 -0
  45. package/src/components/Textarea.vue +184 -0
  46. package/src/components/Textbox.vue +200 -0
  47. package/src/components/Timepicker.vue +108 -0
  48. package/src/components/Toast.vue +93 -0
  49. package/src/components/VirtualScroll.vue +215 -0
  50. package/src/components/VirtualTable.vue +497 -0
  51. package/src/entry-client.js +27 -0
  52. package/src/entry-server.js +73 -0
  53. package/src/index.css +3 -0
  54. package/src/index.js +255 -0
  55. package/src/main.js +38 -0
  56. package/src/router.js +57 -0
  57. package/src/themes/default/index.js +200 -0
  58. package/src/utils/helpers.js +185 -0
  59. package/src/utils/helpers.mjs +197 -0
  60. package/src/utils/importer.js +156 -0
  61. package/src/utils/listpage1.js +1371 -0
  62. package/src/utils/selection.js +64 -0
@@ -0,0 +1,1331 @@
1
+ <template>
2
+ <div :class="$style.comp">
3
+
4
+ <div class="flex-1 flex flex-col p-6 relative">
5
+
6
+ <div class="pb-0 flex flex-row gap-3 items-center mb-6">
7
+ <slot v-if="$slots['lp-start']" name="lp-start" :preset="preset"/>
8
+ <div v-else class="mr-6 hidden md:block">
9
+ <h2>{{ computedTitle }}</h2>
10
+ </div>
11
+ <div class="flex-1"></div>
12
+ <div class="overflow-hidden hidden md:block">
13
+ <slot v-if="$slots['lp-search']" name="lp-search" :preset="preset"/>
14
+ <Textbox v-else :clearable="true" :placeholder="$t('Search...')" class="w-[200px]" v-model="preset.search"
15
+ @clear="clearSearch" @submit="load">
16
+ <template #start>
17
+ <div class="px-2">
18
+ <svg width="16" height="16" class="fill-text-300" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
19
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M17.8723 16.8116C19.1996 15.2436 20 13.2153 20 11C20 6.02944 15.9706 2 11 2C6.02944 2 2 6.02944 2 11C2 15.9706 6.02944 20 11 20C13.2153 20 15.2436 19.1996 16.8116 17.8723L19.4697 20.5303C19.7626 20.8232 20.2374 20.8232 20.5303 20.5303C20.8232 20.2374 20.8232 19.7626 20.5303 19.4697L17.8723 16.8116ZM18.5 11C18.5 15.1421 15.1421 18.5 11 18.5C6.85786 18.5 3.5 15.1421 3.5 11C3.5 6.85786 6.85786 3.5 11 3.5C15.1421 3.5 18.5 6.85786 18.5 11Z"/>
20
+ </svg>
21
+ </div>
22
+ </template>
23
+ </Textbox>
24
+ </div>
25
+ <div class="hidden md:block">
26
+ <slot name="lp-tabspace" :preset="preset"/>
27
+ </div>
28
+ <div class="flex flex-row gap-1 items-center">
29
+ <Dropdown class="w-[150px]" :value="preset.name" mode="custom" position="bottom-right">
30
+ <div class="min-w-[240px] divide-y divide-text-50 p-2 flex flex-col gap-2">
31
+ <div v-for="(_preset, idx) in configs.presets" class="cursor-pointer flex flex-row hover:bg-primary-100 rounded-md">
32
+ <div class="flex-1 p-3" @click="selectPreset(_preset)">
33
+ {{ _preset.name }}
34
+ </div>
35
+ </div>
36
+ </div>
37
+ </Dropdown>
38
+ <button class="border-[1px] border-text-200 bg-base-50 hover:border-text-300 rounded-lg p-3"
39
+ @click="openPreset(presetIdx)">
40
+ <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>
41
+ </button>
42
+ </div>
43
+ </div>
44
+
45
+ <div class="flex-1 mb-4 flex flex-col bg-base-500 border-text-50 border-[1px] relative" v-if="presetSummary && !isLoading">
46
+ <div v-if="isSummaryLoading" :class="$style.overlay">
47
+ <svg class="animate-spin aspect-square w-[48px] h-[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>
48
+ </div>
49
+ <div v-else-if="summary" class="flex-1 flex flex-col">
50
+ <VirtualTable v-if="presetSummary.mode === 'table'" :items="summaryItems" :columns="summaryColumns" class="flex-1"></VirtualTable>
51
+ <Bar v-else-if="presetSummary.mode === 'bar'" :data="chartData" :options="chartOptions" class="flex-1 w-full p-2"/>
52
+ <Line v-else-if="presetSummary.mode === 'line'" :data="chartData" :options="chartOptions" class="flex-1 w-full p-2"/>
53
+ <Gmaps v-else-if="presetSummary.mode === 'map'" :data="summary.items" :config="presetSummary.map"
54
+ class="flex-1 w-full p-2" :apiKey="mapApiKey" />
55
+ </div>
56
+ <div v-else>
57
+ <label>No data</label>
58
+ </div>
59
+ </div>
60
+
61
+ <div class="flex-1 relative flex" v-if="!hideDetails">
62
+ <VirtualTable v-if="!hideDetails" class="flex-1" :columns="presetColumns" :items="items"
63
+ :state="isLoading ? 2 : 1"
64
+ :appearances="presetAppearances" @scroll-end="loadNext" :pinned="pinned">
65
+ <template v-for="column in presetColumns" #[colOf(column.key)]="{}">
66
+ <div :class="getHeader(column)" @click="openColumnOptions(column.key, $event.target.closest('.' + $style.header))">
67
+ <div>
68
+ {{ column.label }}
69
+ </div>
70
+ <div class="absolute top-0 right-0 p-2 bg-base-500" v-if="presetSortedColumns[column.key] === 'asc'">
71
+ <svg width="19" height="19" viewBox="0 0 24 24" class="fill-text-400" xmlns="http://www.w3.org/2000/svg">
72
+ <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"/>
73
+ <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"/>
74
+ <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"/>
75
+ <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"/>
76
+ </svg>
77
+ </div>
78
+ <div class="absolute top-0 right-0 p-2 bg-base-500" v-else-if="presetSortedColumns[column.key] === 'desc'">
79
+ <svg width="19" height="19" viewBox="0 0 24 24" class="fill-text-400" xmlns="http://www.w3.org/2000/svg">
80
+ <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"/>
81
+ <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"/>
82
+ <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"/>
83
+ <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"/>
84
+ </svg>
85
+ </div>
86
+ </div>
87
+ </template>
88
+ <template v-for="(_, slot) in headerSlots" #[slot]="{ item, index }">
89
+ <div :class="getHeader(slot.replace('col-', ''))" @click="openColumnOptions(slot.replace('col-', ''), $event.target.closest('.' + $style.header))">
90
+ <slot :name="slot" :item="item" :index="index"></slot>
91
+ </div>
92
+ </template>
93
+ <template v-for="(_, slot) in contentSlots" #[slot]="{ item, index }">
94
+ <slot :name="slot" :item="item" :index="index"></slot>
95
+ </template>
96
+ </VirtualTable>
97
+ <div v-if="isLoading" :class="$style.overlay">
98
+ <svg class="animate-spin aspect-square w-[48px] h-[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>
99
+ </div>
100
+ </div>
101
+
102
+ <ContextMenu ref="columnMenu" :dismiss="false">
103
+ <div class="flex-1 flex flex-col w-[270px] p-3">
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">Sort By</div>
107
+ <div class="text-primary cursor-pointer text-sm" @click="openPreset('sort');$refs.columnMenu.close()">Sort Options</div>
108
+ </div>
109
+ <div class="p-2 cursor-pointer" :class="$style.hoverable" @click="setSortCurrent(1)">Sort Ascending</div>
110
+ <div class="p-2 cursor-pointer" :class="$style.hoverable" @click="setSortCurrent(2)">Sort Descending</div>
111
+ </div>
112
+ <div class="h-[1px] bg-text-50 my-2"></div>
113
+ <div class="flex flex-col">
114
+ <div class="p-2 cursor-pointer" :class="$style.hoverable" @click="hide">Hide</div>
115
+ <div class="p-2 cursor-pointer" :class="$style.hoverable" @click="openPreset();$refs.columnMenu.close()">Column Options</div>
116
+ </div>
117
+ <div class="h-[1px] bg-text-50 my-2"></div>
118
+ <div class="flex flex-col">
119
+ <div class="flex flex-row gap-2 items-center">
120
+ <div class="p-2 text-text-300 flex-1">Filters</div>
121
+ <div class="text-primary cursor-pointer text-sm" @click="openPreset('filter');$refs.columnMenu.close()">Filter Options</div>
122
+ </div>
123
+ <div v-if="presetCurrentFilters.length > 0">
124
+ <ListPage1Filter v-if="preset.filters" v-for="filter in presetCurrentFilters"
125
+ :filter="filter" :column="configs.columns[filter.key]"
126
+ @remove="removeFilter(filter)" @change="load" />
127
+ </div>
128
+ <div v-else>
129
+ <div class="p-2 cursor-pointer" :class="$style.hoverable" @click.stop="addCurrentFilter">Add Filter</div>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ </ContextMenu>
134
+
135
+ <div v-if="count > 0 && !summaryTeleport" class="flex flex-row bg-base-500 divide-x divide-text-50 border-[1px] border-text-50">
136
+ <div class="p-2 px-3 text-text-400">
137
+ Count: {{ count }}
138
+ </div>
139
+ <div></div>
140
+ </div>
141
+ <Teleport v-else-if="count > 0" to=".footer-wrap">
142
+ <div>
143
+ Count: {{ count }}
144
+ </div>
145
+ </Teleport>
146
+
147
+ </div>
148
+
149
+ <div v-if="configs.presetOpen" class="w-[360px] border-l-[1px] border-text-50 bg-base-500 flex flex-col">
150
+
151
+ <div v-if="configs.presetOpenIdx === -1" class="flex-1 flex flex-col">
152
+
153
+ <div class="p-6">
154
+ <h3>Presets</h3>
155
+ <br />
156
+ </div>
157
+
158
+ <div class="flex-1 overflow-y-auto border-t-[1px] border-text-50">
159
+ <div v-for="(preset, idx) in configs.presets"
160
+ class="px-6 py-3 flex flex-row gap-2 items-center border-b-[1px] border-text-50 hover:bg-text-50 cursor-pointer">
161
+ <div class="px-2">
162
+ <Checkbox :checked="idx === configs.presetIdx" @change="selectPreset(preset)" />
163
+ </div>
164
+ <div @click="configs.presetOpenIdx = idx" class="flex-1">
165
+ <label>{{ preset.name }}</label>
166
+ </div>
167
+ <button class="px-2" v-if="configs.presets.length > 1" @click="removePreset(idx)">
168
+ <svg width="16" height="16" viewBox="0 0 24 24" class="fill-text-300 hover:fill-red-500" xmlns="http://www.w3.org/2000/svg">
169
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M10 10.25C10.4142 10.25 10.75 10.5858 10.75 11V16C10.75 16.4142 10.4142 16.75 10 16.75C9.58579 16.75 9.25 16.4142 9.25 16V11C9.25 10.5858 9.58579 10.25 10 10.25Z"/>
170
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M14 10.25C14.4142 10.25 14.75 10.5858 14.75 11V16C14.75 16.4142 14.4142 16.75 14 16.75C13.5858 16.75 13.25 16.4142 13.25 16V11C13.25 10.5858 13.5858 10.25 14 10.25Z"/>
171
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M10 2.25C8.48122 2.25 7.25 3.48122 7.25 5V5.25H3C2.58579 5.25 2.25 5.58579 2.25 6C2.25 6.41421 2.58579 6.75 3 6.75H4.25V19C4.25 20.5188 5.48122 21.75 7 21.75H17C18.5188 21.75 19.75 20.5188 19.75 19V6.75H21C21.4142 6.75 21.75 6.41421 21.75 6C21.75 5.58579 21.4142 5.25 21 5.25H16.75V5C16.75 3.48122 15.5188 2.25 14 2.25H10ZM15.25 5.25V5C15.25 4.30964 14.6904 3.75 14 3.75H10C9.30964 3.75 8.75 4.30964 8.75 5V5.25H15.25ZM5.75 6.75V19C5.75 19.6904 6.30964 20.25 7 20.25H17C17.6904 20.25 18.25 19.6904 18.25 19V6.75H5.75Z"/>
172
+ </svg>
173
+ </button>
174
+ </div>
175
+ <div class="p-5 text-center">
176
+ <button @click="addPreset">
177
+ <svg width="21" height="21" viewBox="0 0 24 24" class="fill-text-300 hover:fill-primary" xmlns="http://www.w3.org/2000/svg">
178
+ <path d="M12.75 5C12.75 4.58579 12.4142 4.25 12 4.25C11.5858 4.25 11.25 4.58579 11.25 5V11.25H5C4.58579 11.25 4.25 11.5858 4.25 12C4.25 12.4142 4.58579 12.75 5 12.75H11.25V19C11.25 19.4142 11.5858 19.75 12 19.75C12.4142 19.75 12.75 19.4142 12.75 19V12.75H19C19.4142 12.75 19.75 12.4142 19.75 12C19.75 11.5858 19.4142 11.25 19 11.25H12.75V5Z"/>
179
+ </svg>
180
+ </button>
181
+ </div>
182
+ </div>
183
+
184
+ </div>
185
+
186
+ <div v-else class="flex-1 flex flex-col">
187
+ <div class="p-6 pb-0">
188
+ <div class="flex flex-row gap-4 mb-3">
189
+ <button @click="configs.presetOpenIdx = -1">
190
+ <svg width="19" height="19" viewBox="0 0 24 24" class="fill-text-300 hover:fill-text-400" xmlns="http://www.w3.org/2000/svg">
191
+ <path d="M10.5303 17.9697C10.8232 18.2626 10.8232 18.7374 10.5303 19.0303C10.2374 19.3232 9.76253 19.3232 9.46964 19.0303L3.67675 13.2374C2.99333 12.554 2.99333 11.446 3.67675 10.7626L9.46964 4.96967C9.76253 4.67678 10.2374 4.67678 10.5303 4.96967C10.8232 5.26256 10.8232 5.73744 10.5303 6.03033L5.31063 11.25H20C20.4142 11.25 20.75 11.5858 20.75 12C20.75 12.4142 20.4142 12.75 20 12.75H5.31063L10.5303 17.9697Z"/>
192
+ </svg>
193
+ </button>
194
+ <input class="flex-1 text-xl bg-transparent outline-none" :value="preset.name" @blur="preset.name = $event.target.value"
195
+ @keyup.enter="preset.name = $event.target.value" />
196
+ <button @click="closePreset">
197
+ <svg width="21" height="21" viewBox="0 0 24 24" class="fill-text-300 hover:fill-red-500" xmlns="http://www.w3.org/2000/svg">
198
+ <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"/>
199
+ </svg>
200
+ </button>
201
+ </div>
202
+
203
+ <div class="flex justify-center">
204
+ <Tabs v-model="configs.presetTab" :items="tabItems" @change="tabChanged"/>
205
+ </div>
206
+ </div>
207
+
208
+ <div class="flex-1 overflow-y-auto px-6 mb-6">
209
+
210
+ <div class="p-2 mt-6 flex flex-col" v-if="configs.presetTab === 'column'">
211
+ <ListItem :items="presetColumns" @reorder="reorderColumns">
212
+ <template v-slot="{ item }">
213
+ <div class="flex flex-row items-center gap-2" :key="item">
214
+ <div class="cursor-move" data-reorder>
215
+ <svg class="fill-text-200" width="21" height="21" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
216
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M4 16C4 15.4477 4.44772 15 5 15H19C19.5523 15 20 15.4477 20 16C20 16.5523 19.5523 17 19 17H5C4.44772 17 4 16.5523 4 16Z"/>
217
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M4 8C4 7.44772 4.44772 7 5 7H19C19.5523 7 20 7.44772 20 8C20 8.55228 19.5523 9 19 9H5C4.44772 9 4 8.55228 4 8Z"/>
218
+ </svg>
219
+ </div>
220
+ <Checkbox v-model="item.visible" :true-value="true" :false-value="false" @change="load">
221
+ {{ item.label ?? '(No Name)' }}
222
+ </Checkbox>
223
+ </div>
224
+ </template>
225
+ </ListItem>
226
+ </div>
227
+
228
+ <div class="flex flex-col" v-else-if="configs.presetTab === 'filter'">
229
+ <div v-if="filterableColumns.length > 0">
230
+ <ListPage1Filter v-if="preset.filters" v-for="filter in preset.filters"
231
+ :filter="filter" :column="configs.columns[filter.key]"
232
+ @remove="removeFilter(filter)" @change="load"/>
233
+ <div class="py-8">
234
+ <Dropdown @change="addFilter" v-model="presetFilterSelector">
235
+ <option value="" disabled selected>{{ $t('Add Filter')}}</option>
236
+ <option v-for="column in filterableColumns" :value="column.key">{{ column.label }}</option>
237
+ </Dropdown>
238
+ </div>
239
+ </div>
240
+ <div v-else>
241
+ <div class="p-6 text-center">
242
+ <label>Filter not available</label>
243
+ </div>
244
+ </div>
245
+ </div>
246
+
247
+ <div class="flex flex-col" v-else-if="configs.presetTab === 'sort'">
248
+ <div v-if="sortableColumns.length > 0" class="my-8">
249
+ <div v-for="sort in preset.sorts" class="py-4">
250
+ <div class="flex flex-row items-center gap-2">
251
+ <div class="flex-1">
252
+ <Dropdown v-model="sort.key" @change="load">
253
+ <option value="" disabled selected>{{ $t('Add Sort') }}</option>
254
+ <option v-for="column in sortableColumns" :value="column.key">{{ column.label }}</option>
255
+ </Dropdown>
256
+ </div>
257
+ <div>
258
+ <Dropdown v-model="sort.type" @change="load" class="w-[80px]">
259
+ <option value="">Asc</option>
260
+ <option value="desc">Desc</option>
261
+ </Dropdown>
262
+ </div>
263
+ <button @click="removeSort(sort)">
264
+ <svg width="21" height="21" class="fill-text-100 hover:fill-red-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
265
+ <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"/>
266
+ </svg>
267
+ </button>
268
+ </div>
269
+ </div>
270
+ <div v-if="preset.sorts && preset.sorts.length > 0" class="h-[1px] my-4 bg-text-100"></div>
271
+ <div class="py-4">
272
+ <Dropdown @change="addSort" v-model="presetSortSelector">
273
+ <option value="" disabled selected>{{ $t('Add Sort') }}</option>
274
+ <option v-for="column in sortableColumns" :value="column.key">{{ column.label }}</option>
275
+ </Dropdown>
276
+ </div>
277
+ </div>
278
+ <div v-else>
279
+ <div class="p-6 text-center">
280
+ <label>Sort not available</label>
281
+ </div>
282
+ </div>
283
+ </div>
284
+
285
+ <div class="flex flex-col gap-3 py-8" v-else-if="configs.presetTab === 'summary'">
286
+
287
+ <div v-if="configs.summaryOpenIdx === -1">
288
+ <div v-for="(summary, idx) in preset.summaries">
289
+ <div class="flex flex-row items-center gap-3 border-text-50 border-[1px] p-2 rounded-lg">
290
+ <div class="px-2">
291
+ <Checkbox v-model="summary.enabled" @change="enableSummary(idx)" />
292
+ </div>
293
+ <div class="flex-1" @click="configs.summaryOpenIdx = idx">
294
+ <label>{{ summary.title }}</label>
295
+ </div>
296
+ <button class="px-2" v-if="preset.summaries.length > 1" @click="removeSummary(summary)">
297
+ <svg width="16" height="16" viewBox="0 0 24 24" class="fill-text-300 hover:fill-red-500" xmlns="http://www.w3.org/2000/svg">
298
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M10 10.25C10.4142 10.25 10.75 10.5858 10.75 11V16C10.75 16.4142 10.4142 16.75 10 16.75C9.58579 16.75 9.25 16.4142 9.25 16V11C9.25 10.5858 9.58579 10.25 10 10.25Z"/>
299
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M14 10.25C14.4142 10.25 14.75 10.5858 14.75 11V16C14.75 16.4142 14.4142 16.75 14 16.75C13.5858 16.75 13.25 16.4142 13.25 16V11C13.25 10.5858 13.5858 10.25 14 10.25Z"/>
300
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M10 2.25C8.48122 2.25 7.25 3.48122 7.25 5V5.25H3C2.58579 5.25 2.25 5.58579 2.25 6C2.25 6.41421 2.58579 6.75 3 6.75H4.25V19C4.25 20.5188 5.48122 21.75 7 21.75H17C18.5188 21.75 19.75 20.5188 19.75 19V6.75H21C21.4142 6.75 21.75 6.41421 21.75 6C21.75 5.58579 21.4142 5.25 21 5.25H16.75V5C16.75 3.48122 15.5188 2.25 14 2.25H10ZM15.25 5.25V5C15.25 4.30964 14.6904 3.75 14 3.75H10C9.30964 3.75 8.75 4.30964 8.75 5V5.25H15.25ZM5.75 6.75V19C5.75 19.6904 6.30964 20.25 7 20.25H17C17.6904 20.25 18.25 19.6904 18.25 19V6.75H5.75Z"/>
301
+ </svg>
302
+ </button>
303
+ </div>
304
+ </div>
305
+ <div class="p-2 text-center">
306
+ <button @click="addSummary">
307
+ <svg width="21" height="21" viewBox="0 0 24 24" class="fill-text-300 hover:fill-primary" xmlns="http://www.w3.org/2000/svg">
308
+ <path d="M12.75 5C12.75 4.58579 12.4142 4.25 12 4.25C11.5858 4.25 11.25 4.58579 11.25 5V11.25H5C4.58579 11.25 4.25 11.5858 4.25 12C4.25 12.4142 4.58579 12.75 5 12.75H11.25V19C11.25 19.4142 11.5858 19.75 12 19.75C12.4142 19.75 12.75 19.4142 12.75 19V12.75H19C19.4142 12.75 19.75 12.4142 19.75 12C19.75 11.5858 19.4142 11.25 19 11.25H12.75V5Z"/>
309
+ </svg>
310
+ </button>
311
+ </div>
312
+ </div>
313
+
314
+ <div v-if="configs.summaryOpenIdx > -1">
315
+
316
+ <div class="flex flex-row items-center p-3 py-1">
317
+ <label class="text-text-400 flex-1">Enabled</label>
318
+ <Checkbox v-model="openedPresetSummary.enabled" @change="loadSummary"/>
319
+ </div>
320
+
321
+ <div class="h-[1px] bg-text-50 my-1"></div>
322
+
323
+ <div class="flex flex-row items-center p-3 py-1">
324
+ <label class="text-text-400 flex-1">{{ $t('Title') }}</label>
325
+ <Textbox v-model="openedPresetSummary.title" class="mt-1" />
326
+ </div>
327
+
328
+ <div class="h-[1px] bg-text-50 my-1"></div>
329
+
330
+ <div class="flex flex-row items-center p-3 py-1">
331
+ <label class="text-text-400 flex-1">Hide Details</label>
332
+ <Checkbox v-model="openedPresetSummary.hideDetails" @change="loadSummary"/>
333
+ </div>
334
+
335
+ <div class="h-[1px] bg-text-50 my-1"></div>
336
+
337
+ <div class="p-3">
338
+ <label class="text-text-400 flex-1">Type</label>
339
+ <div class="mt-2">
340
+ <Dropdown v-model="openedPresetSummary.mode" class="flex-1" @change="loadSummary">
341
+ <option value="table">{{ $t('Table') }}</option>
342
+ <option value="bar">{{ $t('Bar Chart') }}</option>
343
+ <option value="line">{{ $t('Line Chart') }}</option>
344
+ <option value="map">{{ $t('Maps') }}</option>
345
+ </Dropdown>
346
+ </div>
347
+ <div v-if="[ 'bar', 'line' ].includes(openedPresetSummary.mode)" class="mt-1">
348
+ <Checkbox v-model="openedPresetSummary.hideLegends" @change="loadSummary">Hide Legend</Checkbox>
349
+ </div>
350
+ </div>
351
+
352
+ <div v-if="openedPresetSummary.mode === 'table'">
353
+
354
+ <div class="p-3">
355
+ <label class="text-text-400 flex-1">{{ $t('Row') }}</label>
356
+ <div class="flex flex-row mt-2 gap-2">
357
+ <Dropdown v-model="openedPresetSummary.table.rows[0].key" class="flex-1" @change="loadSummary">
358
+ <option value="" disabled selected>{{ $t('Add Column') }}</option>
359
+ <option v-for="column in filterableColumns" :value="column.key">{{ column.label }}</option>
360
+ </Dropdown>
361
+ <Dropdown v-model="openedPresetSummary.table.rows[0].format" class="w-[100px]" @change="loadSummary">
362
+ <option value="">{{ $t('Default') }}</option>
363
+ <option value="date">{{ $t('Date') }}</option>
364
+ <option value="month">{{ $t('Month') }}</option>
365
+ <option value="quarter">{{ $t('Quarterly') }}</option>
366
+ <option value="year">{{ $t('Year') }}</option>
367
+ </Dropdown>
368
+ </div>
369
+ <Checkbox class="mt-2" v-model="openedPresetSummary.table.showRowTotal">
370
+ Row Total
371
+ </Checkbox>
372
+ </div>
373
+
374
+ <div class="p-3">
375
+ <label class="text-text-400 flex-1">{{$t('Column') }}</label>
376
+ <div>
377
+ <Dropdown v-model="openedPresetSummary.table.columns[0].key" class="flex-1" @change="loadSummary">
378
+ <option value="" disabled selected>{{ $t('Add Column') }}</option>
379
+ <option value="(none)">{{ $t('(None)') }}</option>
380
+ <option v-for="column in filterableColumns" :value="column.key">{{ column.label }}</option>
381
+ </Dropdown>
382
+ </div>
383
+ <Checkbox class="mt-2" v-model="openedPresetSummary.table.showColumnTotal">
384
+ Column Total
385
+ </Checkbox>
386
+ </div>
387
+
388
+ <div class="p-3">
389
+ <label class="text-text-400 flex-1">Values</label>
390
+ <div class="flex flex-row mt-2 gap-2">
391
+ <Dropdown v-model="openedPresetSummary.table.values[0].aggregrate" class="flex-1" @change="loadSummary">
392
+ <option value="" disabled selected>{{ $t('Select') }}</option>
393
+ <option value="count">{{ $t('Count') }}</option>
394
+ <option value="max">{{ $t('Max') }}</option>
395
+ <option value="min">{{ $t('Min') }}</option>
396
+ <option value="avg">{{ $t('Avg') }}</option>
397
+ </Dropdown>
398
+ </div>
399
+ </div>
400
+
401
+ </div>
402
+
403
+ <div v-else-if="openedPresetSummary.mode === 'bar'">
404
+
405
+ <div class="p-3">
406
+ <label class="text-text-400 flex-1">{{ $t('X-axis') }}</label>
407
+ <div class="flex flex-row mt-2 gap-2">
408
+ <Dropdown v-model="openedPresetSummary.bar.rows[0].key" class="flex-1" @change="loadSummary">
409
+ <option value="" disabled selected>{{ $t('Add Column') }}</option>
410
+ <option v-for="column in filterableColumns" :value="column.key">{{ column.label }}</option>
411
+ </Dropdown>
412
+ <Dropdown v-model="openedPresetSummary.bar.rows[0].format" class="w-[100px]" @change="loadSummary">
413
+ <option value="">{{ $t('Default') }}</option>
414
+ <option value="date">{{ $t('Date') }}</option>
415
+ <option value="month">{{ $t('Month') }}</option>
416
+ <option value="quarter">{{ $t('Quarterly') }}</option>
417
+ <option value="year">{{ $t('Year') }}</option>
418
+ </Dropdown>
419
+ </div>
420
+ </div>
421
+
422
+ <div class="p-3">
423
+ <label class="text-text-400 flex-1">{{ $t('Column') }}</label>
424
+ <div class="flex flex-row mt-2 gap-2">
425
+ <Dropdown v-model="openedPresetSummary.bar.columns[0].key" class="flex-1" @change="loadSummary">
426
+ <option value="" disabled selected>{{ $t('Add Column') }}</option>
427
+ <option value="(none)">{{ $t('None') }}</option>
428
+ <option v-for="column in filterableColumns" :value="column.key">{{ column.label }}</option>
429
+ </Dropdown>
430
+ <Dropdown v-model="openedPresetSummary.bar.columns[0].format" class="w-[100px]" @change="loadSummary">
431
+ <option value="">{{ $t('Default') }}</option>
432
+ <option value="date">{{ $t('Date') }}</option>
433
+ </Dropdown>
434
+ </div>
435
+ <Checkbox class="mt-2" v-model="openedPresetSummary.bar.stacked">
436
+ Stacked
437
+ </Checkbox>
438
+ </div>
439
+
440
+ <div class="p-3">
441
+ <label class="text-text-400 flex-1">Values</label>
442
+ <div class="flex flex-row mt-2 gap-2">
443
+ <Dropdown v-model="openedPresetSummary.bar.values[0].aggregrate" class="flex-1" @change="loadSummary">
444
+ <option value="" disabled selected>{{ $t('Select') }}</option>
445
+ <option value="count">{{ $t('Count') }}</option>
446
+ <option value="max">{{ $t('Max') }}</option>
447
+ <option value="min">{{ $t('Min') }}</option>
448
+ <option value="avg">{{ $t('Avg') }}</option>
449
+ </Dropdown>
450
+ </div>
451
+ </div>
452
+
453
+ </div>
454
+
455
+ <div v-else-if="openedPresetSummary.mode === 'line'">
456
+
457
+ <div class="p-3">
458
+ <label class="text-text-400 flex-1">{{ $t('X-axis') }}</label>
459
+ <div class="flex flex-row mt-2 gap-2">
460
+ <Dropdown v-model="openedPresetSummary.line.rows[0].key" class="flex-1" @change="loadSummary">
461
+ <option value="" disabled selected>{{ $t('Add Column') }}</option>
462
+ <option v-for="column in filterableColumns" :value="column.key">{{ column.label }}</option>
463
+ </Dropdown>
464
+ <Dropdown v-model="openedPresetSummary.line.rows[0].format" class="w-[100px]" @change="loadSummary">
465
+ <option value="">{{ $t('Default') }}</option>
466
+ <option value="date">{{ $t('Date') }}</option>
467
+ <option value="month">{{ $t('Month') }}</option>
468
+ <option value="quarter">{{ $t('Quarterly') }}</option>
469
+ <option value="year">{{ $t('Year') }}</option>
470
+ </Dropdown>
471
+ </div>
472
+ </div>
473
+
474
+ <div class="p-3">
475
+ <label class="text-text-400 flex-1">{{$t('Column') }}</label>
476
+ <div class="flex flex-row mt-2 gap-2">
477
+ <Dropdown v-model="openedPresetSummary.line.columns[0].key" class="flex-1" @change="loadSummary">
478
+ <option value="" disabled selected>{{ $t('Add Column') }}</option>
479
+ <option value="(none)">{{ $t('None') }}</option>
480
+ <option v-for="column in filterableColumns" :value="column.key">{{ column.label }}</option>
481
+ </Dropdown>
482
+ <Dropdown v-model="openedPresetSummary.line.columns[0].format" class="w-[100px]" @change="loadSummary">
483
+ <option value="">{{ $t('Default') }}</option>
484
+ <option value="date">{{ $t('Date') }}</option>
485
+ </Dropdown>
486
+ </div>
487
+ </div>
488
+
489
+ <div class="p-3">
490
+ <label class="text-text-400 flex-1">Values</label>
491
+ <div class="flex flex-row mt-2 gap-2">
492
+ <Dropdown v-model="openedPresetSummary.line.values[0].aggregrate" class="flex-1" @change="loadSummary">
493
+ <option value="" disabled selected>{{ $t('Select') }}</option>
494
+ <option value="count">{{ $t('Count') }}</option>
495
+ <option value="max">{{ $t('Max') }}</option>
496
+ <option value="min">{{ $t('Min') }}</option>
497
+ <option value="avg">{{ $t('Avg') }}</option>
498
+ </Dropdown>
499
+ </div>
500
+ </div>
501
+
502
+ </div>
503
+
504
+ <div v-else-if="openedPresetSummary.mode === 'map'">
505
+
506
+ <div class="p-3 flex flex-row gap-3">
507
+ <div class="flex-1">
508
+ <label class="text-text-400 flex-1">Map Type</label>
509
+ <Dropdown class="mt-2" v-model="openedPresetSummary.map.mapType" @change="loadSummary">
510
+ <option value="" disabled selected>(Default)</option>
511
+ <option value="heatmap">Heatmap</option>
512
+ </Dropdown>
513
+ </div>
514
+ <div>
515
+ <label class="text-text-400 flex-1">Radius</label>
516
+ <div class="flex flex-row mt-2 gap-2">
517
+ <Textbox v-model="openedPresetSummary.map.mapRadius" class="w-[60px]" @blur="loadSummary" @keyup.enter="loadSummary" />
518
+ </div>
519
+ </div>
520
+ </div>
521
+
522
+ <div class="p-3">
523
+ <label class="text-text-400 flex-1">{{$t('Property') }}</label>
524
+ <div>
525
+ <Dropdown v-model="openedPresetSummary.map.key" class="flex-1" @change="loadSummary">
526
+ <option value="" disabled selected>{{ $t('Select Column') }}</option>
527
+ <option v-for="column in coordinateColumns" :value="column.key">{{ column.label }}</option>
528
+ </Dropdown>
529
+ </div>
530
+ </div>
531
+
532
+ </div>
533
+
534
+ </div>
535
+
536
+ </div>
537
+
538
+ </div>
539
+ </div>
540
+
541
+ </div>
542
+
543
+ </div>
544
+ </template>
545
+
546
+ <script>
547
+
548
+ import throttle from "lodash/throttle";
549
+ import {urlQuery} from "../utils/helpers.mjs";
550
+ import { Bar, Line } from 'vue-chartjs'
551
+ import Chart from 'chart.js/auto'
552
+ import dayjs from "dayjs";
553
+
554
+ export default{
555
+
556
+ components: { Bar, Line },
557
+
558
+ inject: [ 'socket', 'confirm', 'alert', 'socketEmit', 'toast' ],
559
+
560
+ props:{
561
+
562
+ datasource: String,
563
+
564
+ model: String,
565
+
566
+ title: String,
567
+
568
+ pinned: Function,
569
+
570
+ summaryTeleport: String
571
+
572
+ },
573
+
574
+ computed: {
575
+
576
+ filterableColumns(){
577
+ return Object.values(this.configs.columns).filter((_) => _.filterable)
578
+ },
579
+
580
+ coordinateColumns(){
581
+ return Object.values(this.configs.columns).filter((_) => _.type === 'coord')
582
+ },
583
+
584
+ sortableColumns(){
585
+ return Object.values(this.configs.columns).filter((_) => _.sortable)
586
+ },
587
+
588
+
589
+ preset(){
590
+ return this.configs.presets[this.configs.presetIdx] ?? {}
591
+ },
592
+
593
+ presetColumns(){
594
+ if(!this.preset.columns) return []
595
+
596
+ for(let i = 0 ; i < (this.preset.columns).length ; i++){
597
+ const presetColumn = this.preset.columns[i]
598
+ const column = this.configs.columns[presetColumn.key] ?? {}
599
+ for(let key in column){
600
+ if(!presetColumn[key]){
601
+ this.preset.columns[i][key] = column[key]
602
+ }
603
+ }
604
+ }
605
+
606
+ return this.preset.columns
607
+ },
608
+
609
+ presetAppearances(){
610
+ return this.preset.appearances ?? {}
611
+ },
612
+
613
+ presetSortedColumns(){
614
+ const c = {};
615
+ (this.preset.sorts ?? []).forEach((_) => c[_.key] = _.type);
616
+ return c
617
+ },
618
+
619
+ presetCurrentFilters(){
620
+ if(!this.preset.filters) return []
621
+ return this.preset.filters.filter((_) => _.key === this.selectedColumn)
622
+ },
623
+
624
+
625
+ computedDataSource(){
626
+ return this.datasource ?? this.model
627
+ },
628
+
629
+ computedTitle(){
630
+ return this.presetSummary && this.presetSummary.title ?
631
+ this.presetSummary.title :
632
+ this.title
633
+ },
634
+
635
+
636
+ headerSlots(){
637
+ const slots = {}
638
+ for(let key in this.$slots){
639
+ if(key.startsWith('col-'))
640
+ slots[key] = this.$slots[key]
641
+ }
642
+ return slots
643
+ },
644
+
645
+ contentSlots(){
646
+ const slots = {}
647
+ for(let key in this.$slots){
648
+ if(!key.startsWith('col-'))
649
+ slots[key] = this.$slots[key]
650
+ }
651
+ return slots
652
+ },
653
+
654
+
655
+ chartOptions(){
656
+
657
+ var style = getComputedStyle(document.body)
658
+ var gridColor = style.getPropertyValue('--text-50')
659
+ var gridColor2 = style.getPropertyValue('--text-200')
660
+
661
+ const baseColumn = this.summary.columns[0] ?? {}
662
+ const baseColumnType = baseColumn.type
663
+
664
+ let highGrids = []
665
+ if(baseColumnType === 'date'){
666
+ this.summary.items.forEach((item) => {
667
+ if(dayjs(item.dfDate).day() === 1){
668
+ highGrids.push(dayjs(item.dfDate).format('D MMM'))
669
+ }
670
+ })
671
+ }
672
+
673
+ return {
674
+ responsive: true,
675
+ maintainAspectRatio: false,
676
+ plugins: {
677
+ legend: !this.presetSummary.hideLegends,
678
+ tooltip: {
679
+ callbacks: {
680
+ label: function(context) {
681
+
682
+ const labels = []
683
+
684
+ labels.push(context.dataset.label + ': ' + context.parsed.y)
685
+
686
+ if(context.parsed._stacks){
687
+ let total = 0
688
+ let percent = 0
689
+ for(let key in context.parsed._stacks.y){
690
+ if(!isNaN(parseInt(key))){
691
+ total += parseInt(context.parsed._stacks.y[key])
692
+ }
693
+ }
694
+ percent = Math.round(parseInt(context.parsed.y) / total * 100)
695
+ labels.push(`Total: ${total} (${percent}%)`)
696
+ }
697
+
698
+ return labels;
699
+ }
700
+ }
701
+ }
702
+ },
703
+ scales: {
704
+ x: {
705
+ grid: {
706
+ color: function(context){
707
+ if(baseColumnType === 'date' && context.tick && highGrids.includes(context.tick.label)){
708
+ return `rgb(${gridColor2})`
709
+ }
710
+ return `rgb(${gridColor})`
711
+ }
712
+ },
713
+ stacked: this.presetSummary.mode === 'bar' ? this.presetSummary[this.presetSummary.mode].stacked : undefined
714
+ },
715
+ y: {
716
+ grid: {
717
+ color: function(context){
718
+ return `rgb(${gridColor})`
719
+ }
720
+ },
721
+ stacked: this.presetSummary.mode === 'bar' ? this.presetSummary[this.presetSummary.mode].stacked : undefined
722
+ }
723
+ }
724
+ }
725
+ },
726
+
727
+ chartData(){
728
+
729
+ const column = this.summary.columns.filter((_) => _.key === 'dfDate').pop() ?? {}
730
+
731
+ const labels = []
732
+ this.summary.items.forEach((item) => {
733
+
734
+ let label = item.dfDate
735
+ switch(column.type){
736
+
737
+ case 'date':
738
+ const dateFormat = column.format ?? 'D MMM YY HH:mm:ss'
739
+ const djs = dayjs(item.dfDate)
740
+ label = djs.isValid() ? djs.format(dateFormat) : item.dfDate
741
+ break
742
+ }
743
+ labels.push(label)
744
+ })
745
+
746
+ const datasets = []
747
+ this.summary.columns.forEach((column) => {
748
+ if(column.key === 'dfDate') return
749
+
750
+ const data = []
751
+ this.summary.items.forEach((item) => {
752
+ data.push(parseInt(item[column.key]))
753
+ })
754
+
755
+ const dataset = {
756
+ label: column.label,
757
+ data,
758
+ backgroundColor: this.chartOpt.backgroundColors[datasets.length % 9],
759
+ borderColor: this.chartOpt.borderColors[datasets.length % 9]
760
+ }
761
+
762
+ datasets.push(dataset)
763
+ })
764
+
765
+ return {
766
+ labels,
767
+ datasets
768
+ }
769
+
770
+ /*return {
771
+ labels: [ 'January', 'February', 'March' ],
772
+ datasets: [ { data: [40, 20, 12] } ]
773
+ }*/
774
+ },
775
+
776
+ hideDetails(){
777
+ if(this.preset && this.presetSummary && this.presetSummary.hideDetails)
778
+ return this.presetSummary.hideDetails
779
+ return false;
780
+ },
781
+
782
+ summaryColumns(){
783
+
784
+ const columns = [ ...this.summary.columns ]
785
+
786
+ if(this.presetSummary.mode === 'table' && this.presetSummary.table.showColumnTotal){
787
+ columns.push({ key:"_total", label:"Total", visible:true, type:"number" })
788
+ }
789
+
790
+ return columns
791
+ },
792
+
793
+ summaryItems(){
794
+
795
+ const items = [ ...this.summary.items ]
796
+
797
+ //console.log('#1', this.presetSummary.showColumnTotal, this.presetSummary.showRowTotal, items.length)
798
+
799
+ if(this.presetSummary.mode === 'table' && this.presetSummary.table.showColumnTotal){
800
+ items.forEach((item) => {
801
+ let total = 0
802
+ for(let key in item){
803
+ if([ 'dfDate', '_total' ].includes(key)) continue
804
+ total += parseInt(item[key])
805
+ }
806
+ item._total = total
807
+ })
808
+ }
809
+
810
+
811
+ if(this.presetSummary.mode === 'table' && this.presetSummary.table.showRowTotal){
812
+ const totalItem = { dfDate:"Total" }
813
+ this.summary.items.forEach((item) => {
814
+
815
+ for(let key in item){
816
+ if([ 'dfDate' ].includes(key)) continue
817
+
818
+ if(!(key in totalItem))
819
+ totalItem[key] = 0
820
+ totalItem[key] += parseInt(item[key])
821
+ }
822
+ })
823
+ items.push(totalItem)
824
+ }
825
+
826
+ return items
827
+ },
828
+
829
+ openedPresetSummary(){
830
+ return ((this.preset ?? {}).summaries ?? [])[this.configs.summaryOpenIdx] ?? undefined
831
+ },
832
+
833
+ presetSummary(){
834
+ return ((this.preset ?? {}).summaries ?? []).filter((_) => _.enabled).pop()
835
+ },
836
+
837
+ },
838
+
839
+ data(){
840
+ return {
841
+
842
+ configs: {
843
+ name: null,
844
+ columns: [],
845
+ presets: [],
846
+ presetIdx: -1,
847
+ presetOpen: false,
848
+ presetOpenIdx: -1,
849
+ summaryOpenIdx: -1,
850
+ presetTab: 'column'
851
+ },
852
+
853
+ mapApiKey: null,
854
+
855
+ selectedColumn: null,
856
+ presetFilterSelector: null,
857
+ presetSortSelector: null,
858
+
859
+ items: [],
860
+ count: null,
861
+ hasNext: false,
862
+ isLoading: false,
863
+ isSummaryLoading: false,
864
+
865
+ summary: null,
866
+ summaryLoading: false,
867
+
868
+ lastLoadAt: null,
869
+
870
+ chartOpt: {
871
+ backgroundColors: [
872
+ '#5D9CEC',
873
+ '#A0D468',
874
+ '#FFCE54',
875
+ '#FC6E51',
876
+ '#48CFAD',
877
+ '#AC92EC',
878
+ '#4FC1E9',
879
+ '#FFCE54',
880
+ '#ED5565',
881
+ '#EC87C0'
882
+ ],
883
+ borderColors: [
884
+ '#4A89DC',
885
+ '#8CC152',
886
+ '#F6BB42',
887
+ '#E9573F',
888
+ '#37BC9B',
889
+ '#967ADC',
890
+ '#3BAFDA',
891
+ '#F6BB42',
892
+ '#DA4453',
893
+ '#D770AD'
894
+ ],
895
+ },
896
+ tabItems: [
897
+ { text:'Columns', value:'column' },
898
+ { text:'Filters', value:'filter' },
899
+ { text:'Sorts', value:'sort' },
900
+ { text:'Summary', value:'summary' },
901
+ ],
902
+ }
903
+ },
904
+
905
+ mounted() {
906
+ this.patch()
907
+ this.socket.onAny(this.onHooks)
908
+ window.addEventListener('focus', this.onFocus)
909
+ },
910
+
911
+ unmounted() {
912
+ window.removeEventListener('focus', this.onFocus)
913
+ this.socket.offAny(this.onHooks)
914
+
915
+ if(this.configs.name)
916
+ this.socketEmit(this.configs.name + '.unsubscribe', { name:this.configs.name })
917
+ },
918
+
919
+ methods:{
920
+
921
+ patch(){
922
+ this.isLoading = this.isSummaryLoading = true
923
+ this.socketEmit(`${this.computedDataSource}.patch`, {
924
+ ...(urlQuery().reset ? { reset:1 } : {})
925
+ }, (res) => {
926
+ const data = res.data ? res.data : res
927
+ this.isLoading = this.isSummaryLoading = false
928
+
929
+ Object.assign(this.configs, data.configs)
930
+ this.mapApiKey = data.mapApiKey
931
+ this.patchPresets()
932
+ this.$nextTick(() => {
933
+ this.items = data.items
934
+ this.hasNext = data.hasNext
935
+ this.count = data.count
936
+ this.lastLoadAt = new Date().getTime()
937
+
938
+ this.loadSummary()
939
+
940
+ if(this.configs.name)
941
+ this.socketEmit(this.configs.name + '.subscribe', { name:this.configs.name })
942
+ })
943
+
944
+ }, (err) => {
945
+ this.isLoading = this.isSummaryLoading = false
946
+ this.toast(err)
947
+ })
948
+ },
949
+
950
+ patchPresets(){
951
+
952
+ if(!this.configs.columns) return
953
+
954
+ this.configs.presets.forEach((preset) => {
955
+
956
+ const presetColumnKeys = {}
957
+ const removedKeys = []
958
+ for(let i = (preset.columns ?? []).length - 1 ; i >= 0 ; i--){
959
+ const presetColumn = preset.columns[i]
960
+ const presetColumnKey = presetColumn.key
961
+ if(this.configs.columns[presetColumnKey]){
962
+ const updated = {}
963
+ if(this.configs.columns[presetColumnKey].label !== presetColumn.label)
964
+ updated['label'] = this.configs.columns[presetColumnKey].label
965
+ if(this.configs.columns[presetColumnKey].filterable !== presetColumn.filterable)
966
+ updated['filterable'] = this.configs.columns[presetColumnKey].filterable
967
+ if(this.configs.columns[presetColumnKey].sortable !== presetColumn.sortable)
968
+ updated['sortable'] = this.configs.columns[presetColumnKey].sortable
969
+ if(Object.keys(updated).length > 0){
970
+ Object.assign(preset.columns[i], updated)
971
+ }
972
+ }
973
+ else{
974
+ preset.columns.splice(i, 1)
975
+ removedKeys.push(presetColumnKey)
976
+ }
977
+ presetColumnKeys[presetColumn.key] = 1
978
+ }
979
+
980
+ for(let key in this.configs.columns){
981
+ if(typeof presetColumnKeys[key] === 'undefined'){
982
+ preset.columns.push(this.configs.columns[key])
983
+ }
984
+ }
985
+ })
986
+
987
+ },
988
+
989
+ load(){
990
+ if(this.presetSummary){
991
+ if(!this.presetSummary.enabled && this.presetSummary.hideDetails){
992
+ this.presetSummary.hideDetails = false
993
+ }
994
+ }
995
+
996
+ this.isLoading = true
997
+ this.socketEmit(`${this.computedDataSource}.load`, {
998
+ preset: this.preset
999
+ }, (res) => {
1000
+ const data = res.data ? res.data : res
1001
+ this.items = data.items
1002
+ this.hasNext = data.hasNext
1003
+ this.count = data.count
1004
+ this.isLoading = false
1005
+ this.lastLoadAt = new Date().getTime()
1006
+ }, (err) => {
1007
+ this.isLoading = false
1008
+ this.toast(err)
1009
+ })
1010
+
1011
+ this.loadSummary()
1012
+ },
1013
+
1014
+ loadNext(){
1015
+ this.socketEmit(`${this.computedDataSource}.load`, {
1016
+ preset: this.preset,
1017
+ afterItem: this.items[this.items.length - 1]
1018
+ }, (res) => {
1019
+ this.items.push(...res.items)
1020
+ this.hasNext = res.hasNext
1021
+ })
1022
+ },
1023
+
1024
+ loadSummary(){
1025
+
1026
+ if(!this.presetSummary) return
1027
+
1028
+ this.isSummaryLoading = true
1029
+ this.socketEmit(`${this.computedDataSource}.load-summary`, {
1030
+ preset: this.preset
1031
+ }, (res) => {
1032
+ this.isSummaryLoading = false
1033
+ const data = res && res.data ? res.data : res
1034
+ this.summary = data
1035
+ }, (err) => {
1036
+ this.isSummaryLoading = false
1037
+ this.toast(err)
1038
+ })
1039
+ },
1040
+
1041
+ onFocus(){
1042
+ if(!this.lastLoadAt || (new Date().getTime() - this.lastLoadAt) > 600000)
1043
+ this.load()
1044
+ },
1045
+
1046
+ savePreset: throttle(function() {
1047
+ const presetName = this.configs.name ?? this.model
1048
+ const presetUrl = this.configs.name ?? 'user'
1049
+ this.socketEmit(`${presetUrl}.save-preset`, { key:presetName, preset:this.configs })
1050
+ }, 1000),
1051
+
1052
+ clearSearch(){
1053
+ this.preset.search = ''
1054
+ this.load()
1055
+ },
1056
+
1057
+ selectPreset(preset){
1058
+ this.configs.presetIdx = this.configs.presets.findIndex((_) => _ === preset)
1059
+ this.load()
1060
+ },
1061
+
1062
+ openPreset(index, presetTab = 'column'){
1063
+ this.configs.presetOpenIdx = index
1064
+ this.configs.presetOpen = true
1065
+ this.configs.presetTab = presetTab
1066
+ },
1067
+
1068
+ closePreset(){
1069
+ this.configs.presetOpen = false
1070
+ },
1071
+
1072
+ copyPreset(){
1073
+ const preset = JSON.parse(JSON.stringify(this.preset))
1074
+ preset.name += '(Copy)'
1075
+ this.presets.push(preset)
1076
+ this.presetIdx = this.presets.length - 1
1077
+ },
1078
+
1079
+ removePreset(idx){
1080
+ this.confirm(this.$t('Remove this preset?'), '', () => {
1081
+ this.configs.presets.splice(idx, 1)
1082
+ if(this.configs.presetIdx > this.configs.presets.length - 1){
1083
+ this.configs.presetIdx = this.configs.presets.length - 1
1084
+ }
1085
+ })
1086
+ },
1087
+
1088
+ onHooks(module, event, items){
1089
+
1090
+ if(!Array.isArray(items)) return
1091
+ items = items.filter((_) => _).map((_) => {
1092
+ _['_highlight'] = 1
1093
+ return _
1094
+ })
1095
+
1096
+ if(module === this.model){
1097
+
1098
+ switch(event){
1099
+
1100
+ case 'create':
1101
+ case 'update':
1102
+ items.forEach((item) => {
1103
+ this.$util.unshift(this.items, item)
1104
+ })
1105
+ break
1106
+
1107
+ case 'remove':
1108
+ case 'destroy':
1109
+ items.forEach((item) => {
1110
+ const idx = this.items.findIndex((_) => _.id === item.id)
1111
+ if(idx >= 0){
1112
+ this.items.splice(idx, 1)
1113
+ }
1114
+ })
1115
+ break
1116
+ }
1117
+ }
1118
+ },
1119
+
1120
+ addFilter(key){
1121
+
1122
+ const column = this.configs.columns[key]
1123
+
1124
+ if(!this.preset.filters){
1125
+ this.preset.filters = []
1126
+ }
1127
+
1128
+ let filters = [{}]
1129
+ switch(column.type){
1130
+ case 'date':
1131
+ case 'enum':
1132
+ filters = [{ value:[] }]
1133
+ break
1134
+ }
1135
+
1136
+ this.preset.filters.push({
1137
+ enabled: true,
1138
+ key: column.key,
1139
+ label: column.label,
1140
+ type: column.type,
1141
+ typeParams: column.typeParams,
1142
+ filters
1143
+ })
1144
+
1145
+ this.presetFilterSelector = null
1146
+ },
1147
+
1148
+ addCurrentFilter(){
1149
+ this.addFilter(this.selectedColumn)
1150
+ },
1151
+
1152
+ addSort(key){
1153
+
1154
+ const column = this.preset.columns.filter((_) => _.key === key).pop()
1155
+
1156
+ if(!this.preset.sorts){
1157
+ this.preset.sorts = []
1158
+ }
1159
+
1160
+ this.preset.sorts.push({
1161
+ key: column.key,
1162
+ label: column.label
1163
+ })
1164
+
1165
+ this.presetSortSelector = null
1166
+ this.load()
1167
+ },
1168
+
1169
+ removeSort(sort){
1170
+ this.preset.sorts.splice(this.preset.sorts.indexOf(sort), 1)
1171
+ this.load()
1172
+ },
1173
+
1174
+ removeFilter(filter){
1175
+ this.preset.filters.splice(this.preset.filters.indexOf(filter), 1)
1176
+ this.load()
1177
+ },
1178
+
1179
+ reorderColumns(from, to){
1180
+ this.preset.columns.splice(to, 0, this.preset.columns.splice(from, 1)[0])
1181
+ },
1182
+
1183
+ colOf(key){
1184
+ return 'col-' + key
1185
+ },
1186
+
1187
+ getHeader(column){
1188
+
1189
+ return [
1190
+ this.$style.header,
1191
+ (this.preset.filters ?? []).findIndex((_) => _.key === column.key && _.enabled) >= 0 ?
1192
+ this.$style.headerSelected : ''
1193
+ ]
1194
+ .join(' ')
1195
+ },
1196
+
1197
+ openColumnOptions(key, target){
1198
+ this.selectedColumn = key
1199
+ this.$refs.columnMenu.open(target)
1200
+ },
1201
+
1202
+ setSortCurrent(sortType){
1203
+ this.preset.sorts = [
1204
+ {
1205
+ key: this.selectedColumn,
1206
+ label: this.configs.columns[this.selectedColumn].label,
1207
+ type: sortType === 2 ? 'desc' : 'asc'
1208
+ }
1209
+ ]
1210
+ this.load()
1211
+ this.$refs.columnMenu.close()
1212
+ },
1213
+
1214
+ hide(){
1215
+ const idx = this.preset.columns.findIndex((_) => _.key === this.selectedColumn)
1216
+ this.preset.columns[idx].visible = false
1217
+ this.$refs.columnMenu.close()
1218
+ },
1219
+
1220
+ tabChanged(value){
1221
+ if(value === 'summary'){
1222
+ this.configs.summaryOpenIdx = -1
1223
+ }
1224
+ },
1225
+
1226
+ addSummary(){
1227
+
1228
+ this.preset.summaries.push({
1229
+ title: "New Summary",
1230
+ table: { rows: [{}], columns:[{}], values:[{}] },
1231
+ bar: { rows: [{}], columns:[{}], values:[{}] },
1232
+ line: { rows: [{}], columns:[{}], values:[{}] },
1233
+ map: {}
1234
+ })
1235
+
1236
+ this.configs.summaryOpenIdx = this.preset.summaries.length - 1
1237
+ },
1238
+
1239
+ removeSummary(summary){
1240
+ const index = this.preset.summaries.indexOf(summary)
1241
+ if(index >= 0){
1242
+ this.confirm('Remove this summary?', '', () => {
1243
+ this.preset.summaries.splice(index, 1)
1244
+ })
1245
+ }
1246
+ },
1247
+
1248
+ enableSummary(idx){
1249
+ this.preset.summaries.forEach((_, __) => _.enabled = idx === __)
1250
+ this.loadSummary()
1251
+ },
1252
+
1253
+ addPreset(){
1254
+
1255
+ const columns = []
1256
+ for(let key in this.configs.columns){
1257
+ columns.push({
1258
+ ...this.configs.columns[key],
1259
+ visible: false
1260
+ })
1261
+ }
1262
+
1263
+ this.configs.presets.push({
1264
+ name: 'New Preset',
1265
+ columns,
1266
+ filters: [],
1267
+ sorts: [],
1268
+ summaries: []
1269
+ })
1270
+
1271
+ this.configs.presetTab = 'column'
1272
+ this.configs.presetIdx = this.configs.presetOpenIdx = this.configs.presets.length - 1
1273
+ }
1274
+
1275
+ },
1276
+
1277
+ watch: {
1278
+
1279
+ configs: {
1280
+ deep: true,
1281
+ handler(to, from){
1282
+ if(from !== null){
1283
+ this.savePreset()
1284
+ }
1285
+ }
1286
+ },
1287
+
1288
+ 'openedPresetSummary.mode'(to){
1289
+ if(to && !this.openedPresetSummary[to]){
1290
+ this.openedPresetSummary[to] = { rows: [{}], columns:[{}], values:[{}] }
1291
+ }
1292
+ }
1293
+
1294
+ }
1295
+ }
1296
+
1297
+ </script>
1298
+
1299
+ <style module>
1300
+
1301
+ .comp{
1302
+ @apply flex-1 flex flex-row relative;
1303
+ }
1304
+
1305
+ .header{
1306
+ @apply p-2 cursor-pointer border-b-[2px] border-transparent overflow-hidden;
1307
+ }
1308
+ .header>*:first-child{
1309
+ @apply text-ellipsis whitespace-nowrap overflow-x-hidden;
1310
+ }
1311
+
1312
+ .headerSelected{
1313
+ @apply border-primary;
1314
+ }
1315
+
1316
+ .hoverable{
1317
+ @apply hover:bg-primary hover:text-white;
1318
+ }
1319
+
1320
+ .loadingComp{
1321
+ @apply absolute left-0 top-0 right-0 bottom-0;
1322
+ @apply flex flex-col gap-4 items-center justify-center;
1323
+ background: rgba(0, 0, 0, .3);
1324
+ }
1325
+
1326
+ .overlay{
1327
+ @apply absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center;
1328
+ background-color: rgba(var(--base-500), .7);
1329
+ }
1330
+
1331
+ </style>