@drax/dashboard-vue 2.7.0 → 2.9.0

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
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "2.7.0",
6
+ "version": "2.9.0",
7
7
  "type": "module",
8
8
  "main": "./src/index.ts",
9
9
  "module": "./src/index.ts",
@@ -23,8 +23,8 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@drax/crud-front": "^2.0.0",
26
- "@drax/crud-share": "^2.4.0",
27
- "@drax/dashboard-front": "^2.6.0"
26
+ "@drax/crud-share": "^2.8.0",
27
+ "@drax/dashboard-front": "^2.8.0"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "pinia": "^3.0.4",
@@ -46,5 +46,5 @@
46
46
  "vue-tsc": "^3.2.4",
47
47
  "vuetify": "^3.11.8"
48
48
  },
49
- "gitHead": "6914eb5bfd532fb5e510b95c7d17b5b2ada8b07d"
49
+ "gitHead": "2bc9b59a45c762bd32403dce14db2693be34dcc7"
50
50
  }
@@ -1,31 +1,108 @@
1
1
  <script setup lang="ts">
2
- import { ref, watch,computed, type PropType } from 'vue';
3
- import type { IDashboardCard } from "@drax/dashboard-share";
2
+ import {ref, watch, computed, type PropType} from 'vue';
3
+ import type {IDashboardCard} from "@drax/dashboard-share";
4
4
  import {useEntityStore} from "@drax/crud-vue";
5
+ import type {
6
+ IDraxFieldFilter,
7
+ IEntityCrud,
8
+ IEntityCrudFieldTypes,
9
+ IEntityCrudFilter,
10
+ IEntityCrudFilterOperators
11
+ } from "@drax/crud-share";
12
+ import {useI18n} from "vue-i18n"
13
+ import CrudFormField from "@drax/crud-vue/src/components/CrudFormField.vue";
14
+ import {useFilterIcon} from "@drax/crud-vue";
5
15
 
6
16
  const props = defineProps({
7
- modelValue: { type: Object as PropType<IDashboardCard>, required: true }
17
+ modelValue: {type: Object as PropType<IDashboardCard>, required: true}
8
18
  });
19
+ const {filterIcon} = useFilterIcon()
20
+ const {t, te} = useI18n()
9
21
 
10
22
  const emit = defineEmits(['update:modelValue', 'save', 'cancel']);
11
23
 
12
24
  // Create a local reactive copy
13
25
  const form = ref<IDashboardCard>(JSON.parse(JSON.stringify(props.modelValue)));
14
26
 
27
+ const entityStore = useEntityStore();
28
+
29
+ const entitySelected = computed<IEntityCrud>(() => {
30
+ return entityStore.entities.find((entity: any) => entity.name === form.value.entity)
31
+ })
32
+
33
+ const entityField = computed(() => {
34
+ return (fieldName: string) => {
35
+ if(!entitySelected.value){
36
+ return null
37
+ }
38
+ return entitySelected.value.fields.find(f => f.name === fieldName)
39
+ }
40
+ })
41
+
42
+
43
+ const entities = computed(() => {
44
+ return entityStore.entities.map((entity: any) => entity.name)
45
+ })
46
+
47
+ const columns = computed(() => {
48
+ return entitySelected.value ? entitySelected.value.headers.map((h: any) => ({title: h.title, value: h.key})) : []
49
+ })
50
+
51
+
52
+ const filters = ref<IEntityCrudFilter[]>(prepareFilters(form.value.filters));
53
+
54
+ function prepareFilters(filters: IDraxFieldFilter[] = []): IEntityCrudFilter[] {
55
+
56
+ return filters.map((filter: IDraxFieldFilter) => {
57
+ let theField = entityField.value(filter.field)
58
+
59
+ let value = filter.value
60
+ if(value === 'true'){
61
+ value = true
62
+ }
63
+ if(value === 'false'){
64
+ value = false
65
+ }
66
+
67
+ return {
68
+ name: filter.field,
69
+ operator: filter.operator as IEntityCrudFilterOperators,
70
+ value: value,
71
+ type: theField?.type || 'string',
72
+ label: theField?.label || '',
73
+ default: '',
74
+ ref: theField?.ref || '',
75
+ refDisplay: theField?.refDisplay || '',
76
+ enum: theField?.enum || []
77
+ }
78
+
79
+ })
80
+ }
81
+
15
82
  watch(() => props.modelValue, (newVal) => {
16
83
  form.value = JSON.parse(JSON.stringify(newVal));
17
- }, { deep: true });
84
+ ensureStructure();
85
+ }, {deep: true});
18
86
 
19
87
  // Ensure nested objects exist to avoid v-model errors
20
88
  const ensureStructure = () => {
21
- if (!form.value.layout) form.value.layout = { cols: 12, sm: 12, md: 12, lg: 12, height: 450, cardVariant: 'outlined' };
22
- if (!form.value.groupBy) form.value.groupBy = { fields: [], dateFormat: 'day', render: 'pie' };
23
- if (!form.value.paginate) form.value.paginate = { columns: [], orderBy: '', order: '' };
89
+ if (!form.value.layout) form.value.layout = {cols: 12, sm: 12, md: 12, lg: 12, height: 450, cardVariant: 'elevated'};
90
+ if (!form.value.groupBy) form.value.groupBy = {fields: [], dateFormat: 'day', render: 'pie'};
91
+ if (!form.value.paginate) form.value.paginate = {columns: [], orderBy: '', order: ''};
92
+ if (!form.value.filters) form.value.filters = [];
24
93
  };
25
94
  ensureStructure();
26
95
 
27
96
  const save = () => {
28
- emit('update:modelValue', form.value);
97
+ const payload = form.value
98
+ payload.filters = filters.value
99
+ .filter((filter: IEntityCrudFilter) => filter.name && filter.operator)
100
+ .map((filter: IEntityCrudFilter) => ({
101
+ field: filter.name,
102
+ operator: filter.operator as string,
103
+ value: filter.value == null ? '' : String(filter.value)
104
+ }))
105
+ emit('update:modelValue', payload);
29
106
  emit('save');
30
107
  };
31
108
 
@@ -33,29 +110,105 @@ const cancel = () => {
33
110
  emit('cancel');
34
111
  };
35
112
 
36
- const entities = computed(() => {
37
- const dashboardStore = useEntityStore();
38
- return dashboardStore.entities.map((entity: any) => entity.name)
39
- })
40
-
41
- const columns = computed(() => {
42
- const dashboardStore = useEntityStore();
43
- const entity = dashboardStore.entities.find((entity: any) => entity.name === form.value.entity)
44
- return entity ? entity.headers.map((h:any) => ({title: h.title, value: h.key}) ) : []
45
- })
46
113
 
47
- function onEntityChange(){
48
- console.log('entity change',form.value)
49
- if(form.value){
50
- if(form.value.paginate){
114
+ function onEntityChange() {
115
+ if (form.value) {
116
+ if (form.value.paginate) {
51
117
  form.value.paginate.columns = []
52
118
  }
53
- if(form.value.groupBy){
119
+ if (form.value.groupBy) {
54
120
  form.value.groupBy.fields = []
55
121
  }
56
122
  }
57
123
  }
58
124
 
125
+ const fieldI18n = computed(() => {
126
+ return (field: IEntityCrudFilter) => {
127
+ return te(entitySelected.value?.name?.toLowerCase() + ".field." + field.name) ? t(entitySelected.value?.name?.toLowerCase() + ".field." + field.name) : field.label
128
+ }
129
+ })
130
+
131
+ const selectableFields = computed(() => {
132
+ return entitySelected.value ?
133
+ entitySelected.value.fields
134
+ .filter((f: any) => !['fullFile', 'object', 'array.object'].includes(f.type))
135
+ .map((f: any) => ({title: fieldI18n.value(f), value: f.name}))
136
+ : []
137
+ })
138
+
139
+ const dynamicFilter = computed(() => {
140
+ return (index: number) => {
141
+ return filters.value[index]
142
+ }
143
+ })
144
+
145
+ const operations = [
146
+ {title: t('operation.equals'), value: 'eq'},
147
+ {title: t('operation.notEquals'), value: 'ne'},
148
+ {title: t('operation.contains'), value: 'like'},
149
+ {title: t('operation.greaterThan'), value: 'gt'},
150
+ {title: t('operation.lessThan'), value: 'lt'},
151
+ {title: t('operation.greaterThanOrEqual'), value: 'gte'},
152
+ {title: t('operation.lessThanOrEqual'), value: 'lte'},
153
+ ]
154
+
155
+ function removeFilter(index: number) {
156
+ if (filters.value) {
157
+ filters.value.splice(index, 1)
158
+ }
159
+ }
160
+
161
+ function addFilter() {
162
+ const filter: IEntityCrudFilter = {
163
+ default: undefined,
164
+ label: "",
165
+ name: '',
166
+ operator: 'eq',
167
+ type: 'string',
168
+ permission: '',
169
+ value: ''
170
+ }
171
+ if (filters.value) {
172
+ filters.value.push(filter)
173
+ }
174
+ }
175
+
176
+ function normalizeFieldType(type: string): IEntityCrudFieldTypes {
177
+ if (type === 'array.ref') return 'ref';
178
+ if (type === 'array.string') return 'string';
179
+ if (type === 'longString') return 'string';
180
+ if (type === 'array.number') return 'number';
181
+ if (type === 'array.enum') return 'enum';
182
+ return type as IEntityCrudFieldTypes;
183
+ }
184
+
185
+ function onUpdateField(index: number, val: string) {
186
+ const field = entitySelected.value.fields.find((e: any) => e.name === val)
187
+ let filter = dynamicFilter.value(index)
188
+ if (!filter) {
189
+ return
190
+ }
191
+ filter.value = null
192
+ if (!field) return
193
+
194
+ if (field.ref) {
195
+ filter.ref = field.ref
196
+ }
197
+ if (field.refDisplay) {
198
+ filter.refDisplay = field.refDisplay
199
+ }
200
+ if (field.enum) {
201
+ filter.enum = field.enum
202
+ }
203
+ if (field.type) {
204
+ filter.type = normalizeFieldType(field.type)
205
+ if(field.type === 'boolean'){
206
+ filter.value = false
207
+ }
208
+ }
209
+ }
210
+
211
+
59
212
  </script>
60
213
 
61
214
  <template>
@@ -69,12 +222,13 @@ function onEntityChange(){
69
222
 
70
223
  <v-card-text class="pt-4 flex-grow-1 overflow-y-auto">
71
224
  <v-form @submit.prevent>
72
- <v-row dense>
225
+ <v-row v-if="form" dense>
73
226
  <!-- Base Config -->
74
227
  <v-col cols="12" md="12">
75
- <v-text-field v-model="form.title" label="Título de la tarjeta" variant="outlined" density="compact" hide-details="auto" class="mb-3"></v-text-field>
228
+ <v-text-field v-model="form.title" label="Título de la tarjeta" variant="outlined" density="compact"
229
+ hide-details="auto" class="mb-3"></v-text-field>
76
230
  </v-col>
77
- <v-col cols="12" md="6" lg="4">
231
+ <v-col cols="12" md="6" lg="6">
78
232
  <v-select :items="entities"
79
233
  v-model="form.entity"
80
234
  label="Entidad (ej. User, Country)"
@@ -86,56 +240,134 @@ function onEntityChange(){
86
240
  @update:modelValue="onEntityChange"
87
241
  ></v-select>
88
242
  </v-col>
89
- <v-col cols="12" md="6" lg="4">
90
- <v-select v-model="form.type" :items="['groupBy' , 'paginate']" label="Tipo de tarjeta" variant="outlined" density="compact" hide-details="auto" class="mb-3"></v-select>
243
+ <v-col cols="12" md="6" lg="6">
244
+ <v-select v-model="form.type" :items="['groupBy' , 'paginate']" label="Tipo de tarjeta" variant="outlined"
245
+ density="compact" hide-details="auto" class="mb-3"></v-select>
246
+ </v-col>
247
+
248
+ <v-col cols="12" md="6" lg="6">
249
+ <v-select v-model="form.layout!.cardVariant" :items="['elevated', 'outlined', 'flat','text','tonal','plain']" label="Tipo de tarjeta" variant="outlined"
250
+ density="compact" hide-details="auto" class="mb-3"></v-select>
91
251
  </v-col>
92
252
 
93
- <v-col cols="12" md="6" lg="4">
94
- <v-text-field v-if="form.layout" v-model="form.layout!.height" label="Altura" variant="outlined" density="compact" hide-details="auto" class="mb-3"></v-text-field>
253
+
254
+ <v-col cols="12" md="6" lg="6">
255
+ <v-text-field v-if="form.layout" v-model="form.layout!.height" label="Altura" variant="outlined"
256
+ density="compact" hide-details="auto" class="mb-3"></v-text-field>
95
257
  </v-col>
96
258
 
259
+
260
+ <v-divider class="my-2 w-100"></v-divider>
261
+ <!-- Filters HERE -->
262
+ <v-row dense class="mt-4">
263
+ <v-col v-for="(filter,index) in filters"
264
+ :key="filter.name"
265
+ cols="12"
266
+ >
267
+ <v-row>
268
+ <v-col cols="12" sm="4">
269
+ <v-select
270
+ :items="selectableFields"
271
+ v-model="dynamicFilter(index)!.name"
272
+ :label="t('crud.field')"
273
+ density="compact"
274
+ variant="outlined"
275
+ hide-details
276
+ @update:modelValue="(v:string) => onUpdateField(index, v)"
277
+ />
278
+ </v-col>
279
+ <v-col cols="12" sm="3">
280
+ <v-select
281
+ :items="operations"
282
+ v-model="dynamicFilter(index)!.operator"
283
+ :label="t('crud.operator')"
284
+ density="compact"
285
+ variant="outlined"
286
+ hide-details
287
+ />
288
+ </v-col>
289
+ <v-col cols="12" sm="4">
290
+ <crud-form-field
291
+ v-if="entitySelected"
292
+ :field="filter"
293
+ :entity="entitySelected"
294
+ v-model="dynamicFilter(index)!.value"
295
+ :clearable="true"
296
+ density="compact"
297
+ variant="outlined"
298
+ :prepend-inner-icon="filterIcon(filter)"
299
+ hide-details
300
+ />
301
+ </v-col>
302
+ <v-col cols="12" sm="1">
303
+ <v-btn @click="removeFilter(index)"
304
+ icon="mdi-delete"
305
+ class="mr-1"
306
+ variant="text"
307
+ color="red"
308
+ >
309
+ </v-btn>
310
+ </v-col>
311
+ </v-row>
312
+
313
+
314
+ </v-col>
315
+
316
+ <v-col cols="12">
317
+ <v-btn small variant="outlined" color="primary" @click="addFilter">+ {{ t('action.addFilter') }}</v-btn>
318
+ </v-col>
319
+
320
+ </v-row>
321
+
97
322
  <v-divider class="my-2 w-100" v-if="form.type"></v-divider>
98
323
 
99
324
  <!-- Type specific config -->
100
325
  <template v-if="form.type === 'paginate'">
101
326
  <v-col cols="12">
102
- <div class="text-subtitle-2 mb-2 text-primary d-flex align-center"><v-icon icon="mdi-table" size="small" class="mr-1"></v-icon> Paginate</div>
327
+ <div class="text-subtitle-2 mb-2 text-primary d-flex align-center">
328
+ <v-icon icon="mdi-table" size="small" class="mr-1"></v-icon>
329
+ Paginate
330
+ </div>
103
331
  </v-col>
104
332
  <v-col cols="12">
105
- <v-select item-title="title"
106
- item-value="value"
107
- :items="columns"
108
- v-model="form.paginate!.columns"
109
- label="Columnas (presiona enter)"
110
- multiple chips variant="outlined"
111
- density="compact"
112
- hide-details="auto"
113
- class="mb-3">
333
+ <v-select item-title="title"
334
+ item-value="value"
335
+ :items="columns"
336
+ v-model="form.paginate!.columns"
337
+ label="Columnas (presiona enter)"
338
+ multiple chips variant="outlined"
339
+ density="compact"
340
+ hide-details="auto"
341
+ class="mb-3">
114
342
 
115
343
  </v-select>
116
344
  </v-col>
117
345
  <v-col cols="12" md="6">
118
- <v-select
119
- item-title="title"
120
- item-value="value"
121
- :items="columns"
122
- v-model="form.paginate!.orderBy"
123
- label="Ordenar por campo"
124
- variant="outlined"
125
- density="compact"
126
- hide-details="auto"
127
- class="mb-3"
128
- clearable
129
- ></v-select>
346
+ <v-select
347
+ item-title="title"
348
+ item-value="value"
349
+ :items="columns"
350
+ v-model="form.paginate!.orderBy"
351
+ label="Ordenar por campo"
352
+ variant="outlined"
353
+ density="compact"
354
+ hide-details="auto"
355
+ class="mb-3"
356
+ clearable
357
+ ></v-select>
130
358
  </v-col>
131
359
  <v-col cols="12" md="6">
132
- <v-select v-model="form.paginate!.order" clearable :items="['asc', 'desc']" label="Dirección de orden" variant="outlined" density="compact" hide-details="auto" class="mb-3"></v-select>
360
+ <v-select v-model="form.paginate!.order" clearable :items="['asc', 'desc']" label="Dirección de orden"
361
+ variant="outlined" density="compact" hide-details="auto" class="mb-3"></v-select>
133
362
  </v-col>
134
363
  </template>
135
364
 
136
365
  <template v-else-if="form.type === 'groupBy'">
137
366
  <v-col cols="12">
138
- <div class="text-subtitle-2 mb-2 text-primary d-flex align-center"><v-icon icon="mdi-chart-pie" size="small" class="mr-1"></v-icon> Group By</div>
367
+ <div class="text-subtitle-2 mb-2 text-primary d-flex align-center">
368
+ <v-icon icon="mdi-chart-pie" size="small" class="mr-1"></v-icon>
369
+ Group By
370
+ </div>
139
371
  </v-col>
140
372
  <v-col cols="12">
141
373
  <v-select
@@ -149,10 +381,14 @@ function onEntityChange(){
149
381
  class="mb-3"></v-select>
150
382
  </v-col>
151
383
  <v-col cols="12" md="6">
152
- <v-select v-model="form.groupBy!.dateFormat" :items="['year', 'month', 'day', 'hour', 'minute', 'second']" label="Formato de Fecha (opcional)" variant="outlined" density="compact" hide-details="auto" clearable class="mb-3"></v-select>
384
+ <v-select v-model="form.groupBy!.dateFormat" :items="['year', 'month', 'day', 'hour', 'minute', 'second']"
385
+ label="Formato de Fecha (opcional)" variant="outlined" density="compact" hide-details="auto"
386
+ clearable class="mb-3"></v-select>
153
387
  </v-col>
154
388
  <v-col cols="12" md="6">
155
- <v-select v-model="form.groupBy!.render" :items="['pie', 'bars', 'table', 'gallery']" label="Render visual" variant="outlined" density="compact" hide-details="auto" class="mb-3"></v-select>
389
+ <v-select v-model="form.groupBy!.render" :items="['pie', 'bars', 'table', 'gallery']"
390
+ label="Render visual" variant="outlined" density="compact" hide-details="auto"
391
+ class="mb-3"></v-select>
156
392
  </v-col>
157
393
  </template>
158
394
  </v-row>
@@ -169,7 +405,7 @@ function onEntityChange(){
169
405
 
170
406
  <style scoped>
171
407
  .dashboard-card-editor {
172
- border: 1px solid rgba(0, 0, 0, 0.12);
173
- box-shadow: 0 4px 20px rgba(0,0,0,0.1) !important;
408
+ border: 1px solid rgba(0, 0, 0, 0.12);
409
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1) !important;
174
410
  }
175
411
  </style>
@@ -4,6 +4,7 @@ import type {IDashboardBase, IDashboardCard} from "@drax/dashboard-share";
4
4
  import GroupByCard from "../GroupByCard/GroupByCard.vue";
5
5
  import PaginateCard from "../PaginateCard/PaginateCard.vue";
6
6
  import DashboardCardEditor from "./DashboardCardEditor.vue";
7
+ import {debounce} from "@drax/common-front"
7
8
 
8
9
  const valueModel = defineModel<IDashboardBase>({required: true})
9
10
 
@@ -88,17 +89,22 @@ const addNewCard = () => {
88
89
  valueModel.value.cards.push({
89
90
  title: 'Nueva Tarjeta',
90
91
  entity: '',
92
+ filters: [],
91
93
  type: 'groupBy',
92
- layout: {cols: 12, sm: 12, md: 6, lg: 6, height: 450, cardVariant: 'outlined'}
94
+ layout: {cols: 12, sm: 12, md: 6, lg: 6, height: 450, cardVariant: 'elevated'}
93
95
  });
94
96
  editingCardIndex.value = valueModel.value.cards.length - 1;
95
97
  };
96
98
 
99
+
100
+
97
101
  const onSaveCard = () => {
98
102
  emit("dashboardUpdated")
99
103
  editingCardIndex.value = null;
100
104
  };
101
105
 
106
+ const debouncedSave = debounce(onSaveCard, 500)
107
+
102
108
  const onCancelCard = () => {
103
109
  editingCardIndex.value = null;
104
110
  };
@@ -108,10 +114,16 @@ const emit = defineEmits(["dashboardUpdated"])
108
114
  </script>
109
115
 
110
116
  <template>
111
- <v-card v-if="valueModel" class="mt-3 valueModel-config-wrapper" variant="flat" color="transparent">
117
+ <v-card v-if="valueModel" class="mt-3 valueModel-config-wrapper" variant="flat" >
112
118
  <v-card-title class="px-0 d-flex align-center">
113
- <span class="text-h5 font-weight-bold">{{ valueModel.title || 'Configuración de Dashboard' }}</span>
119
+ <v-text-field
120
+ variant="solo-filled"
121
+ class="font-weight-semibold"
122
+ v-model="valueModel.title"
123
+ @update:modelValue="debouncedSave"
124
+ ></v-text-field>
114
125
  <v-spacer></v-spacer>
126
+ <slot name="buttons"></slot>
115
127
  <v-btn color="primary" prepend-icon="mdi-plus" @click="addNewCard" elevation="2">Añadir Tarjeta</v-btn>
116
128
  </v-card-title>
117
129
 
@@ -11,8 +11,8 @@ const {dashboard} = defineProps({
11
11
  </script>
12
12
 
13
13
  <template>
14
- <v-card v-if="dashboard" class="mt-3" >
15
- <v-card-title>{{dashboard.title}}</v-card-title>
14
+ <v-card v-if="dashboard" class="mt-3" variant="flat" >
15
+ <v-card-title style="font-size: 2rem">{{dashboard.title}}</v-card-title>
16
16
  <v-card-text>
17
17
  <v-row>
18
18
  <v-col v-for="(card,i) in dashboard.cards" :key="i"
@@ -21,7 +21,7 @@ const {dashboard} = defineProps({
21
21
  :md="card?.layout?.md || 12"
22
22
  :lg="card?.layout?.lg || 12"
23
23
  >
24
- <v-card :variant="card?.layout?.cardVariant || 'outlined' " :height="card?.layout?.height || 300" style="overflow-y: auto">
24
+ <v-card :variant="card?.layout?.cardVariant || 'elevated' " hover :height="card?.layout?.height || 300" style="overflow-y: auto">
25
25
  <v-card-title>{{card?.title}}</v-card-title>
26
26
  <v-card-text >
27
27
  <paginate-card v-if="card?.type === 'paginate'" :card="card" />
@@ -90,10 +90,10 @@ const cardData = computed(() => {
90
90
  <v-col
91
91
  v-for="(card, index) in cardData"
92
92
  :key="index"
93
- cols="6"
94
- sm="4"
95
- md="3"
96
- lg="2"
93
+ cols="12"
94
+ sm="6"
95
+ md="4"
96
+ lg="4"
97
97
  class="pa-1"
98
98
  >
99
99
  <v-card
@@ -112,7 +112,7 @@ const cardData = computed(() => {
112
112
  </div>
113
113
  <v-chip
114
114
  :color="card.color"
115
- size="x-small"
115
+ size="small"
116
116
  variant="flat"
117
117
  class="mt-1"
118
118
  >
@@ -126,16 +126,31 @@ const cardData = computed(() => {
126
126
 
127
127
  <v-divider class="my-2"></v-divider>
128
128
 
129
- <div class="total-section pa-2">
130
- <v-card variant="flat" color="grey-lighten-4">
131
- <v-card-text class="pa-2 d-flex align-center justify-space-between">
132
- <span class="text-subtitle-2 font-weight-bold">Total</span>
133
- <v-chip color="primary" size="small" variant="flat">
129
+ <v-card
130
+ color="black"
131
+ variant="tonal"
132
+ class="gallery-card"
133
+ hover
134
+ >
135
+ <v-card-text class="pa-2">
136
+ <div class="d-flex flex-column align-center text-center">
137
+ <div class="card-value text-h5 font-weight-bold mb-1">
134
138
  {{ totalCount }}
139
+ </div>
140
+ <div class="card-label text-caption text-truncate" >
141
+ TOTAL
142
+ </div>
143
+ <v-chip
144
+ color="black"
145
+ size="small"
146
+ variant="flat"
147
+ class="mt-1"
148
+ >
149
+ 100%
135
150
  </v-chip>
136
- </v-card-text>
137
- </v-card>
138
- </div>
151
+ </div>
152
+ </v-card-text>
153
+ </v-card>
139
154
  </template>
140
155
  </div>
141
156
  </template>
@@ -46,11 +46,17 @@ const getPercentage = (count: number) => {
46
46
  v-slot:[`item.${field.name}`]="{ value }"
47
47
  >
48
48
  <template v-if="['ref','object'].includes(field.type) && field.refDisplay">
49
- {{value[field.refDisplay]}}
49
+ {{value ? value[field.refDisplay] : '-' }}
50
50
  </template>
51
+
51
52
  <template v-else-if="field.type === 'date'">
52
53
  {{ formatDateByUnit(value, dateFormat) }}
53
54
  </template>
55
+
56
+ <template v-else-if="field.type === 'number'">
57
+ {{ value.toLocaleString('es-ar') }}
58
+ </template>
59
+
54
60
  <template v-else>
55
61
  {{ value }}
56
62
  </template>
@@ -89,75 +89,75 @@ class DashboardCrud extends EntityCrud implements IEntityCrud {
89
89
  return [
90
90
  {name: 'identifier', type: 'string', label: 'identifier', default: ''},
91
91
  {name: 'title', type: 'string', label: 'title', default: ''},
92
- {
93
- name: 'cards',
94
- type: 'array.object',
95
- label: 'cards',
96
- default: [],
97
- objectFields: [
98
- {name: 'title', type: 'string', label: 'title', default: ''},
99
- {name: 'entity', type: 'enum', enum: this.entities, label: 'entity', default: ''},
100
- {name: 'type', type: 'enum', label: 'type', default: null, enum: ['paginate', 'groupBy']},
101
- {
102
- name: 'filters',
103
- type: 'array.object',
104
- label: 'filters',
105
- default: [],
106
- objectFields: [
107
- {name: 'field', type: 'string', label: 'field', default: ''},
108
- {name: 'operator', type: 'string', label: 'operator', default: ''},
109
- {name: 'value', type: 'string', label: 'value', default: ''}]
110
- },
111
- {
112
- name: 'layout',
113
- type: 'object',
114
- label: 'layout',
115
- default: {"cols": 12, "sm": 12, "md": 12, "lg": 12, "height": 350, "cardVariant": "elevated"},
116
- objectFields: [
117
- {name: 'cols', type: 'number', label: 'cols', default: 12, sm: 3, md: 3, lg: 3},
118
- {name: 'sm', type: 'number', label: 'sm', default: 12, sm: 3, md: 3, lg: 3},
119
- {name: 'md', type: 'number', label: 'md', default: 12, sm: 3, md: 3, lg: 3},
120
- {name: 'lg', type: 'number', label: 'lg', default: 12, sm: 3, md: 3, lg: 3},
121
- {name: 'height', type: 'number', label: 'height', default: 350},
122
- {
123
- name: 'cardVariant',
124
- type: 'enum',
125
- label: 'cardVariant',
126
- default: 'elevated',
127
- enum: ['text', 'flat', 'elevated', 'tonal', 'outlined', 'plain']
128
- }]
129
- },
130
- {
131
- name: 'groupBy',
132
- type: 'object',
133
- label: 'groupBy',
134
- default: {"fields": [], "dateFormat": "day", "render": "table"},
135
- objectFields: [{name: 'fields', type: 'array.string', label: 'fields', default: []},
136
- {
137
- name: 'dateFormat',
138
- type: 'enum',
139
- label: 'dateFormat',
140
- default: 'day',
141
- enum: ['year', 'month', 'day', 'hour', 'minute', 'second']
142
- },
143
- {
144
- name: 'render',
145
- type: 'enum',
146
- label: 'render',
147
- default: 'table',
148
- enum: ['table', 'gallery', 'pie', 'bars']
149
- }]
150
- },
151
- {
152
- name: 'paginate',
153
- type: 'object',
154
- label: 'paginate',
155
- default: {"columns": [], "orderBy": "", "order": null},
156
- objectFields: [{name: 'columns', type: 'array.string', label: 'columns', default: []},
157
- {name: 'orderBy', type: 'string', label: 'orderBy', default: ''},
158
- {name: 'order', type: 'enum', label: 'order', default: null, enum: ['asc', 'desc']}]
159
- }]
160
- }
92
+ // {
93
+ // name: 'cards',
94
+ // type: 'array.object',
95
+ // label: 'cards',
96
+ // default: [],
97
+ // objectFields: [
98
+ // {name: 'title', type: 'string', label: 'title', default: ''},
99
+ // {name: 'entity', type: 'enum', enum: this.entities, label: 'entity', default: ''},
100
+ // {name: 'type', type: 'enum', label: 'type', default: null, enum: ['paginate', 'groupBy']},
101
+ // {
102
+ // name: 'filters',
103
+ // type: 'array.object',
104
+ // label: 'filters',
105
+ // default: [],
106
+ // objectFields: [
107
+ // {name: 'field', type: 'string', label: 'field', default: ''},
108
+ // {name: 'operator', type: 'string', label: 'operator', default: ''},
109
+ // {name: 'value', type: 'string', label: 'value', default: ''}]
110
+ // },
111
+ // {
112
+ // name: 'layout',
113
+ // type: 'object',
114
+ // label: 'layout',
115
+ // default: {"cols": 12, "sm": 12, "md": 12, "lg": 12, "height": 350, "cardVariant": "elevated"},
116
+ // objectFields: [
117
+ // {name: 'cols', type: 'number', label: 'cols', default: 12, sm: 3, md: 3, lg: 3},
118
+ // {name: 'sm', type: 'number', label: 'sm', default: 12, sm: 3, md: 3, lg: 3},
119
+ // {name: 'md', type: 'number', label: 'md', default: 12, sm: 3, md: 3, lg: 3},
120
+ // {name: 'lg', type: 'number', label: 'lg', default: 12, sm: 3, md: 3, lg: 3},
121
+ // {name: 'height', type: 'number', label: 'height', default: 350},
122
+ // {
123
+ // name: 'cardVariant',
124
+ // type: 'enum',
125
+ // label: 'cardVariant',
126
+ // default: 'elevated',
127
+ // enum: ['text', 'flat', 'elevated', 'tonal', 'outlined', 'plain']
128
+ // }]
129
+ // },
130
+ // {
131
+ // name: 'groupBy',
132
+ // type: 'object',
133
+ // label: 'groupBy',
134
+ // default: {"fields": [], "dateFormat": "day", "render": "table"},
135
+ // objectFields: [{name: 'fields', type: 'array.string', label: 'fields', default: []},
136
+ // {
137
+ // name: 'dateFormat',
138
+ // type: 'enum',
139
+ // label: 'dateFormat',
140
+ // default: 'day',
141
+ // enum: ['year', 'month', 'day', 'hour', 'minute', 'second']
142
+ // },
143
+ // {
144
+ // name: 'render',
145
+ // type: 'enum',
146
+ // label: 'render',
147
+ // default: 'table',
148
+ // enum: ['table', 'gallery', 'pie', 'bars']
149
+ // }]
150
+ // },
151
+ // {
152
+ // name: 'paginate',
153
+ // type: 'object',
154
+ // label: 'paginate',
155
+ // default: {"columns": [], "orderBy": "", "order": null},
156
+ // objectFields: [{name: 'columns', type: 'array.string', label: 'columns', default: []},
157
+ // {name: 'orderBy', type: 'string', label: 'orderBy', default: ''},
158
+ // {name: 'order', type: 'enum', label: 'order', default: null, enum: ['asc', 'desc']}]
159
+ // }]
160
+ // }
161
161
  ]
162
162
  }
163
163
 
@@ -168,11 +168,11 @@ class DashboardCrud extends EntityCrud implements IEntityCrud {
168
168
  }
169
169
 
170
170
  get isViewable() {
171
- return true
171
+ return false
172
172
  }
173
173
 
174
174
  get isEditable() {
175
- return true
175
+ return false
176
176
  }
177
177
 
178
178
  get isCreatable() {
@@ -223,6 +223,25 @@ class DashboardCrud extends EntityCrud implements IEntityCrud {
223
223
  return []
224
224
  }
225
225
 
226
+ get listMode(): 'table' | 'gallery' {
227
+ return 'gallery'
228
+ }
229
+
230
+ get searchEnable(){
231
+ return false
232
+ }
233
+
234
+ get dynamicFiltersEnable(){
235
+ return false
236
+ }
237
+
238
+ get filterButtons(){
239
+ return false
240
+ }
241
+
242
+ redirectOnCreate(item: any){
243
+ return '/dashboard/config/'+item.identifier
244
+ }
226
245
 
227
246
  }
228
247
 
@@ -46,13 +46,17 @@ onMounted(() => {
46
46
 
47
47
  <template>
48
48
  <v-container fluid>
49
- <v-btn size="small" prepend-icon="mdi-view-dashboard" :href="'/dashboard/view/'+identifier">ver</v-btn>
50
49
 
51
50
  <v-skeleton-loader :loading="loading"/>
52
51
  <dashboard-config v-if="dashboardSelected"
53
52
  v-model="dashboardSelected"
54
53
  @dashboardUpdated="updateDashboard"
55
- ></dashboard-config>
54
+ >
55
+ <template v-slot:buttons>
56
+ <v-btn class="mx-3" prepend-icon="mdi-view-dashboard" :href="'/dashboard/view/'+identifier">ver</v-btn>
57
+ </template>
58
+
59
+ </dashboard-config>
56
60
 
57
61
  </v-container>
58
62
 
@@ -31,9 +31,8 @@ onMounted(() => {
31
31
  </script>
32
32
 
33
33
  <template>
34
- <v-container >
35
-
36
- <v-skeleton-loader :loading="loading" />
34
+ <v-container fluid >
35
+ <v-skeleton-loader :loading="loading" />
37
36
  <dashboard-view v-if="dashboardSelected" :dashboard="dashboardSelected"></dashboard-view>
38
37
 
39
38
  </v-container>
@@ -7,10 +7,18 @@ import type {IDashboard} from "@drax/dashboard-share";
7
7
 
8
8
  <template>
9
9
  <crud :entity="DashboardCrud.instance">
10
+
11
+ <template v-slot:item="{item}">
12
+ <v-card class="h-100 pa-2">
13
+ <v-card-title>{{item.title}}</v-card-title>
14
+ <v-card-subtitle>{{item.identifier}}</v-card-subtitle>
15
+ </v-card>
16
+ </template>
17
+
10
18
  <template v-slot:item.actions="{ item }">
11
- <v-btn class="mx-1" variant="text" color="purple" size="small" icon="mdi-view-dashboard" :href="'/dashboard/view/'+ (item as IDashboard)?.identifier">
19
+ <v-btn class="mx-1" variant="text" color="purple" icon="mdi-view-dashboard" :href="'/dashboard/view/'+ (item as IDashboard)?.identifier">
12
20
  </v-btn>
13
- <v-btn class="mx-1" variant="text" color="indigo" size="small" icon="mdi-cogs" :href="'/dashboard/config/'+ (item as IDashboard)?.identifier">
21
+ <v-btn class="mx-1" variant="text" color="indigo" icon="mdi-table-edit" :href="'/dashboard/config/'+ (item as IDashboard)?.identifier">
14
22
  </v-btn>
15
23
  </template>
16
24
  </crud>
@@ -1,181 +0,0 @@
1
- <script setup lang="ts">
2
- import type {PropType} from "vue";
3
- import {computed} from "vue";
4
- import {useDateFormat} from "@drax/common-vue"
5
- import type {IDraxDateFormatUnit} from "@drax/common-share";
6
- import type {IEntityCrudField} from "@drax/crud-share";
7
-
8
- const {formatDateByUnit} = useDateFormat()
9
-
10
- const {data, fields, dateFormat} = defineProps({
11
- data: {type: Array as PropType<any[]>, required: false},
12
- headers: {type: Array as PropType<any[]>, required: false},
13
- fields: {type: Array as PropType<IEntityCrudField[]>, required: false},
14
- dateFormat: {type: String as PropType<IDraxDateFormatUnit>, required: false, default:'day'},
15
- })
16
-
17
- // Paleta de colores para las tarjetas
18
- const colors = [
19
- 'purple', 'indigo', 'teal', 'orange', 'pink',
20
- 'cyan', 'lime', 'amber', 'deep-purple', 'light-blue',
21
- 'deep-orange', 'blue-grey', 'brown', 'red', 'green'
22
- ]
23
-
24
- // Calcular el total de todos los counts
25
- const totalCount = computed(() => {
26
- if (!data || data.length === 0) return 0
27
- return data.reduce((sum, item) => sum + (item.count || 0), 0)
28
- })
29
-
30
- // Preparar datos para las tarjetas
31
- const cardData = computed(() => {
32
- if (!data || data.length === 0) return []
33
-
34
- return data.map((item, index) => {
35
- const percentage = totalCount.value > 0 ? (item.count / totalCount.value) * 100 : 0
36
-
37
- // Obtener el label combinando todos los campos excepto count
38
- const labelParts: string[] = []
39
-
40
- // Iterar sobre las claves del item excepto count
41
- Object.keys(item).forEach(key => {
42
- if (key === 'count') return
43
-
44
- // Buscar el campo correspondiente en fields
45
- const field = fields?.find(f => f.name === key)
46
- const value = item[key]
47
-
48
- if (!field || value === null || value === undefined) return
49
-
50
- let formattedValue = ''
51
-
52
- if (['ref','object'].includes(field.type) && field.refDisplay && value) {
53
- formattedValue = value[field.refDisplay]
54
- } else if (field.type === 'date' && value) {
55
- formattedValue = formatDateByUnit(value, dateFormat)
56
- } else if (field.type === 'enum' && value) {
57
- formattedValue = value.toString()
58
- } else if (typeof value === 'object' && !Array.isArray(value)) {
59
- formattedValue = JSON.stringify(value)
60
- } else {
61
- formattedValue = value?.toString() || ''
62
- }
63
-
64
- if (formattedValue) {
65
- labelParts.push(formattedValue)
66
- }
67
- })
68
-
69
- const label = labelParts.length > 0 ? labelParts.join(' - ') : 'N/A'
70
-
71
- return {
72
- label,
73
- value: item.count || 0,
74
- percentage,
75
- color: colors[index % colors.length]
76
- }
77
- })
78
- })
79
- </script>
80
-
81
- <template>
82
- <div class="gallery-container">
83
- <div v-if="!data || data.length === 0" class="empty-state">
84
- <v-icon size="64" color="grey-lighten-1">mdi-view-grid</v-icon>
85
- <p class="text-grey-lighten-1 mt-4">No hay datos para mostrar</p>
86
- </div>
87
-
88
- <template v-else>
89
- <v-row dense class="ma-0">
90
- <v-col
91
- v-for="(card, index) in cardData"
92
- :key="index"
93
- cols="6"
94
- sm="4"
95
- md="3"
96
- lg="2"
97
- class="pa-1"
98
- >
99
- <v-card
100
- :color="card.color"
101
- variant="tonal"
102
- class="gallery-card"
103
- hover
104
- >
105
- <v-card-text class="pa-2">
106
- <div class="d-flex flex-column align-center text-center">
107
- <div class="card-value text-h5 font-weight-bold mb-1">
108
- {{ card.value }}
109
- </div>
110
- <div class="card-label text-caption text-truncate" :title="card.label">
111
- {{ card.label }}
112
- </div>
113
- <v-chip
114
- :color="card.color"
115
- size="x-small"
116
- variant="flat"
117
- class="mt-1"
118
- >
119
- {{ card.percentage.toFixed(1) }}%
120
- </v-chip>
121
- </div>
122
- </v-card-text>
123
- </v-card>
124
- </v-col>
125
- </v-row>
126
-
127
- <v-divider class="my-2"></v-divider>
128
-
129
- <div class="total-section pa-2">
130
- <v-card variant="flat" color="grey-lighten-4">
131
- <v-card-text class="pa-2 d-flex align-center justify-space-between">
132
- <span class="text-subtitle-2 font-weight-bold">Total</span>
133
- <v-chip color="primary" size="small" variant="flat">
134
- {{ totalCount }}
135
- </v-chip>
136
- </v-card-text>
137
- </v-card>
138
- </div>
139
- </template>
140
- </div>
141
- </template>
142
-
143
- <style scoped>
144
- .gallery-container {
145
- width: 100%;
146
- padding: 4px;
147
- }
148
-
149
- .empty-state {
150
- display: flex;
151
- flex-direction: column;
152
- align-items: center;
153
- justify-content: center;
154
- padding: 32px 16px;
155
- min-height: 200px;
156
- }
157
-
158
- .gallery-card {
159
- height: 100%;
160
- transition: transform 0.2s, box-shadow 0.2s;
161
- cursor: pointer;
162
- }
163
-
164
- .gallery-card:hover {
165
- transform: translateY(-2px);
166
- }
167
-
168
- .card-value {
169
- line-height: 1.2;
170
- }
171
-
172
- .card-label {
173
- width: 100%;
174
- line-height: 1.2;
175
- max-width: 100%;
176
- }
177
-
178
- .total-section {
179
- padding: 0 4px;
180
- }
181
- </style>