@drax/dashboard-vue 0.37.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 +66 -0
- package/src/combobox/DashboardCombobox.vue +41 -0
- package/src/components/DashboardView/DashboardView.vue +41 -0
- package/src/components/GroupByCard/GroupByCard.vue +57 -0
- package/src/components/GroupByCard/renders/GroupByBarsRender.vue +361 -0
- package/src/components/GroupByCard/renders/GroupByGalleryRender.vue +181 -0
- package/src/components/GroupByCard/renders/GroupByPieRender.vue +298 -0
- package/src/components/GroupByCard/renders/GroupByTableRender.vue +91 -0
- package/src/components/PaginateCard/PaginateCard.vue +35 -0
- package/src/components/PaginateCard/renders/PaginateTableRender.vue +65 -0
- package/src/composables/UseDashboardCard.ts +86 -0
- package/src/cruds/DashboardCrud.ts +230 -0
- package/src/i18n/Dashboard-i18n.ts +55 -0
- package/src/index.ts +29 -0
- package/src/pages/DashboardIdentifierPage.vue +45 -0
- package/src/pages/DashboardViewPage.vue +25 -0
- package/src/pages/crud/DashboardCrudPage.vue +19 -0
- package/src/routes/DashboardCrudRoute.ts +38 -0
- package/src/stores/UseDashboardStore.ts +25 -0
|
@@ -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, headers, 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 (field.type === 'ref' && 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>
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type {PropType} from "vue";
|
|
3
|
+
import {computed, ref, onMounted, watch} 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, headers, 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
|
+
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
|
18
|
+
|
|
19
|
+
// Paleta de colores para el gráfico
|
|
20
|
+
const colors = [
|
|
21
|
+
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF',
|
|
22
|
+
'#FF9F40', '#FF6384', '#C9CBCF', '#4BC0C0', '#FF9F40',
|
|
23
|
+
'#36A2EB', '#FFCE56', '#9966FF', '#FF6384', '#4BC0C0'
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
// Calcular el total de todos los counts
|
|
27
|
+
const totalCount = computed(() => {
|
|
28
|
+
if (!data || data.length === 0) return 0
|
|
29
|
+
return data.reduce((sum, item) => sum + (item.count || 0), 0)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Preparar datos para el gráfico
|
|
33
|
+
const chartData = computed(() => {
|
|
34
|
+
if (!data || data.length === 0) return []
|
|
35
|
+
|
|
36
|
+
return data.map((item, index) => {
|
|
37
|
+
const percentage = totalCount.value > 0 ? (item.count / totalCount.value) * 100 : 0
|
|
38
|
+
|
|
39
|
+
// Obtener el label combinando todos los campos excepto count
|
|
40
|
+
const labelParts: string[] = []
|
|
41
|
+
|
|
42
|
+
// Iterar sobre las claves del item excepto count
|
|
43
|
+
Object.keys(item).forEach(key => {
|
|
44
|
+
if (key === 'count') return
|
|
45
|
+
|
|
46
|
+
// Buscar el campo correspondiente en fields
|
|
47
|
+
const field = fields?.find(f => f.name === key)
|
|
48
|
+
const value = item[key]
|
|
49
|
+
|
|
50
|
+
if (!field || value === null || value === undefined) return
|
|
51
|
+
|
|
52
|
+
let formattedValue = ''
|
|
53
|
+
|
|
54
|
+
if (field.type === 'ref' && field.refDisplay && value) {
|
|
55
|
+
formattedValue = value[field.refDisplay]
|
|
56
|
+
} else if (field.type === 'date' && value) {
|
|
57
|
+
formattedValue = formatDateByUnit(value, dateFormat)
|
|
58
|
+
} else if (field.type === 'enum' && value) {
|
|
59
|
+
formattedValue = value.toString()
|
|
60
|
+
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
|
61
|
+
formattedValue = JSON.stringify(value)
|
|
62
|
+
} else {
|
|
63
|
+
formattedValue = value?.toString() || ''
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (formattedValue) {
|
|
67
|
+
labelParts.push(formattedValue)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const label = labelParts.length > 0 ? labelParts.join(' - ') : 'N/A'
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
label,
|
|
75
|
+
value: item.count || 0,
|
|
76
|
+
percentage,
|
|
77
|
+
color: colors[index % colors.length]
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// Dibujar el gráfico de torta
|
|
83
|
+
const drawPieChart = () => {
|
|
84
|
+
if (!canvasRef.value || chartData.value.length === 0) return
|
|
85
|
+
|
|
86
|
+
const canvas = canvasRef.value
|
|
87
|
+
const ctx = canvas.getContext('2d')
|
|
88
|
+
if (!ctx) return
|
|
89
|
+
|
|
90
|
+
// Configurar el tamaño del canvas
|
|
91
|
+
const size = Math.min(canvas.parentElement?.clientWidth || 400, 400)
|
|
92
|
+
canvas.width = size
|
|
93
|
+
canvas.height = size
|
|
94
|
+
|
|
95
|
+
const centerX = size / 2
|
|
96
|
+
const centerY = size / 2
|
|
97
|
+
const radius = (size / 2) * 0.7
|
|
98
|
+
|
|
99
|
+
// Limpiar el canvas
|
|
100
|
+
ctx.clearRect(0, 0, size, size)
|
|
101
|
+
|
|
102
|
+
// Dibujar cada segmento
|
|
103
|
+
let currentAngle = -Math.PI / 2 // Comenzar desde arriba
|
|
104
|
+
|
|
105
|
+
chartData.value.forEach((segment) => {
|
|
106
|
+
const sliceAngle = (segment.percentage / 100) * 2 * Math.PI
|
|
107
|
+
|
|
108
|
+
// Dibujar el segmento
|
|
109
|
+
ctx.beginPath()
|
|
110
|
+
ctx.moveTo(centerX, centerY)
|
|
111
|
+
ctx.arc(centerX, centerY, radius, currentAngle, currentAngle + sliceAngle)
|
|
112
|
+
ctx.closePath()
|
|
113
|
+
ctx.fillStyle = segment.color
|
|
114
|
+
ctx.fill()
|
|
115
|
+
|
|
116
|
+
// Dibujar borde blanco
|
|
117
|
+
ctx.strokeStyle = '#ffffff'
|
|
118
|
+
ctx.lineWidth = 2
|
|
119
|
+
ctx.stroke()
|
|
120
|
+
|
|
121
|
+
currentAngle += sliceAngle
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// Dibujar círculo blanco en el centro para efecto "donut" (opcional)
|
|
125
|
+
ctx.beginPath()
|
|
126
|
+
ctx.arc(centerX, centerY, radius * 0.5, 0, 2 * Math.PI)
|
|
127
|
+
ctx.fillStyle = '#ffffff'
|
|
128
|
+
ctx.fill()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Redibujar cuando cambien los datos
|
|
132
|
+
watch(() => data, () => {
|
|
133
|
+
setTimeout(drawPieChart, 100)
|
|
134
|
+
}, { deep: true })
|
|
135
|
+
|
|
136
|
+
onMounted(() => {
|
|
137
|
+
drawPieChart()
|
|
138
|
+
|
|
139
|
+
// Redibujar al cambiar el tamaño de la ventana
|
|
140
|
+
window.addEventListener('resize', drawPieChart)
|
|
141
|
+
})
|
|
142
|
+
</script>
|
|
143
|
+
|
|
144
|
+
<template>
|
|
145
|
+
<div class="pie-chart-container">
|
|
146
|
+
<div v-if="!data || data.length === 0" class="empty-state">
|
|
147
|
+
<v-icon size="64" color="grey-lighten-1">mdi-chart-pie</v-icon>
|
|
148
|
+
<p class="text-grey-lighten-1 mt-4">No hay datos para mostrar</p>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<template v-else>
|
|
152
|
+
<div class="chart-wrapper">
|
|
153
|
+
<canvas ref="canvasRef"></canvas>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div class="legend-container">
|
|
157
|
+
<div
|
|
158
|
+
v-for="(segment, index) in chartData"
|
|
159
|
+
:key="index"
|
|
160
|
+
class="legend-item"
|
|
161
|
+
>
|
|
162
|
+
<div class="legend-color" :style="{ backgroundColor: segment.color }"></div>
|
|
163
|
+
<div class="legend-content">
|
|
164
|
+
<div class="legend-label">{{ segment.label }}</div>
|
|
165
|
+
<div class="legend-stats">
|
|
166
|
+
<v-chip color="primary" size="x-small" variant="flat">
|
|
167
|
+
{{ segment.value }}
|
|
168
|
+
</v-chip>
|
|
169
|
+
<span class="legend-percentage">{{ segment.percentage.toFixed(1) }}%</span>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div class="total-container">
|
|
176
|
+
<v-divider class="my-1"></v-divider>
|
|
177
|
+
<div class="d-flex align-center justify-space-between">
|
|
178
|
+
<span class="text-subtitle-1 font-weight-medium ml-2">Total</span>
|
|
179
|
+
<v-chip color="primary" variant="flat">
|
|
180
|
+
{{ totalCount }}
|
|
181
|
+
</v-chip>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</template>
|
|
185
|
+
</div>
|
|
186
|
+
</template>
|
|
187
|
+
|
|
188
|
+
<style scoped>
|
|
189
|
+
.pie-chart-container {
|
|
190
|
+
padding: 2px;
|
|
191
|
+
width: 100%;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.empty-state {
|
|
195
|
+
display: flex;
|
|
196
|
+
flex-direction: column;
|
|
197
|
+
align-items: center;
|
|
198
|
+
justify-content: center;
|
|
199
|
+
padding: 4px 2px;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.chart-wrapper {
|
|
203
|
+
display: flex;
|
|
204
|
+
justify-content: center;
|
|
205
|
+
align-items: center;
|
|
206
|
+
margin-bottom: 2px;
|
|
207
|
+
padding: 2px;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.chart-wrapper canvas {
|
|
211
|
+
max-width: 200px;
|
|
212
|
+
max-height: 180px;
|
|
213
|
+
height: auto;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.legend-container {
|
|
217
|
+
display: flex;
|
|
218
|
+
flex-direction: column;
|
|
219
|
+
gap: 6px;
|
|
220
|
+
max-height: 200px;
|
|
221
|
+
overflow-y: auto;
|
|
222
|
+
padding: 2px;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.legend-item {
|
|
226
|
+
display: flex;
|
|
227
|
+
align-items: center;
|
|
228
|
+
gap: 8px;
|
|
229
|
+
padding: 2px 4px;
|
|
230
|
+
border-radius: 6px;
|
|
231
|
+
transition: background-color 0.2s;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.legend-item:hover {
|
|
235
|
+
background-color: rgba(0, 0, 0, 0.04);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.legend-color {
|
|
239
|
+
width: 12px;
|
|
240
|
+
height: 12px;
|
|
241
|
+
border-radius: 3px;
|
|
242
|
+
flex-shrink: 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.legend-content {
|
|
246
|
+
flex: 1;
|
|
247
|
+
display: flex;
|
|
248
|
+
justify-content: space-between;
|
|
249
|
+
align-items: center;
|
|
250
|
+
gap: 6px;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.legend-label {
|
|
254
|
+
font-size: 13px;
|
|
255
|
+
font-weight: 500;
|
|
256
|
+
flex: 1;
|
|
257
|
+
overflow: hidden;
|
|
258
|
+
text-overflow: ellipsis;
|
|
259
|
+
white-space: nowrap;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.legend-stats {
|
|
263
|
+
display: flex;
|
|
264
|
+
align-items: center;
|
|
265
|
+
gap: 6px;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.legend-percentage {
|
|
269
|
+
font-size: 11px;
|
|
270
|
+
color: rgba(0, 0, 0, 0.6);
|
|
271
|
+
font-weight: 500;
|
|
272
|
+
min-width: 40px;
|
|
273
|
+
text-align: right;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.total-container {
|
|
277
|
+
margin-top: 8px;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/* Scrollbar personalizado para la leyenda */
|
|
281
|
+
.legend-container::-webkit-scrollbar {
|
|
282
|
+
width: 4px;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.legend-container::-webkit-scrollbar-track {
|
|
286
|
+
background: rgba(0, 0, 0, 0.05);
|
|
287
|
+
border-radius: 2px;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.legend-container::-webkit-scrollbar-thumb {
|
|
291
|
+
background: rgba(0, 0, 0, 0.2);
|
|
292
|
+
border-radius: 2px;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.legend-container::-webkit-scrollbar-thumb:hover {
|
|
296
|
+
background: rgba(0, 0, 0, 0.3);
|
|
297
|
+
}
|
|
298
|
+
</style>
|
|
@@ -0,0 +1,91 @@
|
|
|
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, headers, 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
|
+
// Calcular el total de todos los counts
|
|
18
|
+
const totalCount = computed(() => {
|
|
19
|
+
if (!data || data.length === 0) return 0
|
|
20
|
+
return data.reduce((sum, item) => sum + (item.count || 0), 0)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// Calcular el porcentaje para cada item
|
|
24
|
+
const getPercentage = (count: number) => {
|
|
25
|
+
if (totalCount.value === 0) return 0
|
|
26
|
+
return ((count / totalCount.value) * 100).toFixed(1)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<div>
|
|
33
|
+
<v-data-table
|
|
34
|
+
:headers="headers"
|
|
35
|
+
:items="data"
|
|
36
|
+
density="compact"
|
|
37
|
+
:items-per-page="-1"
|
|
38
|
+
hide-default-footer
|
|
39
|
+
>
|
|
40
|
+
<template v-slot:bottom></template>
|
|
41
|
+
|
|
42
|
+
<!-- Slot para personalizar la visualización de cada campo -->
|
|
43
|
+
<template
|
|
44
|
+
v-for="field in fields"
|
|
45
|
+
:key="field.name"
|
|
46
|
+
v-slot:[`item.${field.name}`]="{ value }"
|
|
47
|
+
>
|
|
48
|
+
<template v-if="field.type === 'ref' && field.refDisplay">
|
|
49
|
+
{{value[field.refDisplay]}}
|
|
50
|
+
</template>
|
|
51
|
+
<template v-else-if="field.type === 'date'">
|
|
52
|
+
{{ formatDateByUnit(value, dateFormat) }}
|
|
53
|
+
</template>
|
|
54
|
+
<template v-else>
|
|
55
|
+
{{ value }}
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
</template>
|
|
59
|
+
|
|
60
|
+
<!-- Formato especial para el count con porcentaje -->
|
|
61
|
+
<template v-slot:item.count="{ item }">
|
|
62
|
+
<div class="d-flex align-center justify-end ga-2">
|
|
63
|
+
<v-chip color="primary" size="small" variant="flat">
|
|
64
|
+
{{ item.count }}
|
|
65
|
+
</v-chip>
|
|
66
|
+
<span class="text-caption text-grey text-left percentage-text">
|
|
67
|
+
({{ getPercentage(item.count) }}%)
|
|
68
|
+
</span>
|
|
69
|
+
</div>
|
|
70
|
+
</template>
|
|
71
|
+
</v-data-table>
|
|
72
|
+
|
|
73
|
+
<!-- Fila de totales -->
|
|
74
|
+
<v-divider class="my-2"></v-divider>
|
|
75
|
+
<div class="d-flex align-center justify-space-between px-4 py-2">
|
|
76
|
+
<span class="text-subtitle-1 font-weight-medium">Total</span>
|
|
77
|
+
<v-chip color="primary" variant="flat">
|
|
78
|
+
{{ totalCount }}
|
|
79
|
+
</v-chip>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
</template>
|
|
84
|
+
|
|
85
|
+
<style scoped>
|
|
86
|
+
.percentage-text {
|
|
87
|
+
display: inline-block;
|
|
88
|
+
min-width: 45px;
|
|
89
|
+
text-align: right;
|
|
90
|
+
}
|
|
91
|
+
</style>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type {PropType} from "vue";
|
|
3
|
+
import type {IDashboardCard} from "@drax/dashboard-share";
|
|
4
|
+
import {useDashboardCard} from "../../composables/UseDashboardCard";
|
|
5
|
+
import PaginateTableRender from "./renders/PaginateTableRender.vue";
|
|
6
|
+
import {ref, onMounted, defineProps } from "vue";
|
|
7
|
+
import type {IDraxPaginateResult} from "@drax/crud-share";
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
const {card} = defineProps({
|
|
11
|
+
card: {type: Object as PropType<IDashboardCard>, required: true},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const {fetchPaginateData, cardEntityFields, paginateHeaders} = useDashboardCard(card)
|
|
15
|
+
|
|
16
|
+
const data = ref<IDraxPaginateResult<any>>()
|
|
17
|
+
|
|
18
|
+
onMounted(async ()=> {
|
|
19
|
+
data.value = await fetchPaginateData()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<paginate-table-render v-if="card?.groupBy?.render === 'table'"
|
|
26
|
+
:data="data"
|
|
27
|
+
:fields="cardEntityFields"
|
|
28
|
+
:headers="paginateHeaders"
|
|
29
|
+
/>
|
|
30
|
+
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<style scoped>
|
|
34
|
+
|
|
35
|
+
</style>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type {PropType} from "vue";
|
|
3
|
+
import {useDateFormat} from "@drax/common-vue"
|
|
4
|
+
import type {IDraxDateFormatUnit} from "@drax/common-share";
|
|
5
|
+
import type {IEntityCrudField} from "@drax/crud-share";
|
|
6
|
+
import type {IDraxPaginateResult} from "@drax/crud-share";
|
|
7
|
+
|
|
8
|
+
const {formatDateByUnit} = useDateFormat()
|
|
9
|
+
|
|
10
|
+
const {data, headers, fields, dateFormat} = defineProps({
|
|
11
|
+
data: {type: Object as PropType<IDraxPaginateResult<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
|
+
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<div v-if="data">
|
|
22
|
+
<v-data-table-server
|
|
23
|
+
:headers="headers"
|
|
24
|
+
:items="data?.items"
|
|
25
|
+
density="compact"
|
|
26
|
+
v-model:page="data.page"
|
|
27
|
+
v-model:items-per-page="data.limit"
|
|
28
|
+
:items-length="data.total"
|
|
29
|
+
striped="odd"
|
|
30
|
+
hide-default-footer
|
|
31
|
+
>
|
|
32
|
+
|
|
33
|
+
<!-- Slot para personalizar la visualización de cada campo -->
|
|
34
|
+
<template
|
|
35
|
+
v-for="field in fields"
|
|
36
|
+
:key="field.name"
|
|
37
|
+
v-slot:[`item.${field.name}`]="{ value }"
|
|
38
|
+
>
|
|
39
|
+
<template v-if="field.type === 'ref' && field.refDisplay">
|
|
40
|
+
{{value[field.refDisplay]}}
|
|
41
|
+
</template>
|
|
42
|
+
<template v-else-if="field.type === 'date'">
|
|
43
|
+
{{ formatDateByUnit(value, dateFormat) }}
|
|
44
|
+
</template>
|
|
45
|
+
<template v-else>
|
|
46
|
+
{{ value }}
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
</v-data-table-server>
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
</template>
|
|
58
|
+
|
|
59
|
+
<style scoped>
|
|
60
|
+
.percentage-text {
|
|
61
|
+
display: inline-block;
|
|
62
|
+
min-width: 45px;
|
|
63
|
+
text-align: right;
|
|
64
|
+
}
|
|
65
|
+
</style>
|