@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.
- package/package.json +4 -4
- package/src/components/DashboardConfig/DashboardCardEditor.vue +411 -0
- package/src/components/DashboardConfig/DashboardConfig.vue +253 -0
- package/src/components/DashboardView/DashboardView.vue +3 -3
- package/src/components/GroupByCard/GroupByCard.vue +1 -1
- package/src/components/GroupByCard/renders/GroupByBarsRender.vue +34 -21
- package/src/components/GroupByCard/renders/GroupByGalleryRender.vue +28 -13
- package/src/components/GroupByCard/renders/GroupByGalleryRenderbkp.vue +181 -0
- package/src/components/GroupByCard/renders/GroupByPieRender.vue +104 -138
- package/src/components/PaginateCard/PaginateCard.vue +2 -3
- package/src/cruds/DashboardCrud.ts +90 -71
- package/src/pages/DashboardConfigPage.vue +67 -0
- package/src/pages/DashboardIdentifierPage.vue +2 -3
- package/src/pages/crud/DashboardCrudPage.vue +11 -1
- package/src/routes/DashboardCrudRoute.ts +11 -1
|
@@ -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 || '
|
|
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
|
|
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:
|
|
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
|
|
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 +
|
|
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 = '
|
|
191
|
+
ctx.font = '12px sans-serif'
|
|
181
192
|
ctx.textAlign = 'right'
|
|
182
193
|
|
|
183
194
|
// Truncar label si es muy largo
|
|
184
|
-
const maxLabelLength =
|
|
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
|
-
|
|
321
|
-
|
|
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-
|
|
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="
|
|
94
|
-
sm="
|
|
95
|
-
md="
|
|
96
|
-
lg="
|
|
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="
|
|
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
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
</
|
|
137
|
-
</v-card>
|
|
138
|
-
</
|
|
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>
|