@drax/crud-vue 0.35.0 → 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 +6 -6
- package/src/EntityCrud.ts +15 -3
- package/src/components/CrudActiveFilters.vue +176 -0
- package/src/components/CrudFilters.vue +5 -32
- package/src/components/CrudList.vue +19 -16
- package/src/components/CrudRefDisplay.vue +47 -0
- package/src/components/buttons/CrudColumnsButton.vue +82 -0
- package/src/components/buttons/CrudGroupByButton.vue +203 -0
- package/src/composables/UseCrudColumns.ts +174 -0
- package/src/composables/UseCrudGroupBy.ts +91 -0
- package/src/composables/useCrudRefDisplay.ts +23 -0
- package/src/composables/useFilterIcon.ts +36 -0
- package/src/stores/UseCrudStore.ts +12 -4
- package/src/stores/UseGroupByStore.ts +105 -0
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.37.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./src/index.ts",
|
|
9
9
|
"module": "./src/index.ts",
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
"format": "prettier --write src/"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@drax/common-front": "^0.
|
|
28
|
-
"@drax/crud-front": "^0.
|
|
29
|
-
"@drax/crud-share": "^0.
|
|
30
|
-
"@drax/media-vue": "^0.
|
|
27
|
+
"@drax/common-front": "^0.37.0",
|
|
28
|
+
"@drax/crud-front": "^0.37.0",
|
|
29
|
+
"@drax/crud-share": "^0.37.0",
|
|
30
|
+
"@drax/media-vue": "^0.37.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"pinia": "^2.2.2",
|
|
@@ -64,5 +64,5 @@
|
|
|
64
64
|
"vue-tsc": "^2.1.10",
|
|
65
65
|
"vuetify": "^3.8.2"
|
|
66
66
|
},
|
|
67
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "0bca48d4b686fd9536a78d84f5befe6801238000"
|
|
68
68
|
}
|
package/src/EntityCrud.ts
CHANGED
|
@@ -16,13 +16,17 @@ class EntityCrud implements IEntityCrud {
|
|
|
16
16
|
throw new Error('EntityCrud instance not found')
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
20
19
|
get headers(): IEntityCrudHeader[] {
|
|
21
20
|
return [
|
|
22
21
|
{title: 'ID', key: '_id'},
|
|
23
22
|
]
|
|
24
23
|
}
|
|
25
24
|
|
|
25
|
+
get selectedHeaders(): string[] {
|
|
26
|
+
// Retrocompatibilidad: si no se define, retorna todas las keys de headers
|
|
27
|
+
return this.headers.map(header => header.key)
|
|
28
|
+
}
|
|
29
|
+
|
|
26
30
|
get actionHeaders(): IEntityCrudHeader[] {
|
|
27
31
|
return [
|
|
28
32
|
{
|
|
@@ -54,11 +58,11 @@ class EntityCrud implements IEntityCrud {
|
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
get createFields() {
|
|
57
|
-
return this.fields
|
|
61
|
+
return this.fields.filter(field => !['_id','createdAt','updatedAt'].includes(field.name))
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
get updateFields() {
|
|
61
|
-
return this.fields
|
|
65
|
+
return this.fields.filter(field => !['_id','createdAt','updatedAt'].includes(field.name))
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
get deleteFields() {
|
|
@@ -190,6 +194,14 @@ class EntityCrud implements IEntityCrud {
|
|
|
190
194
|
return ['CSV', 'JSON']
|
|
191
195
|
}
|
|
192
196
|
|
|
197
|
+
get isColumnSelectable() {
|
|
198
|
+
return true
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
get isGroupable() {
|
|
202
|
+
return false
|
|
203
|
+
}
|
|
204
|
+
|
|
193
205
|
get dialogFullscreen() {
|
|
194
206
|
return false
|
|
195
207
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, watch, onMounted, type PropType } from 'vue'
|
|
3
|
+
import type { IEntityCrud, IEntityCrudFilter } from '@drax/crud-share'
|
|
4
|
+
import { useCrudStore } from '../stores/UseCrudStore'
|
|
5
|
+
import { useI18n } from 'vue-i18n'
|
|
6
|
+
import { useFilterIcon } from '../composables/useFilterIcon'
|
|
7
|
+
import { useCrudRefDisplay } from '../composables/useCrudRefDisplay'
|
|
8
|
+
import dayjs from "dayjs";
|
|
9
|
+
import CrudRefDisplay from "./CrudRefDisplay.vue";
|
|
10
|
+
|
|
11
|
+
const { t, d } = useI18n()
|
|
12
|
+
const store = useCrudStore()
|
|
13
|
+
const { filterIcon } = useFilterIcon()
|
|
14
|
+
const { refDisplay } = useCrudRefDisplay()
|
|
15
|
+
|
|
16
|
+
const props = defineProps({
|
|
17
|
+
entity: {
|
|
18
|
+
type: Object as PropType<IEntityCrud>,
|
|
19
|
+
required: true
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const activeFilters = computed<any[]>(() => {
|
|
24
|
+
return store.filters
|
|
25
|
+
.map((filter:any, index: any) => {
|
|
26
|
+
const filterDef = props.entity.filters[index]
|
|
27
|
+
if (!filterDef) return null
|
|
28
|
+
|
|
29
|
+
// Solo mostrar si tiene valor
|
|
30
|
+
if (filter.value === null || filter.value === undefined || filter.value === '') {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Para arrays vacíos
|
|
35
|
+
if (Array.isArray(filter.value) && filter.value.length === 0) {
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
...filterDef,
|
|
41
|
+
value: filter.value,
|
|
42
|
+
index
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
.filter((f: any) => f !== null)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const getFilterLabel = (filter: any) => {
|
|
49
|
+
const label = t(`${props.entity.name.toLowerCase()}.field.${filter.label}`, filter.label)
|
|
50
|
+
return label
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
const getFilterValue = (filter: any) => {
|
|
56
|
+
switch (filter.type) {
|
|
57
|
+
case 'date':
|
|
58
|
+
return dayjs(filter.value).format('YYYY-MM-DD')
|
|
59
|
+
|
|
60
|
+
case 'boolean':
|
|
61
|
+
return filter.value ? t('common.yes') : t('common.no')
|
|
62
|
+
|
|
63
|
+
case 'ref':
|
|
64
|
+
return filter.value
|
|
65
|
+
|
|
66
|
+
case 'array.ref':
|
|
67
|
+
if (Array.isArray(filter.value)) {
|
|
68
|
+
return filter.value.map((v: any) => v[filter.refDisplay] || v).join(', ')
|
|
69
|
+
}
|
|
70
|
+
return filter.value
|
|
71
|
+
|
|
72
|
+
case 'enum':
|
|
73
|
+
return filter.value
|
|
74
|
+
|
|
75
|
+
case 'array.enum':
|
|
76
|
+
if (Array.isArray(filter.value)) {
|
|
77
|
+
return filter.value.join(', ')
|
|
78
|
+
}
|
|
79
|
+
return filter.value
|
|
80
|
+
|
|
81
|
+
case 'number':
|
|
82
|
+
return filter.value.toString()
|
|
83
|
+
|
|
84
|
+
case 'string':
|
|
85
|
+
default:
|
|
86
|
+
return filter.value
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// watch(activeFilters, async (filters) => {
|
|
91
|
+
// for (const filter of filters) {
|
|
92
|
+
// filter.display = await getFilterValue(filter)
|
|
93
|
+
// }
|
|
94
|
+
// }, { immediate: true, deep: true })
|
|
95
|
+
//
|
|
96
|
+
// const renegerateDisplay = async () => {
|
|
97
|
+
// for (const filter of activeFilters.value) {
|
|
98
|
+
// filter.display = await getFilterValue(filter)
|
|
99
|
+
// }
|
|
100
|
+
// }
|
|
101
|
+
//
|
|
102
|
+
// onMounted(() => {
|
|
103
|
+
// renegerateDisplay()
|
|
104
|
+
// })
|
|
105
|
+
|
|
106
|
+
const removeFilter = (index: number) => {
|
|
107
|
+
const filter = store.filters[index]
|
|
108
|
+
const filterDef = props.entity.filters[index]
|
|
109
|
+
|
|
110
|
+
// Resetear al valor por defecto
|
|
111
|
+
filter.value = filterDef.default
|
|
112
|
+
|
|
113
|
+
// Emitir evento para aplicar filtros
|
|
114
|
+
emit('filterRemoved')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const clearAllFilters = () => {
|
|
118
|
+
store.filters.forEach((filter: any, index: any) => {
|
|
119
|
+
const filterDef = props.entity.filters[index]
|
|
120
|
+
filter.value = filterDef.default
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
emit('filtersCleared')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const emit = defineEmits(['filterRemoved', 'filtersCleared'])
|
|
127
|
+
</script>
|
|
128
|
+
|
|
129
|
+
<template>
|
|
130
|
+
<v-card v-if="activeFilters.length > 0" flat class="mb-2">
|
|
131
|
+
<v-card-text class="py-2">
|
|
132
|
+
<div class="d-flex align-center flex-wrap ga-2">
|
|
133
|
+
<span class="text-caption text-medium-emphasis">
|
|
134
|
+
<v-icon size="x-small">mdi-filter</v-icon> {{ t('crud.activeFilters') }}:
|
|
135
|
+
</span>
|
|
136
|
+
|
|
137
|
+
<v-chip
|
|
138
|
+
v-for="filter in activeFilters"
|
|
139
|
+
:key="filter.index"
|
|
140
|
+
closable
|
|
141
|
+
size="small"
|
|
142
|
+
color="primary"
|
|
143
|
+
variant="tonal"
|
|
144
|
+
@click:close="removeFilter(filter.index)"
|
|
145
|
+
>
|
|
146
|
+
|
|
147
|
+
<span class="font-weight-medium">{{ getFilterLabel(filter) }}</span>
|
|
148
|
+
<v-icon :icon="filterIcon(filter)" size="x-small" class="mx-1" />
|
|
149
|
+
<!-- <span>{{ getFilterValue(filter) }}</span>-->
|
|
150
|
+
<span v-if="['ref','array.ref'].includes(filter.type)">
|
|
151
|
+
<crud-ref-display
|
|
152
|
+
:ref-display="filter.refDisplay"
|
|
153
|
+
:value="filter.value"
|
|
154
|
+
:entity="entity.getRef(filter.ref)" />
|
|
155
|
+
</span>
|
|
156
|
+
<span v-else>{{ getFilterValue(filter) }}</span>
|
|
157
|
+
|
|
158
|
+
<v-tooltip
|
|
159
|
+
v-if="filter.endOfDay"
|
|
160
|
+
activator="parent"
|
|
161
|
+
location="top"
|
|
162
|
+
>
|
|
163
|
+
{{ t('crud.endOfDayFilter') }}
|
|
164
|
+
</v-tooltip>
|
|
165
|
+
</v-chip>
|
|
166
|
+
|
|
167
|
+
</div>
|
|
168
|
+
</v-card-text>
|
|
169
|
+
</v-card>
|
|
170
|
+
</template>
|
|
171
|
+
|
|
172
|
+
<style scoped>
|
|
173
|
+
.v-chip {
|
|
174
|
+
max-width: 300px;
|
|
175
|
+
}
|
|
176
|
+
</style>
|
|
@@ -4,10 +4,12 @@ import CrudFormField from "./CrudFormField.vue";
|
|
|
4
4
|
import type {IEntityCrud, IEntityCrudFilter} from "@drax/crud-share";
|
|
5
5
|
import {useI18n} from "vue-i18n";
|
|
6
6
|
import {useAuth} from "@drax/identity-vue";
|
|
7
|
+
import {useFilterIcon} from "../composables/useFilterIcon";
|
|
7
8
|
|
|
8
9
|
const {t} = useI18n()
|
|
9
10
|
const valueModel = defineModel({type: [Object]})
|
|
10
11
|
const {hasPermission} = useAuth()
|
|
12
|
+
const {filterIcon} = useFilterIcon()
|
|
11
13
|
|
|
12
14
|
const {entity, actionButtons} = defineProps({
|
|
13
15
|
entity: {type: Object as PropType<IEntityCrud>, required: true},
|
|
@@ -18,39 +20,11 @@ const aFields = computed(() => {
|
|
|
18
20
|
return entity.filters.filter((field:IEntityCrudFilter) => !field.permission || hasPermission(field.permission))
|
|
19
21
|
})
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
return (field: IEntityCrudFilter) => {
|
|
23
|
-
switch(field.operator){
|
|
24
|
-
case 'eq':
|
|
25
|
-
return 'mdi-equal'
|
|
26
|
-
case 'ne':
|
|
27
|
-
return 'mdi-not-equal'
|
|
28
|
-
case 'gt':
|
|
29
|
-
return 'mdi-greater-than'
|
|
30
|
-
case 'gte':
|
|
31
|
-
return 'mdi-greater-than-or-equal'
|
|
32
|
-
case 'lt':
|
|
33
|
-
return 'mdi-less-than'
|
|
34
|
-
case 'lte':
|
|
35
|
-
return 'mdi-less-than-or-equal'
|
|
36
|
-
case 'in':
|
|
37
|
-
return 'mdi-code-array'
|
|
38
|
-
case 'nin':
|
|
39
|
-
return 'mdi-not-equal'
|
|
40
|
-
case 'like':
|
|
41
|
-
return 'mdi-contain'
|
|
42
|
-
default:
|
|
43
|
-
return 'eq'
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
function filter() {
|
|
23
|
+
function filter() {
|
|
50
24
|
emit('applyFilter')
|
|
51
25
|
}
|
|
52
26
|
|
|
53
|
-
|
|
27
|
+
function clear() {
|
|
54
28
|
emit('clearFilter')
|
|
55
29
|
}
|
|
56
30
|
|
|
@@ -60,7 +34,6 @@ function onUpdateValue(){
|
|
|
60
34
|
}
|
|
61
35
|
}
|
|
62
36
|
|
|
63
|
-
|
|
64
37
|
const emit = defineEmits(['applyFilter', 'clearFilter'])
|
|
65
38
|
|
|
66
39
|
</script>
|
|
@@ -85,7 +58,7 @@ const emit = defineEmits(['applyFilter', 'clearFilter'])
|
|
|
85
58
|
:clearable="true"
|
|
86
59
|
density="compact"
|
|
87
60
|
variant="outlined"
|
|
88
|
-
:prepend-inner-icon="
|
|
61
|
+
:prepend-inner-icon="filterIcon(filter)"
|
|
89
62
|
hide-details disable-rules
|
|
90
63
|
@updateValue="onUpdateValue"
|
|
91
64
|
/>
|
|
@@ -9,18 +9,19 @@ import CrudCreateButton from "./buttons/CrudCreateButton.vue";
|
|
|
9
9
|
import CrudUpdateButton from "./buttons/CrudUpdateButton.vue";
|
|
10
10
|
import CrudDeleteButton from "./buttons/CrudDeleteButton.vue";
|
|
11
11
|
import CrudViewButton from "./buttons/CrudViewButton.vue";
|
|
12
|
+
import CrudGroupByButton from "./buttons/CrudGroupByButton.vue";
|
|
13
|
+
import CrudColumnsButton from "./buttons/CrudColumnsButton.vue";
|
|
12
14
|
import CrudExportList from "./CrudExportList.vue";
|
|
13
15
|
import type {IEntityCrud} from "@drax/crud-share";
|
|
14
16
|
import {useI18n} from "vue-i18n";
|
|
15
|
-
import type {IEntityCrudHeader} from "@drax/crud-share";
|
|
16
17
|
import CrudFilters from "./CrudFilters.vue";
|
|
18
|
+
import { useCrudColumns } from "../composables/UseCrudColumns";
|
|
17
19
|
|
|
18
20
|
const {t, te} = useI18n()
|
|
19
21
|
const {hasPermission} = useAuth()
|
|
20
22
|
|
|
21
23
|
const {entity} = defineProps({
|
|
22
24
|
entity: {type: Object as PropType<IEntityCrud>, required: true},
|
|
23
|
-
|
|
24
25
|
})
|
|
25
26
|
|
|
26
27
|
const {
|
|
@@ -28,19 +29,8 @@ const {
|
|
|
28
29
|
doPaginate, filters, applyFilters, clearFilters
|
|
29
30
|
} = useCrud(entity)
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
title: te(header.title) ? t(header.title) : header.title,
|
|
34
|
-
}))
|
|
35
|
-
|
|
36
|
-
const tHeaders: IEntityCrudHeader[] = entity.headers
|
|
37
|
-
.filter(header => !header.permission || hasPermission(header.permission))
|
|
38
|
-
.map(header => ({
|
|
39
|
-
...header,
|
|
40
|
-
title: te(`${entity.name.toLowerCase()}.field.${header.title}`) ? t(`${entity.name.toLowerCase()}.field.${header.title}`) : header.title
|
|
41
|
-
}))
|
|
42
|
-
|
|
43
|
-
const headers: IEntityCrudHeader[] = [...tHeaders, ...actions]
|
|
32
|
+
// Usar el composable de columnas
|
|
33
|
+
const { filteredHeaders } = useCrudColumns(entity)
|
|
44
34
|
|
|
45
35
|
|
|
46
36
|
defineExpose({
|
|
@@ -61,7 +51,7 @@ defineEmits(['import', 'export', 'create', 'update', 'delete', 'view', 'edit'])
|
|
|
61
51
|
:items-per-page-options="[5, 10, 20, 50]"
|
|
62
52
|
v-model:page="page"
|
|
63
53
|
v-model:sort-by="sortBy"
|
|
64
|
-
:headers="
|
|
54
|
+
:headers="filteredHeaders"
|
|
65
55
|
:items="items"
|
|
66
56
|
:items-length="totalItems"
|
|
67
57
|
:loading="loading"
|
|
@@ -98,6 +88,16 @@ defineEmits(['import', 'export', 'create', 'update', 'delete', 'view', 'edit'])
|
|
|
98
88
|
@export="v => $emit('export',v)"
|
|
99
89
|
/>
|
|
100
90
|
|
|
91
|
+
<crud-group-by-button
|
|
92
|
+
v-if="entity.isGroupable"
|
|
93
|
+
:entity="entity"
|
|
94
|
+
/>
|
|
95
|
+
|
|
96
|
+
<crud-columns-button
|
|
97
|
+
v-if="entity.isColumnSelectable"
|
|
98
|
+
:entity="entity"
|
|
99
|
+
/>
|
|
100
|
+
|
|
101
101
|
<crud-create-button
|
|
102
102
|
v-if="entity.isCreatable"
|
|
103
103
|
:entity="entity"
|
|
@@ -144,6 +144,9 @@ defineEmits(['import', 'export', 'create', 'update', 'delete', 'view', 'edit'])
|
|
|
144
144
|
</v-card>
|
|
145
145
|
|
|
146
146
|
<v-divider></v-divider>
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
|
|
147
150
|
</template>
|
|
148
151
|
|
|
149
152
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {onMounted, ref, computed} from "vue";
|
|
3
|
+
import type {PropType} from "vue";
|
|
4
|
+
import type {IEntityCrud} from "@drax/crud-share";
|
|
5
|
+
import type {IDraxFieldFilter} from "@drax/crud-share/dist";
|
|
6
|
+
|
|
7
|
+
const {entity, value, refDisplay} = defineProps({
|
|
8
|
+
entity: {type: Object as PropType<IEntityCrud | undefined>, required: true},
|
|
9
|
+
value: {type: Array as PropType<any[]>, required: true},
|
|
10
|
+
refDisplay: {type: String as PropType<String>, required: true},
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const loading = ref(false)
|
|
14
|
+
const items = ref<any[]>([])
|
|
15
|
+
|
|
16
|
+
onMounted(()=> {
|
|
17
|
+
console.log('onMounted',entity, value, refDisplay)
|
|
18
|
+
fetch()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
async function fetch() {
|
|
22
|
+
try{
|
|
23
|
+
loading.value = true
|
|
24
|
+
if(entity?.provider?.find){
|
|
25
|
+
const ids = Array.isArray(value) ? value : [value]
|
|
26
|
+
const filters: IDraxFieldFilter[] = [{field: '_id', operator: 'in', value: ids}]
|
|
27
|
+
items.value = await entity.provider.find({filters})
|
|
28
|
+
}
|
|
29
|
+
}catch (e){
|
|
30
|
+
console.error(e)
|
|
31
|
+
}finally{
|
|
32
|
+
loading.value = false
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const display = computed(() => {
|
|
37
|
+
return items.value.map(item => item[refDisplay as any]).join(', ')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<template>
|
|
44
|
+
{{display}}
|
|
45
|
+
|
|
46
|
+
</template>
|
|
47
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { PropType } from 'vue'
|
|
3
|
+
import type { IEntityCrud } from '@drax/crud-share'
|
|
4
|
+
import { useI18n } from 'vue-i18n'
|
|
5
|
+
import { useCrudColumns } from '../../composables/UseCrudColumns'
|
|
6
|
+
|
|
7
|
+
const { t } = useI18n()
|
|
8
|
+
|
|
9
|
+
const props = defineProps({
|
|
10
|
+
entity: { type: Object as PropType<IEntityCrud>, required: true },
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
availableColumns,
|
|
15
|
+
toggleColumn,
|
|
16
|
+
allSelected,
|
|
17
|
+
someSelected,
|
|
18
|
+
selectAll,
|
|
19
|
+
deselectAll
|
|
20
|
+
} = useCrudColumns(props.entity)
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<v-menu offset-y :close-on-content-click="false">
|
|
25
|
+
<template v-slot:activator="{ props }">
|
|
26
|
+
<v-btn
|
|
27
|
+
v-bind="props"
|
|
28
|
+
icon
|
|
29
|
+
variant="text"
|
|
30
|
+
>
|
|
31
|
+
<v-icon>mdi-view-column</v-icon>
|
|
32
|
+
<v-tooltip activator="parent" location="bottom">
|
|
33
|
+
{{ t('crud.columns.select') }}
|
|
34
|
+
</v-tooltip>
|
|
35
|
+
</v-btn>
|
|
36
|
+
</template>
|
|
37
|
+
<v-list>
|
|
38
|
+
<v-list-subheader>
|
|
39
|
+
{{ t('crud.columns.title') }}
|
|
40
|
+
</v-list-subheader>
|
|
41
|
+
|
|
42
|
+
<v-list-item>
|
|
43
|
+
<div class="d-flex gap-2">
|
|
44
|
+
<v-btn
|
|
45
|
+
size="small"
|
|
46
|
+
variant="text"
|
|
47
|
+
color="primary"
|
|
48
|
+
@click="selectAll"
|
|
49
|
+
:disabled="allSelected"
|
|
50
|
+
>
|
|
51
|
+
{{ t('crud.columns.selectAll') }}
|
|
52
|
+
</v-btn>
|
|
53
|
+
<v-btn
|
|
54
|
+
size="small"
|
|
55
|
+
variant="text"
|
|
56
|
+
color="primary"
|
|
57
|
+
@click="deselectAll"
|
|
58
|
+
:disabled="!someSelected"
|
|
59
|
+
>
|
|
60
|
+
{{ t('crud.columns.deselectAll') }}
|
|
61
|
+
</v-btn>
|
|
62
|
+
</div>
|
|
63
|
+
</v-list-item>
|
|
64
|
+
|
|
65
|
+
<v-divider></v-divider>
|
|
66
|
+
|
|
67
|
+
<v-list-item
|
|
68
|
+
v-for="column in availableColumns"
|
|
69
|
+
:key="column.key"
|
|
70
|
+
@click="toggleColumn(column.key)"
|
|
71
|
+
>
|
|
72
|
+
<template v-slot:prepend>
|
|
73
|
+
<v-checkbox-btn
|
|
74
|
+
:model-value="column.visible"
|
|
75
|
+
@click.stop="toggleColumn(column.key)"
|
|
76
|
+
></v-checkbox-btn>
|
|
77
|
+
</template>
|
|
78
|
+
<v-list-item-title>{{ column.title }}</v-list-item-title>
|
|
79
|
+
</v-list-item>
|
|
80
|
+
</v-list>
|
|
81
|
+
</v-menu>
|
|
82
|
+
</template>
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { PropType } from 'vue'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import type { IEntityCrud } from "@drax/crud-share"
|
|
5
|
+
import { useDateFormat } from "@drax/common-vue"
|
|
6
|
+
import { useI18n } from "vue-i18n"
|
|
7
|
+
import { useCrudGroupBy } from '../../composables/UseCrudGroupBy'
|
|
8
|
+
import CrudActiveFilters from "../CrudActiveFilters.vue";
|
|
9
|
+
|
|
10
|
+
const { t, te } = useI18n()
|
|
11
|
+
|
|
12
|
+
const {formatDateByUnit} = useDateFormat()
|
|
13
|
+
|
|
14
|
+
const props = defineProps({
|
|
15
|
+
entity: { type: Object as PropType<IEntityCrud>, required: true }
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const emit = defineEmits(['groupBy'])
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
dialog,
|
|
22
|
+
selectedFields,
|
|
23
|
+
loading,
|
|
24
|
+
groupByData,
|
|
25
|
+
availableFields,
|
|
26
|
+
dateFormat,
|
|
27
|
+
hasDateFields,
|
|
28
|
+
dateFormatOptions,
|
|
29
|
+
openDialog,
|
|
30
|
+
resetAndClose,
|
|
31
|
+
handleGroupBy
|
|
32
|
+
} = useCrudGroupBy(props.entity)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
// Generar headers dinámicamente basados en los campos seleccionados
|
|
37
|
+
const headers = computed(() => {
|
|
38
|
+
if (!groupByData.value.length || !selectedFields.value.length) return []
|
|
39
|
+
|
|
40
|
+
const fieldHeaders = selectedFields.value.map(field => {
|
|
41
|
+
const label = field.name
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
title: te(`${props.entity.name.toLowerCase()}.field.${label}`)
|
|
45
|
+
? t(`${props.entity.name.toLowerCase()}.field.${label}`)
|
|
46
|
+
: label,
|
|
47
|
+
key: field.name,
|
|
48
|
+
align: 'start' as const
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return [
|
|
53
|
+
...fieldHeaders,
|
|
54
|
+
{
|
|
55
|
+
title: t('crud.groupBy.count'),
|
|
56
|
+
key: 'count',
|
|
57
|
+
align: 'end' as const
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Calcular total de registros
|
|
63
|
+
const totalCount = computed(() => {
|
|
64
|
+
return groupByData.value.reduce((sum: Number, item: any) => sum + (item.count || 0), 0)
|
|
65
|
+
})
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<template>
|
|
69
|
+
<div>
|
|
70
|
+
<v-btn
|
|
71
|
+
icon
|
|
72
|
+
variant="text"
|
|
73
|
+
@click="openDialog"
|
|
74
|
+
>
|
|
75
|
+
<v-icon>mdi-chart-bar</v-icon>
|
|
76
|
+
<v-tooltip activator="parent" location="bottom">
|
|
77
|
+
{{ t('crud.groupBy.button') }}
|
|
78
|
+
</v-tooltip>
|
|
79
|
+
</v-btn>
|
|
80
|
+
|
|
81
|
+
<v-dialog v-model="dialog" max-width="800" >
|
|
82
|
+
<v-card>
|
|
83
|
+
<v-card-title class="d-flex align-center">
|
|
84
|
+
<v-icon class="mr-2">mdi-chart-bar</v-icon>
|
|
85
|
+
{{ t('crud.groupBy.title') }}
|
|
86
|
+
<v-spacer></v-spacer>
|
|
87
|
+
<v-btn
|
|
88
|
+
icon
|
|
89
|
+
variant="text"
|
|
90
|
+
@click="resetAndClose"
|
|
91
|
+
:disabled="loading"
|
|
92
|
+
>
|
|
93
|
+
<v-icon>mdi-close</v-icon>
|
|
94
|
+
</v-btn>
|
|
95
|
+
</v-card-title>
|
|
96
|
+
|
|
97
|
+
<v-divider></v-divider>
|
|
98
|
+
|
|
99
|
+
<v-card-text>
|
|
100
|
+
<crud-active-filters :entity="entity"></crud-active-filters>
|
|
101
|
+
<v-divider></v-divider>
|
|
102
|
+
|
|
103
|
+
<v-select
|
|
104
|
+
v-model="selectedFields"
|
|
105
|
+
:items="availableFields"
|
|
106
|
+
item-title="label"
|
|
107
|
+
:label="t('crud.groupBy.selectFields')"
|
|
108
|
+
multiple
|
|
109
|
+
chips
|
|
110
|
+
closable-chips
|
|
111
|
+
return-object
|
|
112
|
+
>
|
|
113
|
+
</v-select>
|
|
114
|
+
|
|
115
|
+
<!-- Selector de formato de fecha -->
|
|
116
|
+
<v-select
|
|
117
|
+
v-if="hasDateFields"
|
|
118
|
+
v-model="dateFormat"
|
|
119
|
+
:items="dateFormatOptions"
|
|
120
|
+
:label="t('crud.groupBy.dateFormatLabel')"
|
|
121
|
+
density="compact"
|
|
122
|
+
variant="outlined"
|
|
123
|
+
class="mt-4"
|
|
124
|
+
>
|
|
125
|
+
<template v-slot:prepend-inner>
|
|
126
|
+
<v-icon>mdi-calendar-clock</v-icon>
|
|
127
|
+
</template>
|
|
128
|
+
</v-select>
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
</v-card-text>
|
|
132
|
+
|
|
133
|
+
<v-divider></v-divider>
|
|
134
|
+
|
|
135
|
+
<v-card-actions>
|
|
136
|
+
<v-spacer></v-spacer>
|
|
137
|
+
<v-btn
|
|
138
|
+
color="primary"
|
|
139
|
+
variant="flat"
|
|
140
|
+
@click="handleGroupBy"
|
|
141
|
+
:disabled="selectedFields.length === 0"
|
|
142
|
+
:loading="loading"
|
|
143
|
+
>
|
|
144
|
+
{{ t('action.apply') }}
|
|
145
|
+
</v-btn>
|
|
146
|
+
</v-card-actions>
|
|
147
|
+
<v-divider class="mb-4"></v-divider>
|
|
148
|
+
<!-- Tabla de resultados -->
|
|
149
|
+
<v-card-text v-if="groupByData.length > 0">
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
<div class="d-flex align-center mb-3">
|
|
153
|
+
<v-icon class="mr-2">mdi-table</v-icon>
|
|
154
|
+
<span class="text-h6">{{ t('crud.groupBy.results') }}</span>
|
|
155
|
+
<v-spacer></v-spacer>
|
|
156
|
+
<v-chip color="primary" variant="flat" size="small">
|
|
157
|
+
{{ t('crud.groupBy.total') }}: {{ totalCount }}
|
|
158
|
+
</v-chip>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
<v-data-table
|
|
163
|
+
:headers="headers"
|
|
164
|
+
:items="groupByData"
|
|
165
|
+
density="compact"
|
|
166
|
+
:items-per-page="-1"
|
|
167
|
+
hide-default-footer
|
|
168
|
+
>
|
|
169
|
+
<template v-slot:bottom></template>
|
|
170
|
+
|
|
171
|
+
<!-- Slot para personalizar la visualización de cada campo -->
|
|
172
|
+
<template
|
|
173
|
+
v-for="field in selectedFields"
|
|
174
|
+
:key="field.name"
|
|
175
|
+
v-slot:[`item.${field.name}`]="{ value }"
|
|
176
|
+
>
|
|
177
|
+
<template v-if="field.type === 'ref' && field.refDisplay">
|
|
178
|
+
{{value[field.refDisplay]}}
|
|
179
|
+
</template>
|
|
180
|
+
<template v-else-if="field.type === 'date'">
|
|
181
|
+
{{ formatDateByUnit(value, dateFormat) }}
|
|
182
|
+
</template>
|
|
183
|
+
<template v-else>
|
|
184
|
+
{{ value }}
|
|
185
|
+
</template>
|
|
186
|
+
|
|
187
|
+
</template>
|
|
188
|
+
|
|
189
|
+
<!-- Formato especial para el count -->
|
|
190
|
+
<template v-slot:item.count="{ value }">
|
|
191
|
+
<v-chip color="primary" size="small" variant="flat">
|
|
192
|
+
{{ value }}
|
|
193
|
+
</v-chip>
|
|
194
|
+
</template>
|
|
195
|
+
</v-data-table>
|
|
196
|
+
</v-card-text>
|
|
197
|
+
</v-card>
|
|
198
|
+
</v-dialog>
|
|
199
|
+
</div>
|
|
200
|
+
</template>
|
|
201
|
+
|
|
202
|
+
<style scoped>
|
|
203
|
+
</style>
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import type { IEntityCrud, IEntityCrudHeader } from '@drax/crud-share'
|
|
4
|
+
import { useAuth } from '@drax/identity-vue'
|
|
5
|
+
import { useI18n } from 'vue-i18n'
|
|
6
|
+
import { useCrudStore } from '../stores/UseCrudStore'
|
|
7
|
+
|
|
8
|
+
export function useCrudColumns(entity: IEntityCrud) {
|
|
9
|
+
const { hasPermission } = useAuth()
|
|
10
|
+
const { t, te } = useI18n()
|
|
11
|
+
const crudStore = useCrudStore()
|
|
12
|
+
|
|
13
|
+
// Clave única para localStorage basada en el nombre de la entidad
|
|
14
|
+
const storageKey = `crud_visible_columns_${entity.name.toLowerCase()}`
|
|
15
|
+
|
|
16
|
+
// Cargar columnas desde localStorage
|
|
17
|
+
const loadColumnsFromStorage = (): string[] | null => {
|
|
18
|
+
try {
|
|
19
|
+
const stored = localStorage.getItem(storageKey)
|
|
20
|
+
return stored ? JSON.parse(stored) : null
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error('Error loading columns from localStorage:', error)
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Guardar columnas en localStorage
|
|
28
|
+
const saveColumnsToStorage = (columns: string[]) => {
|
|
29
|
+
try {
|
|
30
|
+
localStorage.setItem(storageKey, JSON.stringify(columns))
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('Error saving columns to localStorage:', error)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Inicializar columnas visibles si no existen en el store
|
|
37
|
+
const initializeVisibleColumns = () => {
|
|
38
|
+
if (crudStore.visibleColumns.length === 0) {
|
|
39
|
+
const availableHeaders = entity.headers
|
|
40
|
+
.filter(header => !header.permission || hasPermission(header.permission))
|
|
41
|
+
.map(header => header.key)
|
|
42
|
+
|
|
43
|
+
// Intentar cargar desde localStorage primero
|
|
44
|
+
const storedColumns = loadColumnsFromStorage()
|
|
45
|
+
|
|
46
|
+
let initialColumns: string[]
|
|
47
|
+
|
|
48
|
+
if (storedColumns) {
|
|
49
|
+
// Filtrar columnas almacenadas para asegurar que aún existen y tienen permisos
|
|
50
|
+
initialColumns = storedColumns.filter(key => availableHeaders.includes(key))
|
|
51
|
+
|
|
52
|
+
// Si no quedaron columnas válidas, usar las predeterminadas
|
|
53
|
+
if (initialColumns.length === 0) {
|
|
54
|
+
initialColumns = entity.selectedHeaders?.filter(key =>
|
|
55
|
+
availableHeaders.includes(key)
|
|
56
|
+
) || availableHeaders
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
// Si no hay columnas guardadas, usar selectedHeaders o todas las disponibles
|
|
60
|
+
initialColumns = entity.selectedHeaders?.filter(key =>
|
|
61
|
+
availableHeaders.includes(key)
|
|
62
|
+
) || availableHeaders
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
crudStore.setVisibleColumns(initialColumns)
|
|
66
|
+
// Guardar la configuración inicial en localStorage
|
|
67
|
+
saveColumnsToStorage(initialColumns)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Inicializar al crear el composable
|
|
72
|
+
initializeVisibleColumns()
|
|
73
|
+
|
|
74
|
+
// Obtener columnas visibles del store
|
|
75
|
+
const visibleColumns = computed(() => crudStore.visibleColumns)
|
|
76
|
+
|
|
77
|
+
// Headers traducidos y filtrados por permisos
|
|
78
|
+
const translatedHeaders = computed<IEntityCrudHeader[]>(() => {
|
|
79
|
+
return entity.headers
|
|
80
|
+
.filter(header => !header.permission || hasPermission(header.permission))
|
|
81
|
+
.map(header => ({
|
|
82
|
+
...header,
|
|
83
|
+
title: te(`${entity.name.toLowerCase()}.field.${header.title}`)
|
|
84
|
+
? t(`${entity.name.toLowerCase()}.field.${header.title}`)
|
|
85
|
+
: header.title
|
|
86
|
+
}))
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// Headers filtrados por columnas visibles
|
|
90
|
+
const filteredHeaders = computed<IEntityCrudHeader[]>(() => {
|
|
91
|
+
const filtered = translatedHeaders.value.filter(header =>
|
|
92
|
+
visibleColumns.value.includes(header.key)
|
|
93
|
+
)
|
|
94
|
+
const actions = entity.actionHeaders.map(header => ({
|
|
95
|
+
...header,
|
|
96
|
+
title: te(header.title) ? t(header.title) : header.title,
|
|
97
|
+
}))
|
|
98
|
+
return [...filtered, ...actions]
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Lista de columnas disponibles para el menú
|
|
102
|
+
const availableColumns = computed(() => {
|
|
103
|
+
return translatedHeaders.value.map(header => ({
|
|
104
|
+
key: header.key,
|
|
105
|
+
title: header.title,
|
|
106
|
+
visible: visibleColumns.value.includes(header.key)
|
|
107
|
+
}))
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// Toggle de visibilidad de columna
|
|
111
|
+
const toggleColumn = (columnKey: string) => {
|
|
112
|
+
const currentColumns = [...visibleColumns.value]
|
|
113
|
+
const index = currentColumns.indexOf(columnKey)
|
|
114
|
+
|
|
115
|
+
if (index > -1) {
|
|
116
|
+
currentColumns.splice(index, 1)
|
|
117
|
+
} else {
|
|
118
|
+
currentColumns.push(columnKey)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
crudStore.setVisibleColumns(currentColumns)
|
|
122
|
+
// Guardar cambios en localStorage
|
|
123
|
+
saveColumnsToStorage(currentColumns)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const allSelected = computed(() =>
|
|
127
|
+
availableColumns.value.every(col => col.visible)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const someSelected = computed(() =>
|
|
131
|
+
availableColumns.value.some(col => col.visible)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
const selectAll = () => {
|
|
135
|
+
const allColumns = availableColumns.value.map(col => col.key)
|
|
136
|
+
crudStore.setVisibleColumns(allColumns)
|
|
137
|
+
// Guardar cambios en localStorage
|
|
138
|
+
saveColumnsToStorage(allColumns)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const deselectAll = () => {
|
|
142
|
+
crudStore.setVisibleColumns([])
|
|
143
|
+
// Guardar cambios en localStorage
|
|
144
|
+
saveColumnsToStorage([])
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Función para resetear a las columnas predeterminadas
|
|
148
|
+
const resetToDefault = () => {
|
|
149
|
+
const availableHeaders = entity.headers
|
|
150
|
+
.filter(header => !header.permission || hasPermission(header.permission))
|
|
151
|
+
.map(header => header.key)
|
|
152
|
+
|
|
153
|
+
const defaultColumns = entity.selectedHeaders?.filter(key =>
|
|
154
|
+
availableHeaders.includes(key)
|
|
155
|
+
) || availableHeaders
|
|
156
|
+
|
|
157
|
+
crudStore.setVisibleColumns(defaultColumns)
|
|
158
|
+
saveColumnsToStorage(defaultColumns)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
visibleColumns,
|
|
163
|
+
translatedHeaders,
|
|
164
|
+
filteredHeaders,
|
|
165
|
+
availableColumns,
|
|
166
|
+
toggleColumn,
|
|
167
|
+
initializeVisibleColumns,
|
|
168
|
+
allSelected,
|
|
169
|
+
someSelected,
|
|
170
|
+
selectAll,
|
|
171
|
+
deselectAll,
|
|
172
|
+
resetToDefault,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { computed } from 'vue'
|
|
2
|
+
import type {IEntityCrud, IEntityCrudField, IDraxGroupByDateFormat} from "@drax/crud-share"
|
|
3
|
+
import { useI18n } from "vue-i18n"
|
|
4
|
+
import { useGroupByStore } from '../stores/UseGroupByStore'
|
|
5
|
+
import {useCrudStore} from '../stores/UseCrudStore'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export const useCrudGroupBy = (entity: IEntityCrud) => {
|
|
9
|
+
const { t, te } = useI18n()
|
|
10
|
+
const groupBystore = useGroupByStore()
|
|
11
|
+
const crudStore = useCrudStore()
|
|
12
|
+
const entityName = entity.name.toLowerCase()
|
|
13
|
+
|
|
14
|
+
const availableFields = computed<IEntityCrudField[]>(() => {
|
|
15
|
+
return entity.fields
|
|
16
|
+
.filter(field => {
|
|
17
|
+
// Excluir campos que no son apropiados para agrupar
|
|
18
|
+
const excludedTypes = ['password', 'longString', 'array.string','array.number','array.ref','array.object', 'object', 'file', 'fullFile']
|
|
19
|
+
return !excludedTypes.includes(field.type)
|
|
20
|
+
}).map(field => ({...field, label: te(`${entityName}.field.${field?.name}`) ? t(`${entityName}.field.${field?.name}`) : field?.label}))
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const handleGroupBy = async (callback: (fields: string[]) => void | Promise<void>) => {
|
|
24
|
+
if (groupBystore.selectedFields.length === 0) return
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
groupBystore.setLoading(true)
|
|
28
|
+
try {
|
|
29
|
+
if(entity?.provider?.groupBy){
|
|
30
|
+
const data = await entity.provider.groupBy({
|
|
31
|
+
fields: groupBystore.selectedFields.map(sf => sf.name),
|
|
32
|
+
filters: crudStore.filters
|
|
33
|
+
})
|
|
34
|
+
groupBystore.setGroupByData(data)
|
|
35
|
+
}
|
|
36
|
+
} finally {
|
|
37
|
+
groupBystore.setLoading(false)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Verificar si hay campos de tipo fecha seleccionados
|
|
42
|
+
const hasDateFields = computed(() => {
|
|
43
|
+
return groupBystore.selectedFields.some(field => {
|
|
44
|
+
return field?.type === 'date'
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// Opciones de formato de fecha
|
|
49
|
+
const dateFormatOptions = computed(() => [
|
|
50
|
+
{ value: 'year', title: t('crud.groupBy.dateFormat.year') },
|
|
51
|
+
{ value: 'month', title: t('crud.groupBy.dateFormat.month') },
|
|
52
|
+
{ value: 'day', title: t('crud.groupBy.dateFormat.day') },
|
|
53
|
+
{ value: 'hour', title: t('crud.groupBy.dateFormat.hour') },
|
|
54
|
+
{ value: 'minute', title: t('crud.groupBy.dateFormat.minute') },
|
|
55
|
+
{ value: 'second', title: t('crud.groupBy.dateFormat.second') }
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
// Store state
|
|
62
|
+
dialog: computed(() => groupBystore.dialog),
|
|
63
|
+
selectedFields: computed({
|
|
64
|
+
get: () => groupBystore.selectedFields,
|
|
65
|
+
set: (value: IEntityCrudField[]) => {
|
|
66
|
+
groupBystore.clearGroupByData()
|
|
67
|
+
groupBystore.setSelectedFields(value)
|
|
68
|
+
}
|
|
69
|
+
}),
|
|
70
|
+
groupByData: computed(() => groupBystore.groupByData),
|
|
71
|
+
loading: computed(() => groupBystore.loading),
|
|
72
|
+
// Computed
|
|
73
|
+
availableFields,
|
|
74
|
+
// Methods
|
|
75
|
+
openDialog: groupBystore.openDialog,
|
|
76
|
+
closeDialog: groupBystore.closeDialog,
|
|
77
|
+
resetAndClose: groupBystore.resetAndClose,
|
|
78
|
+
clearGroupByData: groupBystore.clearGroupByData,
|
|
79
|
+
handleGroupBy,
|
|
80
|
+
|
|
81
|
+
dateFormat: computed({
|
|
82
|
+
get: () => groupBystore.dateFormat,
|
|
83
|
+
set: (value: IDraxGroupByDateFormat) => {
|
|
84
|
+
groupBystore.clearGroupByData()
|
|
85
|
+
groupBystore.setDateFormat(value)
|
|
86
|
+
}
|
|
87
|
+
}),
|
|
88
|
+
hasDateFields,
|
|
89
|
+
dateFormatOptions,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type {IEntityCrud} from '@drax/crud-share'
|
|
2
|
+
|
|
3
|
+
export function useCrudRefDisplay() {
|
|
4
|
+
|
|
5
|
+
async function refDisplay(entity: IEntityCrud, value: any, refDisplay: string) {
|
|
6
|
+
try{
|
|
7
|
+
if(entity?.provider?.findByIds){
|
|
8
|
+
// Asegurar que value sea un array
|
|
9
|
+
const ids = Array.isArray(value) ? value : [value]
|
|
10
|
+
const items = await entity.provider.findByIds(ids)
|
|
11
|
+
return items.map(item => item[refDisplay as any]).join(', ')
|
|
12
|
+
}
|
|
13
|
+
return value
|
|
14
|
+
}catch (e){
|
|
15
|
+
console.error(e)
|
|
16
|
+
return value
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
refDisplay
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import type { IEntityCrudFilter } from '@drax/crud-share'
|
|
4
|
+
|
|
5
|
+
export function useFilterIcon() {
|
|
6
|
+
const filterIcon = computed(() => {
|
|
7
|
+
return (field: IEntityCrudFilter) => {
|
|
8
|
+
switch(field.operator){
|
|
9
|
+
case 'eq':
|
|
10
|
+
return 'mdi-equal'
|
|
11
|
+
case 'ne':
|
|
12
|
+
return 'mdi-not-equal'
|
|
13
|
+
case 'gt':
|
|
14
|
+
return 'mdi-greater-than'
|
|
15
|
+
case 'gte':
|
|
16
|
+
return 'mdi-greater-than-or-equal'
|
|
17
|
+
case 'lt':
|
|
18
|
+
return 'mdi-less-than'
|
|
19
|
+
case 'lte':
|
|
20
|
+
return 'mdi-less-than-or-equal'
|
|
21
|
+
case 'in':
|
|
22
|
+
return 'mdi-code-array'
|
|
23
|
+
case 'nin':
|
|
24
|
+
return 'mdi-not-equal'
|
|
25
|
+
case 'like':
|
|
26
|
+
return 'mdi-contain'
|
|
27
|
+
default:
|
|
28
|
+
return 'mdi-equal'
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
filterIcon
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {defineStore} from "pinia";
|
|
2
|
-
import type {IEntityCrudOperation} from "@drax/crud-share";
|
|
2
|
+
import type {IEntityCrudOperation, IDraxFieldFilter} from "@drax/crud-share";
|
|
3
3
|
|
|
4
4
|
export const useCrudStore = defineStore('CrudStore', {
|
|
5
5
|
state: () => (
|
|
@@ -11,7 +11,7 @@ export const useCrudStore = defineStore('CrudStore', {
|
|
|
11
11
|
notify: false as boolean,
|
|
12
12
|
message: '' as string,
|
|
13
13
|
error: '' as string,
|
|
14
|
-
filters: [] as
|
|
14
|
+
filters: [] as IDraxFieldFilter[],
|
|
15
15
|
items: [] as any[],
|
|
16
16
|
totalItems: 0 as number,
|
|
17
17
|
itemsPerPage: 10 as number,
|
|
@@ -23,7 +23,8 @@ export const useCrudStore = defineStore('CrudStore', {
|
|
|
23
23
|
exportLoading: false,
|
|
24
24
|
exportFiles: [] as string[],
|
|
25
25
|
exportListVisible: false,
|
|
26
|
-
exportError: false
|
|
26
|
+
exportError: false,
|
|
27
|
+
visibleColumns: []
|
|
27
28
|
}
|
|
28
29
|
),
|
|
29
30
|
getters: {
|
|
@@ -123,7 +124,7 @@ export const useCrudStore = defineStore('CrudStore', {
|
|
|
123
124
|
setExportError(error: boolean){
|
|
124
125
|
this.exportError = error
|
|
125
126
|
},
|
|
126
|
-
setFilters(filters:
|
|
127
|
+
setFilters(filters: IDraxFieldFilter[]) {
|
|
127
128
|
this.filters = filters
|
|
128
129
|
},
|
|
129
130
|
setFilterValue(name:string, value:any) {
|
|
@@ -131,6 +132,13 @@ export const useCrudStore = defineStore('CrudStore', {
|
|
|
131
132
|
if (index >= 0) {
|
|
132
133
|
this.filters[index].value = value
|
|
133
134
|
}
|
|
135
|
+
},
|
|
136
|
+
setVisibleColumns(columns: string[]) {
|
|
137
|
+
this.visibleColumns = columns
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
clearVisibleColumns() {
|
|
141
|
+
this.visibleColumns = []
|
|
134
142
|
}
|
|
135
143
|
}
|
|
136
144
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
|
|
2
|
+
import { defineStore } from 'pinia'
|
|
3
|
+
import type {IDraxGroupByDateFormat, IEntityCrudField} from "@drax/crud-share";
|
|
4
|
+
|
|
5
|
+
export const useGroupByStore = defineStore('useGroupByStore', {
|
|
6
|
+
state: () => ({
|
|
7
|
+
dialog: false as boolean,
|
|
8
|
+
selectedFields: [] as IEntityCrudField[],
|
|
9
|
+
dateFormat: 'day' as IDraxGroupByDateFormat,
|
|
10
|
+
loading: false as boolean,
|
|
11
|
+
groupByData: [] as any[],
|
|
12
|
+
groupByError: '' as string
|
|
13
|
+
}),
|
|
14
|
+
|
|
15
|
+
getters: {
|
|
16
|
+
hasSelectedFields(state): boolean {
|
|
17
|
+
return state.selectedFields.length > 0
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
selectedFieldsCount(state): number {
|
|
21
|
+
return state.selectedFields.length
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
isFieldSelected(state) {
|
|
25
|
+
return (field: IEntityCrudField): boolean => {
|
|
26
|
+
return state.selectedFields.some(sf => sf.name === field.name)
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
hasGroupByData(state): boolean {
|
|
31
|
+
return state.groupByData.length > 0
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
groupByDataCount(state): number {
|
|
35
|
+
return state.groupByData.length
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
getGroupByDataByField(state) {
|
|
39
|
+
return (fieldName: string, fieldValue: any): any | undefined => {
|
|
40
|
+
return state.groupByData.find((result: any) => result[fieldName] === fieldValue)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
actions: {
|
|
46
|
+
openDialog() {
|
|
47
|
+
this.dialog = true
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
closeDialog() {
|
|
51
|
+
this.dialog = false
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
resetFields() {
|
|
55
|
+
this.selectedFields = []
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
resetAndClose() {
|
|
59
|
+
this.resetFields()
|
|
60
|
+
this.closeDialog()
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
setLoading(value: boolean) {
|
|
64
|
+
this.loading = value
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
setSelectedFields(fields: IEntityCrudField[]) {
|
|
68
|
+
this.selectedFields = fields
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
addField(field: IEntityCrudField) {
|
|
72
|
+
if (!this.selectedFields.some(f => f.name === field.name)) {
|
|
73
|
+
this.selectedFields.push(field)
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
removeField(field: IEntityCrudField) {
|
|
78
|
+
const index = this.selectedFields.findIndex((f) => f.name === field.name)
|
|
79
|
+
if (index > -1) {
|
|
80
|
+
this.selectedFields.splice(index, 1)
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
setGroupByData(data: any[]) {
|
|
85
|
+
this.groupByData = data
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
clearGroupByData() {
|
|
89
|
+
this.groupByData = []
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
setGroupByError(error: string) {
|
|
93
|
+
this.groupByError = error
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
clearGroupByError() {
|
|
97
|
+
this.groupByError = ''
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
setDateFormat(dateFormat: IDraxGroupByDateFormat) {
|
|
101
|
+
this.dateFormat = dateFormat
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
}
|
|
105
|
+
})
|