@drax/crud-vue 0.36.0 → 0.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.36.0",
6
+ "version": "0.37.1",
7
7
  "type": "module",
8
8
  "main": "./src/index.ts",
9
9
  "module": "./src/index.ts",
@@ -24,12 +24,13 @@
24
24
  "format": "prettier --write src/"
25
25
  },
26
26
  "dependencies": {
27
- "@drax/common-front": "^0.36.0",
28
- "@drax/crud-front": "^0.36.0",
29
- "@drax/crud-share": "^0.36.0",
30
- "@drax/media-vue": "^0.36.0"
27
+ "@drax/common-front": "^0.37.0",
28
+ "@drax/crud-front": "^0.37.1",
29
+ "@drax/crud-share": "^0.37.0",
30
+ "@drax/media-vue": "^0.37.0"
31
31
  },
32
32
  "peerDependencies": {
33
+ "dayjs": "^1.11.13",
33
34
  "pinia": "^2.2.2",
34
35
  "vue": "^3.5.7",
35
36
  "vue-i18n": "^9.14.0",
@@ -64,5 +65,5 @@
64
65
  "vue-tsc": "^2.1.10",
65
66
  "vuetify": "^3.8.2"
66
67
  },
67
- "gitHead": "096f17a9a7f6e6969b8367a978137e090916f16d"
68
+ "gitHead": "e4be12cd506bf5255e0f8b26b89cdd2a660a523d"
68
69
  }
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,149 @@
1
+ <script setup lang="ts">
2
+ import { computed, type PropType } from 'vue'
3
+ import type { IEntityCrud } from '@drax/crud-share'
4
+ import { useCrudStore } from '../stores/UseCrudStore'
5
+ import { useI18n } from 'vue-i18n'
6
+ import { useFilterIcon } from '../composables/useFilterIcon'
7
+ import dayjs from "dayjs";
8
+ import CrudRefDisplay from "./CrudRefDisplay.vue";
9
+
10
+ const { t} = useI18n()
11
+ const store = useCrudStore()
12
+ const { filterIcon } = useFilterIcon()
13
+
14
+ const props = defineProps({
15
+ entity: {
16
+ type: Object as PropType<IEntityCrud>,
17
+ required: true
18
+ }
19
+ })
20
+
21
+ const activeFilters = computed<any[]>(() => {
22
+ return store.filters
23
+ .map((filter:any, index: any) => {
24
+ const filterDef = props.entity.filters[index]
25
+ if (!filterDef) return null
26
+
27
+ // Solo mostrar si tiene valor
28
+ if (filter.value === null || filter.value === undefined || filter.value === '') {
29
+ return null
30
+ }
31
+
32
+ // Para arrays vacíos
33
+ if (Array.isArray(filter.value) && filter.value.length === 0) {
34
+ return null
35
+ }
36
+
37
+ return {
38
+ ...filterDef,
39
+ value: filter.value,
40
+ index
41
+ }
42
+ })
43
+ .filter((f: any) => f !== null)
44
+ })
45
+
46
+ const getFilterLabel = (filter: any) => {
47
+ const label = t(`${props.entity.name.toLowerCase()}.field.${filter.label}`, filter.label)
48
+ return label
49
+ }
50
+
51
+
52
+
53
+ const getFilterValue = (filter: any) => {
54
+ switch (filter.type) {
55
+ case 'date':
56
+ return dayjs(filter.value).format('YYYY-MM-DD')
57
+
58
+ case 'boolean':
59
+ return filter.value ? t('common.yes') : t('common.no')
60
+
61
+ case 'ref':
62
+ return filter.value
63
+
64
+ case 'array.ref':
65
+ if (Array.isArray(filter.value)) {
66
+ return filter.value.map((v: any) => v[filter.refDisplay] || v).join(', ')
67
+ }
68
+ return filter.value
69
+
70
+ case 'enum':
71
+ return filter.value
72
+
73
+ case 'array.enum':
74
+ if (Array.isArray(filter.value)) {
75
+ return filter.value.join(', ')
76
+ }
77
+ return filter.value
78
+
79
+ case 'number':
80
+ return filter.value.toString()
81
+
82
+ case 'string':
83
+ default:
84
+ return filter.value
85
+ }
86
+ }
87
+
88
+ const removeFilter = (index: number) => {
89
+ const filter = store.filters[index]
90
+ const filterDef = props.entity.filters[index]
91
+
92
+ // Resetear al valor por defecto
93
+ filter.value = filterDef.default
94
+
95
+ // Emitir evento para aplicar filtros
96
+ emit('filterRemoved')
97
+ }
98
+
99
+
100
+ const emit = defineEmits(['filterRemoved', 'filtersCleared'])
101
+ </script>
102
+
103
+ <template>
104
+ <v-card v-if="activeFilters.length > 0" flat class="mb-2">
105
+ <v-card-text class="py-2">
106
+ <div class="d-flex align-center flex-wrap ga-2">
107
+ <span class="text-caption text-medium-emphasis">
108
+ <v-icon size="x-small">mdi-filter</v-icon> {{ t('crud.activeFilters') }}:
109
+ </span>
110
+
111
+ <v-chip
112
+ v-for="filter in activeFilters"
113
+ :key="filter.index"
114
+ closable
115
+ size="small"
116
+ color="primary"
117
+ variant="tonal"
118
+ @click:close="removeFilter(filter.index)"
119
+ >
120
+
121
+ <span class="font-weight-medium">{{ getFilterLabel(filter) }}</span>
122
+ <v-icon :icon="filterIcon(filter)" size="x-small" class="mx-1" />
123
+ <span v-if="['ref','array.ref'].includes(filter.type)">
124
+ <crud-ref-display
125
+ :ref-display="filter.refDisplay"
126
+ :value="filter.value"
127
+ :entity="entity.getRef(filter.ref)" />
128
+ </span>
129
+ <span v-else>{{ getFilterValue(filter) }}</span>
130
+
131
+ <v-tooltip
132
+ v-if="filter.endOfDay"
133
+ activator="parent"
134
+ location="top"
135
+ >
136
+ {{ t('crud.endOfDayFilter') }}
137
+ </v-tooltip>
138
+ </v-chip>
139
+
140
+ </div>
141
+ </v-card-text>
142
+ </v-card>
143
+ </template>
144
+
145
+ <style scoped>
146
+ .v-chip {
147
+ max-width: 300px;
148
+ }
149
+ </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
- const icon = computed(() => {
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
- function clear() {
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="icon(filter)"
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
- const actions: IEntityCrudHeader[] = entity.actionHeaders.map(header => ({
32
- ...header,
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="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
 
@@ -165,12 +168,12 @@ defineEmits(['import', 'export', 'create', 'update', 'delete', 'view', 'edit'])
165
168
  />
166
169
 
167
170
  <crud-update-button
168
- v-if="entity.isEditable && entity.isItemEditable(item) && hasPermission(entity.permissions.update)"
171
+ v-if="entity.isEditable && entity.isItemEditable(item) && hasPermission(entity.permissions?.update)"
169
172
  @click="$emit('edit', item)"
170
173
  />
171
174
 
172
175
  <crud-delete-button
173
- v-if="entity.isDeletable && hasPermission(entity.permissions.delete)"
176
+ v-if="entity.isDeletable && hasPermission(entity.permissions?.delete)"
174
177
  @click="$emit('delete', item)"
175
178
  />
176
179
 
@@ -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";
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>
@@ -2,13 +2,13 @@ import type {IDraxPaginateResult, IEntityCrud} from "@drax/crud-share";
2
2
  import {useCrudStore} from "../stores/UseCrudStore";
3
3
  import {computed, nextTick, toRaw} from "vue";
4
4
  import getItemId from "../helpers/getItemId";
5
- import { useI18n } from "vue-i18n";
5
+ import {useI18n} from "vue-i18n";
6
6
 
7
7
  export function useCrud(entity: IEntityCrud) {
8
8
 
9
9
  const store = useCrudStore()
10
10
 
11
- const { t: $t } = useI18n()
11
+ const {t: $t} = useI18n()
12
12
 
13
13
  const exportError = computed({
14
14
  get() {
@@ -224,7 +224,7 @@ export function useCrud(entity: IEntityCrud) {
224
224
  .forEach(field => {
225
225
  if (item[field.name] && Array.isArray(item[field.name])) {
226
226
  item[field.name] = item[field.name].map(((i: any) => getItemId(i) ? getItemId(i) : i))
227
- }else{
227
+ } else {
228
228
  item[field.name] = []
229
229
  }
230
230
  })
@@ -252,10 +252,10 @@ export function useCrud(entity: IEntityCrud) {
252
252
  openDialog()
253
253
  }
254
254
 
255
- function cloneItem(item: object):object {
256
- try{
255
+ function cloneItem(item: object): object {
256
+ try {
257
257
  return JSON.parse(JSON.stringify(item))
258
- }catch (error){
258
+ } catch (error) {
259
259
  console.error("Error cloning item", error)
260
260
  return ({})
261
261
  }
@@ -273,7 +273,7 @@ export function useCrud(entity: IEntityCrud) {
273
273
  store.setInputErrors(null)
274
274
  }
275
275
 
276
- async function onSubmit(formData: any): Promise<{status:string,item?:any}> {
276
+ async function onSubmit(formData: any): Promise<{ status: string, item?: any }> {
277
277
  store.setInputErrors(null)
278
278
  switch (store.operation) {
279
279
  case "view":
@@ -300,11 +300,14 @@ export function useCrud(entity: IEntityCrud) {
300
300
  async function doCreate(formData: any) {
301
301
  try {
302
302
  store.setLoading(true)
303
- let item = await entity?.provider.create(toRaw(formData))
304
- await doPaginate()
305
- closeDialog()
306
- store.showMessage("Entity created successfully!")
307
- return {status: 'created', item: item}
303
+ if (entity?.provider.create) {
304
+ let item = await entity?.provider.create(toRaw(formData))
305
+ await doPaginate()
306
+ closeDialog()
307
+ store.showMessage("Entity created successfully!")
308
+ return {status: 'created', item: item}
309
+ }
310
+ throw new Error("provider.create not implemented")
308
311
  } catch (e: any) {
309
312
  if (e.inputErrors) {
310
313
  store.setInputErrors(e.inputErrors)
@@ -321,11 +324,15 @@ export function useCrud(entity: IEntityCrud) {
321
324
  async function doUpdate(formData: any) {
322
325
  try {
323
326
  store.setLoading(true)
324
- let item = await entity?.provider.update(getItemId(formData), toRaw(formData))
325
- await doPaginate()
326
- closeDialog()
327
- store.showMessage("Entity updated successfully!")
328
- return {status: 'updated', item: item}
327
+ if (entity?.provider.update) {
328
+ let item = await entity?.provider.update(getItemId(formData), toRaw(formData))
329
+ await doPaginate()
330
+ closeDialog()
331
+ store.showMessage("Entity updated successfully!")
332
+ return {status: 'updated', item: item}
333
+ }
334
+ throw new Error("provider.update not implemented")
335
+
329
336
  } catch (e: any) {
330
337
  //console.log("inputErrors", e.inputErrors)
331
338
  if (e.inputErrors) {
@@ -343,11 +350,14 @@ export function useCrud(entity: IEntityCrud) {
343
350
  async function doDelete(formData: any) {
344
351
  try {
345
352
  store.setLoading(true)
346
- await entity?.provider.delete(getItemId(formData))
347
- await doPaginate()
348
- closeDialog()
349
- store.showMessage("Entity deleted successfully!")
350
- return {status: 'deleted'}
353
+ if (entity?.provider.delete) {
354
+ await entity?.provider.delete(getItemId(formData))
355
+ await doPaginate()
356
+ closeDialog()
357
+ store.showMessage("Entity deleted successfully!")
358
+ return {status: 'deleted'}
359
+ }
360
+ throw new Error("provider.delete not implemented")
351
361
  } catch (e: any) {
352
362
  store.setError(e.message || "An error occurred while deleting the entity")
353
363
  console.error("Error updating entity", e)
@@ -366,7 +376,7 @@ export function useCrud(entity: IEntityCrud) {
366
376
  store.setFilters(entity.formFilters)
367
377
  }
368
378
 
369
- async function clearFilters(){
379
+ async function clearFilters() {
370
380
  prepareFilters()
371
381
  store.setSearch("")
372
382
  search.value = ""
@@ -376,7 +386,7 @@ export function useCrud(entity: IEntityCrud) {
376
386
 
377
387
  }
378
388
 
379
- async function applyFilters(){
389
+ async function applyFilters() {
380
390
  await doPaginate()
381
391
  }
382
392
 
@@ -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 any[],
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: any[]) {
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
+ })