@drax/dashboard-vue 2.6.0 → 2.8.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.
@@ -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" />
@@ -6,7 +6,7 @@ import GroupByTableRender from "./renders/GroupByTableRender.vue";
6
6
  import GroupByPieRender from "./renders/GroupByPieRender.vue";
7
7
  import GroupByBarsRender from "./renders/GroupByBarsRender.vue";
8
8
  import GroupByGalleryRender from "./renders/GroupByGalleryRender.vue";
9
- import {ref, onMounted, defineProps } from "vue";
9
+ import {ref, onMounted} from "vue";
10
10
 
11
11
 
12
12
  const {card} = defineProps({
@@ -100,7 +100,7 @@ const drawBarChart = () => {
100
100
  canvas.width = containerWidth
101
101
  canvas.height = containerHeight
102
102
 
103
- const padding = { top: 20, right: 20, bottom: 60, left: 60 }
103
+ const padding = { top: 40, right: 20, bottom: 80, left: 60 }
104
104
  const chartWidth = containerWidth - padding.left - padding.right
105
105
  const chartHeight = containerHeight - padding.top - padding.bottom
106
106
 
@@ -162,26 +162,37 @@ const drawBarChart = () => {
162
162
  ctx.lineWidth = 2
163
163
  ctx.strokeRect(x, y, barWidth - barPadding, barHeight)
164
164
 
165
- // Dibujar el valor encima de la barra
165
+ // Dibujar el valor y porcentaje encima de la barra
166
166
  ctx.fillStyle = '#333'
167
- ctx.font = 'bold 11px sans-serif'
167
+ ctx.font = 'bold 12px sans-serif'
168
168
  ctx.textAlign = 'center'
169
+
170
+ // Valor
169
171
  ctx.fillText(
170
172
  segment.value.toString(),
171
173
  x + (barWidth - barPadding) / 2,
174
+ y - 18
175
+ )
176
+
177
+ // Porcentaje
178
+ ctx.font = '11px sans-serif'
179
+ ctx.fillStyle = '#666'
180
+ ctx.fillText(
181
+ `(${segment.percentage.toFixed(1)}%)`,
182
+ x + (barWidth - barPadding) / 2,
172
183
  y - 5
173
184
  )
174
185
 
175
186
  // Dibujar etiqueta del eje X (rotada si es necesario)
176
187
  ctx.save()
177
- ctx.translate(x + (barWidth - barPadding) / 2, padding.top + chartHeight + 10)
188
+ ctx.translate(x + (barWidth - barPadding) / 2, padding.top + chartHeight + 12)
178
189
  ctx.rotate(-Math.PI / 4)
179
190
  ctx.fillStyle = '#666'
180
- ctx.font = '10px sans-serif'
191
+ ctx.font = '12px sans-serif'
181
192
  ctx.textAlign = 'right'
182
193
 
183
194
  // Truncar label si es muy largo
184
- const maxLabelLength = 15
195
+ const maxLabelLength = 20
185
196
  const label = segment.label.length > maxLabelLength
186
197
  ? segment.label.substring(0, maxLabelLength) + '...'
187
198
  : segment.label
@@ -212,6 +223,16 @@ onMounted(() => {
212
223
  </div>
213
224
 
214
225
  <template v-else>
226
+ <div v-if="showLegend" class="total-container-top">
227
+ <div class="d-flex align-center justify-space-between">
228
+ <span class="text-h6 font-weight-bold">Total</span>
229
+ <v-chip color="primary" size="large" variant="flat">
230
+ {{ totalCount }}
231
+ </v-chip>
232
+ </div>
233
+ <v-divider class="my-2"></v-divider>
234
+ </div>
235
+
215
236
  <div class="chart-wrapper">
216
237
  <canvas ref="canvasRef"></canvas>
217
238
  </div>
@@ -234,16 +255,6 @@ onMounted(() => {
234
255
  </div>
235
256
  </div>
236
257
  </div>
237
-
238
- <div v-if="showLegend" class="total-container">
239
- <v-divider class="my-1"></v-divider>
240
- <div class="d-flex align-center justify-space-between">
241
- <span class="text-subtitle-1 font-weight-medium ml-2">Total</span>
242
- <v-chip color="primary" variant="flat">
243
- {{ totalCount }}
244
- </v-chip>
245
- </div>
246
- </div>
247
258
  </template>
248
259
  </div>
249
260
  </template>
@@ -317,9 +328,8 @@ onMounted(() => {
317
328
  font-size: 13px;
318
329
  font-weight: 500;
319
330
  flex: 1;
320
- overflow: hidden;
321
- text-overflow: ellipsis;
322
- white-space: nowrap;
331
+ word-break: break-word;
332
+ line-height: 1.3;
323
333
  }
324
334
 
325
335
  .legend-stats {
@@ -336,8 +346,11 @@ onMounted(() => {
336
346
  text-align: right;
337
347
  }
338
348
 
339
- .total-container {
340
- margin-top: 8px;
349
+ .total-container-top {
350
+ margin-bottom: 12px;
351
+ padding: 8px 12px;
352
+ background-color: rgba(0, 0, 0, 0.02);
353
+ border-radius: 8px;
341
354
  }
342
355
 
343
356
  /* Scrollbar personalizado para la leyenda */
@@ -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>
@@ -0,0 +1,181 @@
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>