@mixd-id/web-scaffold 0.1.2301231353 → 0.1.2301231355

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.2301231353",
4
+ "version": "0.1.2301231355",
5
5
  "scripts": {
6
6
  "dev": "vite serve",
7
7
  "build": "vite build",
@@ -2,7 +2,7 @@
2
2
  <Suspense @resolve="patch">
3
3
  <div :class="$style.comp">
4
4
 
5
- <div class="flex-1 flex flex-col p-6">
5
+ <div class="flex-1 flex flex-col p-6 relative">
6
6
 
7
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"/>
@@ -43,15 +43,19 @@
43
43
  </div>
44
44
  </div>
45
45
 
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
-
46
+ <div class="flex-1 mb-4 flex flex-col bg-base-500 border-text-50 border-[1px]" v-if="summary && !isLoading">
47
+ <VirtualTable v-if="preset.summary.mode === 'table'" :items="summaryItems" :columns="summaryColumns" class="flex-1"></VirtualTable>
49
48
  <Bar v-else-if="preset.summary.mode === 'bar'" :data="chartData" :options="chartOptions" class="flex-1 w-full p-2"/>
50
-
49
+ <Line v-else-if="preset.summary.mode === 'line'" :data="chartData" :options="chartOptions" class="flex-1 w-full p-2"/>
51
50
  </div>
52
51
 
53
- <VirtualTable v-if="!hideDetails" class="flex-1" :columns="presetColumns" :items="items"
52
+ <div v-if="isLoading" :class="$style.loadingComp">
53
+ <svg class="animate-spin aspect-square w-[36px] 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>
54
+ <label>Memuat...</label>
55
+ </div>
56
+ <VirtualTable v-else-if="!hideDetails" class="flex-1" :columns="presetColumns" :items="items" :state="isLoading ? 2 : 1"
54
57
  :appearances="appearances" @scroll-end="loadNext" :pinned="pinned">
58
+
55
59
  <template v-for="column in presetColumns" #[colOf(column.key)]="{}">
56
60
  <div :class="getHeader(column)" @click="openColumnOptions(column.key, $event.target.closest('.' + $style.header))">
57
61
  <div>
@@ -255,7 +259,18 @@
255
259
  </div>
256
260
 
257
261
  <div class="p-3">
258
- <label class="text-text-400 flex-1">Row</label>
262
+ <label class="text-text-400 flex-1">Type</label>
263
+ <div class="mt-2">
264
+ <Dropdown v-model="preset.summary.mode" class="flex-1" @change="load">
265
+ <option value="table">{{ $t('Table') }}</option>
266
+ <option value="bar">{{ $t('Bar Chart') }}</option>
267
+ <option value="line">{{ $t('Line Chart') }}</option>
268
+ </Dropdown>
269
+ </div>
270
+ </div>
271
+
272
+ <div class="p-3">
273
+ <label class="text-text-400 flex-1">{{ preset.summary.mode === 'table' ? $t('Row') : $t('X-axis') }}</label>
259
274
  <div class="flex flex-row mt-2 gap-2">
260
275
  <Dropdown v-model="preset.summary.rows[0].key" class="flex-1" @change="load">
261
276
  <option value="" disabled selected>{{ $t('Add Column') }}</option>
@@ -264,15 +279,21 @@
264
279
  <Dropdown v-model="preset.summary.rows[0].format" class="w-[100px]" @change="load">
265
280
  <option value="">{{ $t('Default') }}</option>
266
281
  <option value="date">{{ $t('Date') }}</option>
282
+ <option value="month">{{ $t('Month') }}</option>
283
+ <option value="year">{{ $t('Year') }}</option>
267
284
  </Dropdown>
268
285
  </div>
286
+ <Checkbox class="mt-2" v-if="preset.summary.mode === 'table'" v-model="preset.summary.showRowTotal">
287
+ Row Total
288
+ </Checkbox>
269
289
  </div>
270
290
 
271
291
  <div class="p-3">
272
- <label class="text-text-400 flex-1">Column</label>
292
+ <label class="text-text-400 flex-1">{{ preset.summary.mode === 'table' ? $t('Column') : $t('Dimension') }}</label>
273
293
  <div class="flex flex-row mt-2 gap-2">
274
294
  <Dropdown v-model="preset.summary.columns[0].key" class="flex-1" @change="load">
275
295
  <option value="" disabled selected>{{ $t('Add Column') }}</option>
296
+ <option value="(none)">{{ $t('None') }}</option>
276
297
  <option v-for="column in filterableColumns" :value="column.key">{{ column.label }}</option>
277
298
  </Dropdown>
278
299
  <Dropdown v-model="preset.summary.columns[0].format" class="w-[100px]" @change="load">
@@ -280,28 +301,23 @@
280
301
  <option value="date">{{ $t('Date') }}</option>
281
302
  </Dropdown>
282
303
  </div>
304
+ <Checkbox class="mt-2" v-if="preset.summary.mode === 'table'" v-model="preset.summary.showColumnTotal">
305
+ Column Total
306
+ </Checkbox>
307
+ <Checkbox class="mt-2" v-else-if="preset.summary.mode === 'bar'" v-model="preset.summary.stacked">
308
+ Stacked
309
+ </Checkbox>
283
310
  </div>
284
311
 
285
312
  <div class="p-3">
286
313
  <label class="text-text-400 flex-1">Values</label>
287
314
  <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>
315
+ <Dropdown v-model="preset.summary.values[0].aggregrate" class="flex-1" @change="load">
316
+ <option value="" disabled selected>{{ $t('Select') }}</option>
294
317
  <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>
318
+ <option value="max">{{ $t('Max') }}</option>
319
+ <option value="min">{{ $t('Min') }}</option>
320
+ <option value="avg">{{ $t('Avg') }}</option>
305
321
  </Dropdown>
306
322
  </div>
307
323
  </div>
@@ -310,8 +326,6 @@
310
326
 
311
327
  </div>
312
328
 
313
- <div class="px-6 py-3 flex flex-row gap-2"></div>
314
-
315
329
  </div>
316
330
 
317
331
  </div>
@@ -325,14 +339,13 @@
325
339
 
326
340
  import throttle from "lodash/throttle";
327
341
  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)
342
+ import { Bar, Line } from 'vue-chartjs'
343
+ import Chart from 'chart.js/auto'
344
+ import dayjs from "dayjs";
332
345
 
333
346
  export default{
334
347
 
335
- components: { Bar },
348
+ components: { Bar, Line },
336
349
 
337
350
  inject: [ 'socket', 'confirm', 'alert', 'socketEmit', 'toast' ],
338
351
 
@@ -421,6 +434,19 @@ export default{
421
434
 
422
435
  var style = getComputedStyle(document.body)
423
436
  var gridColor = style.getPropertyValue('--text-50')
437
+ var gridColor2 = style.getPropertyValue('--text-200')
438
+
439
+ const baseColumn = this.summary.columns[0]
440
+ const baseColumnType = baseColumn.type
441
+
442
+ let highGrids = []
443
+ if(baseColumnType === 'date'){
444
+ this.summary.items.forEach((item) => {
445
+ if(dayjs(item.dfDate).day() === 1){
446
+ highGrids.push(dayjs(item.dfDate).format('D MMM'))
447
+ }
448
+ })
449
+ }
424
450
 
425
451
  return {
426
452
  responsive: true,
@@ -431,12 +457,20 @@ export default{
431
457
  scales: {
432
458
  x: {
433
459
  grid: {
434
- color: `rgb(${gridColor})`
435
- }
460
+ color: function(context){
461
+ if(baseColumnType === 'date' && context.tick && highGrids.includes(context.tick.label)){
462
+ return `rgb(${gridColor2})`
463
+ }
464
+ return `rgb(${gridColor})`
465
+ }
466
+ },
467
+ stacked: this.preset.summary.stacked
436
468
  },
437
469
  y: {
438
470
  grid: {
439
- color: `rgb(${gridColor})`
471
+ color: function(context){
472
+ return `rgb(${gridColor})`
473
+ }
440
474
  }
441
475
  }
442
476
  }
@@ -447,9 +481,21 @@ export default{
447
481
 
448
482
  //console.log(this.summary)
449
483
 
484
+ const column = this.summary.columns.filter((_) => _.key === 'dfDate').pop()
485
+
450
486
  const labels = []
451
487
  this.summary.items.forEach((item) => {
452
- labels.push(item.dfDate)
488
+
489
+ let label = item.dfDate
490
+ switch(column.type){
491
+
492
+ case 'date':
493
+ const dateFormat = column.format ?? 'D MMM YY HH:mm:ss'
494
+ const djs = dayjs(item.dfDate)
495
+ label = djs.isValid() ? djs.format(dateFormat) : item.dfDate
496
+ break
497
+ }
498
+ labels.push(label)
453
499
  })
454
500
 
455
501
  const datasets = []
@@ -489,6 +535,52 @@ export default{
489
535
  if(this.preset && this.preset.summary && this.preset.summary.hideDetails)
490
536
  return this.preset.summary.hideDetails
491
537
  return false;
538
+ },
539
+
540
+ summaryColumns(){
541
+
542
+ const columns = [ ...this.summary.columns ]
543
+
544
+ if(this.preset.summary.showColumnTotal){
545
+ columns.push({ key:"_total", label:"Total", visible:true, type:"number" })
546
+ }
547
+
548
+ return columns
549
+ },
550
+
551
+ summaryItems(){
552
+
553
+ const items = [ ...this.summary.items ]
554
+
555
+ //console.log('#1', this.preset.summary.showColumnTotal, this.preset.summary.showRowTotal, items.length)
556
+
557
+ if(this.preset.summary.showColumnTotal){
558
+ items.forEach((item) => {
559
+ let total = 0
560
+ for(let key in item){
561
+ if([ 'dfDate', '_total' ].includes(key)) continue
562
+ total += parseInt(item[key])
563
+ }
564
+ item._total = total
565
+ })
566
+ }
567
+
568
+ if(this.preset.summary.showRowTotal){
569
+ const totalItem = { dfDate:"Total" }
570
+ this.summary.items.forEach((item) => {
571
+
572
+ for(let key in item){
573
+ if([ 'dfDate' ].includes(key)) continue
574
+
575
+ if(!(key in totalItem))
576
+ totalItem[key] = 0
577
+ totalItem[key] += parseInt(item[key])
578
+ }
579
+ })
580
+ items.push(totalItem)
581
+ }
582
+
583
+ return items
492
584
  }
493
585
 
494
586
  },
@@ -512,6 +604,7 @@ export default{
512
604
  { text:'Sorts', value:'sort' },
513
605
  { text:'Summary', value:'summary' },
514
606
  ],
607
+ isLoading: false,
515
608
  name: null,
516
609
  columns: [],
517
610
  selectedColumn: null,
@@ -542,6 +635,7 @@ export default{
542
635
 
543
636
  patch(){
544
637
 
638
+ this.isLoading = true
545
639
  this.socketEmit(`${this.computedDataSource}.patch`, {
546
640
  ...(urlQuery().reset ? { reset:1 } : {})
547
641
  }, (res) => {
@@ -557,10 +651,14 @@ export default{
557
651
  this.summary = data.summary
558
652
  this.hasNext = data.hasNext
559
653
  this.count = data.count
654
+ this.isLoading = false
560
655
  })
561
656
 
562
657
  if(this.name)
563
658
  this.socketEmit(this.name + '.subscribe', { name:this.name })
659
+ }, (err) => {
660
+ this.isLoading = false
661
+ this.toast(err)
564
662
  })
565
663
  },
566
664
 
@@ -604,6 +702,7 @@ export default{
604
702
  },
605
703
 
606
704
  load(){
705
+ this.isLoading = true
607
706
  this.socketEmit(`${this.computedDataSource}.load`, {
608
707
  preset: this.preset
609
708
  }, (res) => {
@@ -612,6 +711,10 @@ export default{
612
711
  this.summary = data.summary
613
712
  this.hasNext = data.hasNext
614
713
  this.count = data.count
714
+ this.isLoading = false
715
+ }, (err) => {
716
+ this.isLoading = false
717
+ this.toast(err)
615
718
  })
616
719
  },
617
720
 
@@ -843,7 +946,7 @@ export default{
843
946
  <style module>
844
947
 
845
948
  .comp{
846
- @apply flex-1 flex flex-row;
949
+ @apply flex-1 flex flex-row relative;
847
950
  }
848
951
 
849
952
  .header{
@@ -861,4 +964,10 @@ export default{
861
964
  @apply hover:bg-primary hover:text-white;
862
965
  }
863
966
 
967
+ .loadingComp{
968
+ @apply absolute left-0 top-0 right-0 bottom-0;
969
+ @apply flex flex-col gap-4 items-center justify-center;
970
+ background: rgba(0, 0, 0, .3);
971
+ }
972
+
864
973
  </style>
@@ -331,12 +331,13 @@ export default{
331
331
  }
332
332
 
333
333
  let text = value
334
+ let dateFormat, val
334
335
  switch(column.type){
335
336
 
336
337
  case 'date':
337
- const dateFormat = column.format ?? 'DD MMM YY HH:mm'
338
- const val = dayjs(value)
339
- text = val.isValid() ? val.format(dateFormat) : ''
338
+ dateFormat = column.format ?? 'D MMM YY HH:mm:ss'
339
+ val = dayjs(value)
340
+ text = val.isValid() ? val.format(dateFormat) : value
340
341
  break
341
342
 
342
343
  case 'currency':
@@ -1,7 +1,6 @@
1
1
  const { Op } = module.parent.require('sequelize')
2
2
  const Sequelize = module.parent.require('sequelize')
3
3
  const dayjs = require("dayjs");
4
- const util = require("util");
5
4
 
6
5
  let ListPage1 = {
7
6
 
@@ -107,11 +106,12 @@ let ListPage1 = {
107
106
  }
108
107
 
109
108
  if(preset.search){
109
+ const tableName = this.model.tableName
110
110
  where = {
111
111
  ...where,
112
- [Op.and]: Sequelize.literal('MATCH(tag) AGAINST (? IN BOOLEAN MODE)')
112
+ [Op.and]: Sequelize.literal(`MATCH(\`${tableName}\`.tag) AGAINST (? IN BOOLEAN MODE)`)
113
113
  }
114
- replacements.push(ftW)
114
+ replacements.push(preset.search)
115
115
  }
116
116
 
117
117
  if(afterItem){
@@ -164,6 +164,154 @@ let ListPage1 = {
164
164
  }
165
165
  },
166
166
 
167
+ async loadSummary(preset, modelParams){
168
+
169
+ const { columns, rows, values } = preset.summary
170
+
171
+ if(!Array.isArray(columns) || columns.length !== 1) return
172
+ if(!Array.isArray(rows) || rows.length !== 1) return
173
+ if(!Array.isArray(values) || values.length !== 1) return
174
+
175
+ const col = columns[0]
176
+ let columnValues = []
177
+ let colKey
178
+ if(col.key !== '(none)'){
179
+ colKey = this.model.rawAttributes[col.key].field
180
+ columnValues = await this.model.findAll({
181
+ attributes: [
182
+ [ Sequelize.fn("DISTINCT", Sequelize.col(colKey)), 'cols' ]
183
+ ],
184
+ order: [
185
+ [ colKey, 'asc' ]
186
+ ],
187
+ ...modelParams
188
+ })
189
+ //console.log(columnValues)
190
+ }
191
+
192
+ const attributes = []
193
+ const group = []
194
+ const summaryColumns = [
195
+ /*{ key:"dfDate", label:"Date", width:"200px", visible:true },
196
+ { key:"type-1", label:"Type 1", type:"number", visible:true },
197
+ { key:"type-2", label:"Type 2", type:"number", visible:true },
198
+ { key:"type-3", label:"Type 3", type:"number", visible:true },
199
+ { key:"type-4", label:"Type 4", type:"number", visible:true },
200
+ { key:"type-8", label:"Type 8", type:"number", visible:true },*/
201
+ ]
202
+
203
+ const row = rows[0]
204
+ const rowKey = this.model.rawAttributes[row.key].field
205
+ const rowLabel = this.columns[row.key].label
206
+ switch(row.format){
207
+ case 'date':
208
+ attributes.push([Sequelize.fn("DATE_FORMAT", Sequelize.col(rowKey), '%Y-%m-%d'), "dfDate"])
209
+ group.push([Sequelize.fn("DATE_FORMAT", Sequelize.col(rowKey), '%Y-%m-%d'), "dfDate"])
210
+ summaryColumns.push({ key:"dfDate", label:rowLabel, width:"200px", visible:true, type:"date", format:"D MMM" })
211
+ break
212
+
213
+ case 'month':
214
+ attributes.push([Sequelize.fn("DATE_FORMAT", Sequelize.col(rowKey), '%Y-%m'), "dfDate"])
215
+ group.push([Sequelize.fn("DATE_FORMAT", Sequelize.col(rowKey), '%Y-%m'), "dfDate"])
216
+ summaryColumns.push({ key:"dfDate", label:rowLabel, width:"200px", visible:true, type:"date", format:"MMM YYYY" })
217
+ break
218
+
219
+ case 'year':
220
+ attributes.push([Sequelize.fn("DATE_FORMAT", Sequelize.col(rowKey), '%Y'), "dfDate"])
221
+ group.push([Sequelize.fn("DATE_FORMAT", Sequelize.col(rowKey), '%Y'), "dfDate"])
222
+ summaryColumns.push({ key:"dfDate", label:rowLabel, width:"200px", visible:true, type:"date", format:"YYYY" })
223
+ break
224
+
225
+ default:
226
+ attributes.push([Sequelize.col(rowKey), "dfDate"])
227
+ group.push([Sequelize.col(rowKey), "dfDate"])
228
+ summaryColumns.push({ key:"dfDate", label:rowLabel, width:"200px", visible:true })
229
+ break
230
+ }
231
+
232
+ const value = values[0]
233
+ if(columnValues.length > 0){
234
+ columnValues.forEach((obj) => {
235
+ const columnValue = obj.dataValues['cols']
236
+
237
+ switch(value.aggregrate){
238
+ case 'count':
239
+ attributes.push([Sequelize.fn("SUM", Sequelize.fn("IF", Sequelize.literal(`${colKey} = '${columnValue}'`), 1, 0)), colKey + "-" + columnValue])
240
+ summaryColumns.push({ key:colKey + "-" + columnValue, label:(columnValue ?? '').toString(), visible:true, aggregrate:"count" })
241
+ break
242
+
243
+ case 'min':
244
+ attributes.push([Sequelize.fn("MIN", colKey), colKey + "-" + columnValue])
245
+ summaryColumns.push({ key:colKey + "-" + columnValue, label:(columnValue ?? '').toString(), visible:true, aggregrate:"count" })
246
+ break
247
+
248
+ case 'max':
249
+ attributes.push([Sequelize.fn("MAX", colKey), colKey + "-" + columnValue])
250
+ summaryColumns.push({ key:colKey + "-" + columnValue, label:(columnValue ?? '').toString(), visible:true, aggregrate:"count" })
251
+ break
252
+
253
+ case 'avg':
254
+ attributes.push([Sequelize.fn("AVG", colKey), colKey + "-" + columnValue])
255
+ summaryColumns.push({ key:colKey + "-" + columnValue, label:(columnValue ?? '').toString(), visible:true, aggregrate:"count" })
256
+ break
257
+ }
258
+ })
259
+ }
260
+ else{
261
+ switch(value.aggregrate) {
262
+ case 'count':
263
+ default:
264
+ attributes.push([Sequelize.fn('count', '*'), 'Count'])
265
+ summaryColumns.push({ key:'Count', label:'Count', type:"number", visible:true, aggregrate:"count" })
266
+ }
267
+ }
268
+
269
+ //console.log(attributes)
270
+
271
+ const summaryItems = await this.model.findAll({
272
+ attributes,
273
+ group,
274
+ order: [
275
+ [ 'dfDate', 'asc' ]
276
+ ],
277
+ ...modelParams
278
+ })
279
+ /*const summaryItems = await this.model.findAll({
280
+ attributes: [
281
+ [Sequelize.fn("DATE_FORMAT", Sequelize.col("created_at"), '%Y-%m-%d'), "dfDate"],
282
+ [Sequelize.fn("SUM", Sequelize.fn("IF", Sequelize.literal("type = 1"), 1, 0)), "type-1"],
283
+ [Sequelize.literal("SUM(IF(type = 2, 1, 0))"), "type-2"],
284
+ [Sequelize.literal("SUM(IF(type = 3, 1, 0))"), "type-3"],
285
+ [Sequelize.literal("SUM(IF(type = 4, 1, 0))"), "type-4"],
286
+ [Sequelize.literal("SUM(IF(type = 8, 1, 0))"), "type-8"],
287
+ ],
288
+ group: [
289
+ [Sequelize.fn("DATE_FORMAT", Sequelize.col("created_at"), '%Y-%m-%d')]
290
+ ],
291
+ order: [
292
+ [ 'dfDate', 'asc' ]
293
+ ],
294
+ ...modelParams
295
+ })*/
296
+ //console.log(summaryItems)
297
+
298
+ const summary = {
299
+ type: "table",
300
+ columns: summaryColumns,
301
+ items: summaryItems
302
+ /*items: [
303
+ { date:"21 Jan 23", "type-1":12, "type-2":11, "type-3":16, "type-4":2, "type-8":8 },
304
+ { date:"22 Jan 23", "type-1":51, "type-2":87, "type-3":86, "type-4":3, "type-8":9 },
305
+ { date:"23 Jan 23", "type-1":14, "type-2":66, "type-3":77, "type-4":4, "type-8":18 },
306
+ { date:"24 Jan 23", "type-1":7, "type-2":3, "type-3":63, "type-4":5, "type-8":28 },
307
+ { date:"25 Jan 23", "type-1":88, "type-2":55, "type-3":68, "type-4":6, "type-8":38 },
308
+ { date:"26 Jan 23", "type-1":172, "type-2":13, "type-3":6, "type-4":7, "type-8":48 }
309
+ ]*/
310
+ }
311
+
312
+ return summary
313
+ },
314
+
167
315
  async subscribe(params, socket){
168
316
  const { name } = params
169
317
  if(name){