@innertia-solutions/nuxt-theme-spark 0.1.141 → 0.1.143
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.
|
@@ -1,29 +1,45 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { IconSearch, IconLayoutColumns, IconGripVertical, IconMinus, IconMaximize, IconX, IconPlus, IconChevronLeft, IconCheck, IconChevronDown } from '@tabler/icons-vue'
|
|
2
|
+
import { IconSearch, IconLayoutColumns, IconGripVertical, IconMinus, IconMaximize, IconX, IconPlus, IconChevronLeft, IconCheck, IconChevronDown, IconExternalLink, IconTrash } from '@tabler/icons-vue'
|
|
3
|
+
import Table from './index.vue'
|
|
3
4
|
|
|
4
5
|
const props = defineProps({
|
|
5
|
-
table:
|
|
6
|
-
endpoint:
|
|
7
|
-
columns:
|
|
8
|
-
name:
|
|
9
|
-
params:
|
|
10
|
-
checkable:
|
|
11
|
-
cached:
|
|
12
|
-
showReloadButton:
|
|
13
|
-
clickRowToOpen:
|
|
14
|
-
searchPlaceholder:
|
|
15
|
-
showSearch:
|
|
16
|
-
showFilters:
|
|
17
|
-
showExport:
|
|
18
|
-
filters:
|
|
19
|
-
splitRatio:
|
|
20
|
-
autoClosePreview:
|
|
6
|
+
table: { type: Object, default: null },
|
|
7
|
+
endpoint: { type: String, default: '' },
|
|
8
|
+
columns: { type: Array, required: true },
|
|
9
|
+
name: { type: String, default: '' },
|
|
10
|
+
params: { type: Object, default: () => ({}) },
|
|
11
|
+
checkable: { type: Boolean, default: false },
|
|
12
|
+
cached: { type: Boolean, default: false },
|
|
13
|
+
showReloadButton: { type: Boolean, default: true },
|
|
14
|
+
clickRowToOpen: { type: Boolean, default: false },
|
|
15
|
+
searchPlaceholder: { type: String, default: 'Buscar...' },
|
|
16
|
+
showSearch: { type: Boolean, default: true },
|
|
17
|
+
showFilters: { type: Boolean, default: true },
|
|
18
|
+
showExport: { type: Boolean, default: true },
|
|
19
|
+
filters: { type: Array, default: () => [] },
|
|
20
|
+
splitRatio: { type: Number, default: 60 },
|
|
21
|
+
autoClosePreview: { type: Boolean, default: true },
|
|
22
|
+
previewHref: { type: [String, Function], default: null }, // url fija o (row) => url
|
|
23
|
+
previewDeletable: { type: Boolean, default: false },
|
|
24
|
+
defaultPinnedColumns: { type: Object, default: null }, // { left?: string[], right?: string[] }
|
|
25
|
+
persistPreferences: { type: Boolean, default: true }, // persist column prefs in backend
|
|
21
26
|
})
|
|
22
27
|
|
|
23
28
|
const resolvedEndpoint = computed(() => props.table?.endpoint ?? props.endpoint)
|
|
24
29
|
const resolvedName = computed(() => props.table?.name ?? props.name)
|
|
25
30
|
|
|
26
|
-
|
|
31
|
+
// ─── Table preferences (column pinning, visibility, order) ───────────────────
|
|
32
|
+
const tablePrefName = computed(() => resolvedName.value || 'default')
|
|
33
|
+
const { preferences: tablePrefs, load: loadPrefs, save: savePrefs } = useTablePreferences(tablePrefName.value)
|
|
34
|
+
|
|
35
|
+
// Resolved initial pinned columns: merge defaultPinnedColumns with saved preferences
|
|
36
|
+
const resolvedPinnedColumns = computed(() => {
|
|
37
|
+
const saved = tablePrefs.value.pinning
|
|
38
|
+
if (saved) return saved
|
|
39
|
+
return props.defaultPinnedColumns ?? null
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const emit = defineEmits(['row-click', 'loaded', 'preview-delete'])
|
|
27
43
|
const slots = useSlots()
|
|
28
44
|
const forwardedSlots = computed(() => {
|
|
29
45
|
const excluded = new Set(['toolbar', 'preview'])
|
|
@@ -33,6 +49,7 @@ const forwardedSlots = computed(() => {
|
|
|
33
49
|
const search = ref('')
|
|
34
50
|
const activeFilters = ref({})
|
|
35
51
|
const tableRef = ref(null)
|
|
52
|
+
const prefsLoaded = ref(false)
|
|
36
53
|
|
|
37
54
|
// ─── Filter config ─────────────────────────────────────────────────────────────
|
|
38
55
|
const filtersConfig = computed(() =>
|
|
@@ -51,6 +68,20 @@ const filterMenuRef = ref(null)
|
|
|
51
68
|
const filterAddBtnRef = ref(null)
|
|
52
69
|
const filterMenuStyle = ref({})
|
|
53
70
|
|
|
71
|
+
const pendingOperator = ref('contains')
|
|
72
|
+
const pendingValueInputRef = ref(null)
|
|
73
|
+
|
|
74
|
+
const textOps = [
|
|
75
|
+
{ value: 'contains', label: 'contiene' },
|
|
76
|
+
{ value: 'starts_with', label: 'empieza con' },
|
|
77
|
+
{ value: 'equals', label: 'es igual a' },
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
const selectOps = [
|
|
81
|
+
{ value: 'is', label: 'es' },
|
|
82
|
+
{ value: 'is_not', label: 'no es' },
|
|
83
|
+
]
|
|
84
|
+
|
|
54
85
|
const dateOps = [
|
|
55
86
|
{ value: 'before', label: 'antes de' },
|
|
56
87
|
{ value: 'after', label: 'después de' },
|
|
@@ -61,8 +92,9 @@ const activeFilterList = computed(() =>
|
|
|
61
92
|
filtersConfig.value
|
|
62
93
|
.filter(col => {
|
|
63
94
|
const v = activeFilters.value[col.key]
|
|
95
|
+
if (!v) return false
|
|
64
96
|
if (col.filterType === 'daterange') return v?.from || v?.to
|
|
65
|
-
return v !== null && v !== undefined && v !== ''
|
|
97
|
+
return v?.value !== null && v?.value !== undefined && v?.value !== ''
|
|
66
98
|
})
|
|
67
99
|
.map(col => {
|
|
68
100
|
const v = activeFilters.value[col.key]
|
|
@@ -72,10 +104,13 @@ const activeFilterList = computed(() =>
|
|
|
72
104
|
else if (v.from) { displayOp = 'después de'; displayVal = v.from }
|
|
73
105
|
else { displayOp = 'antes de'; displayVal = v.to }
|
|
74
106
|
} else if (col.filterType === 'select') {
|
|
75
|
-
|
|
76
|
-
|
|
107
|
+
const op = selectOps.find(o => o.value === v.operator) ?? selectOps[0]
|
|
108
|
+
displayOp = op.label
|
|
109
|
+
displayVal = col.filterOptions?.find(o => o.value === v.value)?.label ?? v.value
|
|
77
110
|
} else {
|
|
78
|
-
|
|
111
|
+
const op = textOps.find(o => o.value === v.operator) ?? textOps[0]
|
|
112
|
+
displayOp = op.label
|
|
113
|
+
displayVal = v.value
|
|
79
114
|
}
|
|
80
115
|
return { key: col.key, label: col.label, displayOp, displayVal, col }
|
|
81
116
|
})
|
|
@@ -87,8 +122,9 @@ const activeFilterCount = computed(() => activeFilterList.value.length)
|
|
|
87
122
|
const availableFilterColumns = computed(() =>
|
|
88
123
|
filtersConfig.value.filter(col => {
|
|
89
124
|
const v = activeFilters.value[col.key]
|
|
90
|
-
if (
|
|
91
|
-
|
|
125
|
+
if (!v) return true
|
|
126
|
+
if (col.filterType === 'daterange') return !v.from && !v.to
|
|
127
|
+
return !v.value
|
|
92
128
|
})
|
|
93
129
|
)
|
|
94
130
|
|
|
@@ -97,15 +133,14 @@ const enrichedFilters = computed(() => {
|
|
|
97
133
|
const result = []
|
|
98
134
|
for (const col of filtersConfig.value) {
|
|
99
135
|
const v = activeFilters.value[col.key]
|
|
100
|
-
if (v
|
|
136
|
+
if (!v) continue
|
|
101
137
|
if (col.filterType === 'daterange') {
|
|
102
|
-
if (!v
|
|
138
|
+
if (!v.from && !v.to) continue
|
|
103
139
|
if (v.from) result.push({ field: col.key, operator: 'after', value: v.from })
|
|
104
140
|
if (v.to) result.push({ field: col.key, operator: 'before', value: v.to })
|
|
105
|
-
} else if (col.filterType === 'select') {
|
|
106
|
-
result.push({ field: col.key, operator: 'is', value: v })
|
|
107
141
|
} else {
|
|
108
|
-
|
|
142
|
+
if (!v.value) continue
|
|
143
|
+
result.push({ field: col.key, operator: v.operator ?? 'contains', value: v.value })
|
|
109
144
|
}
|
|
110
145
|
}
|
|
111
146
|
return result
|
|
@@ -120,13 +155,17 @@ const removeFilter = (key) => {
|
|
|
120
155
|
const u = { ...activeFilters.value }; delete u[key]; activeFilters.value = u
|
|
121
156
|
}
|
|
122
157
|
|
|
158
|
+
const updateFilterMenuPosition = () => {
|
|
159
|
+
const rect = filterAddBtnRef.value?.getBoundingClientRect()
|
|
160
|
+
if (rect) filterMenuStyle.value = { top: rect.bottom + 4 + 'px', left: rect.left + 'px' }
|
|
161
|
+
}
|
|
162
|
+
|
|
123
163
|
const openFilterMenu = async () => {
|
|
124
164
|
filterMenuStep.value = 'columns'
|
|
125
165
|
pendingCol.value = null
|
|
126
166
|
showFilterPanel.value = true
|
|
127
167
|
await nextTick()
|
|
128
|
-
|
|
129
|
-
if (rect) filterMenuStyle.value = { top: rect.bottom + 4 + 'px', left: rect.left + 'px' }
|
|
168
|
+
updateFilterMenuPosition()
|
|
130
169
|
}
|
|
131
170
|
|
|
132
171
|
const toggleFilterMenu = async () => {
|
|
@@ -149,7 +188,8 @@ const selectFilterColumn = (col) => {
|
|
|
149
188
|
else if (existing?.to) { pendingDateOp.value = 'before'; pendingValue.value = { singleDate: existing.to, from: '', to: '' } }
|
|
150
189
|
else { pendingDateOp.value = 'before'; pendingValue.value = { singleDate: '', from: '', to: '' } }
|
|
151
190
|
} else {
|
|
152
|
-
|
|
191
|
+
pendingOperator.value = existing?.operator ?? (col.filterType === 'select' ? 'is' : 'contains')
|
|
192
|
+
pendingValue.value = existing?.value ?? ''
|
|
153
193
|
}
|
|
154
194
|
filterMenuStep.value = 'value'
|
|
155
195
|
}
|
|
@@ -163,9 +203,9 @@ const applyPendingFilter = () => {
|
|
|
163
203
|
else if (pendingDateOp.value === 'after') v = { from: pendingValue.value.singleDate }
|
|
164
204
|
else v = { to: pendingValue.value.singleDate }
|
|
165
205
|
} else {
|
|
166
|
-
v = pendingValue.value
|
|
206
|
+
v = { value: pendingValue.value, operator: pendingOperator.value }
|
|
167
207
|
}
|
|
168
|
-
activeFilters.value = { ...activeFilters.value, [col.key]: v
|
|
208
|
+
activeFilters.value = { ...activeFilters.value, [col.key]: v }
|
|
169
209
|
closeFilterMenu()
|
|
170
210
|
}
|
|
171
211
|
|
|
@@ -184,8 +224,30 @@ const onFilterMenuOutsideClick = (e) => {
|
|
|
184
224
|
}
|
|
185
225
|
}
|
|
186
226
|
watch(showFilterPanel, v => {
|
|
187
|
-
if (v)
|
|
188
|
-
|
|
227
|
+
if (v) {
|
|
228
|
+
document.addEventListener('mousedown', onFilterMenuOutsideClick)
|
|
229
|
+
window.addEventListener('scroll', updateFilterMenuPosition, true)
|
|
230
|
+
window.addEventListener('resize', updateFilterMenuPosition)
|
|
231
|
+
} else {
|
|
232
|
+
document.removeEventListener('mousedown', onFilterMenuOutsideClick)
|
|
233
|
+
window.removeEventListener('scroll', updateFilterMenuPosition, true)
|
|
234
|
+
window.removeEventListener('resize', updateFilterMenuPosition)
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
watch(filterMenuStep, async (step) => {
|
|
239
|
+
if (step === 'value' && pendingCol.value?.filterType === 'text') {
|
|
240
|
+
await nextTick()
|
|
241
|
+
pendingValueInputRef.value?.focus()
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
// ─── Preview href helper ───────────────────────────────────────────────────────
|
|
246
|
+
const resolvedPreviewHref = computed(() => {
|
|
247
|
+
if (!props.previewHref || !previewRow.value) return null
|
|
248
|
+
return typeof props.previewHref === 'function'
|
|
249
|
+
? props.previewHref(previewRow.value)
|
|
250
|
+
: props.previewHref
|
|
189
251
|
})
|
|
190
252
|
|
|
191
253
|
// ─── Preview panel ─────────────────────────────────────────────────────────────
|
|
@@ -210,12 +272,19 @@ const resolvedHistoryEndpoint = computed(() => {
|
|
|
210
272
|
return `history/${tableMeta.value.entity_type}/${previewRow.value.id}`
|
|
211
273
|
})
|
|
212
274
|
|
|
213
|
-
watch(previewRow, () => {
|
|
275
|
+
watch(previewRow, async (row) => {
|
|
276
|
+
previewTab.value = 'datos'
|
|
277
|
+
if (row) {
|
|
278
|
+
await nextTick()
|
|
279
|
+
if (typeof window !== 'undefined') window.HSStaticMethods?.autoInit?.(['Tooltip'])
|
|
280
|
+
}
|
|
281
|
+
})
|
|
214
282
|
|
|
215
283
|
const handleRowClick = (row) => {
|
|
216
284
|
if (previewEnabled.value) {
|
|
285
|
+
if (previewRow.value?.id === row.id) return // ya está abierto, no pestañear
|
|
217
286
|
collapseDock()
|
|
218
|
-
previewRow.value =
|
|
287
|
+
previewRow.value = row
|
|
219
288
|
} else {
|
|
220
289
|
emit('row-click', row)
|
|
221
290
|
}
|
|
@@ -348,6 +417,24 @@ onMounted(async () => {
|
|
|
348
417
|
}
|
|
349
418
|
} catch {}
|
|
350
419
|
}
|
|
420
|
+
|
|
421
|
+
// Load column preferences from backend
|
|
422
|
+
if (props.persistPreferences && resolvedName.value) {
|
|
423
|
+
await loadPrefs()
|
|
424
|
+
// Apply saved visibility
|
|
425
|
+
if (tablePrefs.value.visibility && tableRef.value?.table) {
|
|
426
|
+
tableRef.value.table.setColumnVisibility(tablePrefs.value.visibility)
|
|
427
|
+
}
|
|
428
|
+
// Apply saved column order
|
|
429
|
+
if (tablePrefs.value.order?.length && tableRef.value?.setColumnOrder) {
|
|
430
|
+
tableRef.value.setColumnOrder(tablePrefs.value.order)
|
|
431
|
+
}
|
|
432
|
+
// Apply saved pinning
|
|
433
|
+
if (tablePrefs.value.pinning && tableRef.value?.table) {
|
|
434
|
+
tableRef.value.table.setColumnPinning(tablePrefs.value.pinning)
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
prefsLoaded.value = true
|
|
351
438
|
})
|
|
352
439
|
onBeforeUnmount(() => {
|
|
353
440
|
window.removeEventListener('keydown', onEsc)
|
|
@@ -368,24 +455,105 @@ const orderedColumns = computed(() => {
|
|
|
368
455
|
})
|
|
369
456
|
|
|
370
457
|
let draggedKey = null
|
|
371
|
-
|
|
458
|
+
let draggedFromSection = null // 'left' | 'center' | 'right'
|
|
459
|
+
const dragOverKey = ref(null)
|
|
460
|
+
const dragOverSection = ref(null)
|
|
461
|
+
|
|
462
|
+
// ─── Columns grouped by pinning section ───────────────────────────────────────
|
|
463
|
+
const columnsBySection = computed(() => {
|
|
464
|
+
// reactive dependency on pinning state
|
|
465
|
+
const _pin = tableRef.value?.columnPinning?.value
|
|
466
|
+
const cols = orderedColumns.value
|
|
467
|
+
if (!tableRef.value?.table) return { left: [], center: cols, right: [] }
|
|
468
|
+
|
|
469
|
+
const left = [], center = [], right = []
|
|
470
|
+
for (const col of cols) {
|
|
471
|
+
const pinned = tableRef.value.table.getColumn(col.key)?.getIsPinned()
|
|
472
|
+
if (pinned === 'left') left.push(col)
|
|
473
|
+
else if (pinned === 'right') right.push(col)
|
|
474
|
+
else center.push(col)
|
|
475
|
+
}
|
|
476
|
+
return { left, center, right }
|
|
477
|
+
})
|
|
372
478
|
|
|
373
|
-
const
|
|
374
|
-
const onDragOver = (e, key) => { e.preventDefault(); dragOverKey.value = key }
|
|
375
|
-
const onDragLeave = () => { dragOverKey.value = null }
|
|
376
|
-
const onDrop = (key) => {
|
|
377
|
-
if (!draggedKey || draggedKey === key) return
|
|
378
|
-
const ids = tableRef.value?.table.getAllLeafColumns().map(c => c.id) ?? []
|
|
379
|
-
const from = ids.indexOf(draggedKey)
|
|
380
|
-
const to = ids.indexOf(key)
|
|
381
|
-
if (from < 0 || to < 0) return
|
|
382
|
-
ids.splice(from, 1)
|
|
383
|
-
ids.splice(to, 0, draggedKey)
|
|
384
|
-
const selIdx = ids.indexOf('select')
|
|
385
|
-
if (selIdx > 0) { ids.splice(selIdx, 1); ids.unshift('select') }
|
|
386
|
-
tableRef.value?.setColumnOrder(ids)
|
|
479
|
+
const resetColDrag = () => {
|
|
387
480
|
draggedKey = null
|
|
481
|
+
draggedFromSection = null
|
|
388
482
|
dragOverKey.value = null
|
|
483
|
+
dragOverSection.value = null
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const onDragStart = (key, section) => { draggedKey = key; draggedFromSection = section }
|
|
487
|
+
const onDragLeave = () => { dragOverKey.value = null }
|
|
488
|
+
|
|
489
|
+
// Auto-pin anchor columns when any column enters/leaves a pinned section:
|
|
490
|
+
// Left section → checkbox (select) is always pinned left
|
|
491
|
+
// Right section → actions column is always pinned right
|
|
492
|
+
// Called AFTER pinColumn(draggedKey) so columnsBySection reflects the new state.
|
|
493
|
+
const enforceAnchorPins = (targetSection) => {
|
|
494
|
+
const t = tableRef.value
|
|
495
|
+
if (!t) return
|
|
496
|
+
const from = draggedFromSection // still valid before resetColDrag()
|
|
497
|
+
|
|
498
|
+
// ─── Left anchor: select checkbox ────────────────────────────────────────────
|
|
499
|
+
// Order (select always first) is enforced by a watch in Table/index.vue
|
|
500
|
+
if (props.checkable && t.table?.getColumn('select')) {
|
|
501
|
+
if (targetSection === 'left') {
|
|
502
|
+
t.pinColumn('select', 'left')
|
|
503
|
+
} else if (from === 'left' && columnsBySection.value.left.length === 0) {
|
|
504
|
+
t.pinColumn('select', false)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ─── Right anchor: actions ────────────────────────────────────────────────────
|
|
509
|
+
// 'actions' has label:'' so it's excluded from orderedColumns/columnsBySection.
|
|
510
|
+
// We only auto-pin it if it exists in the columns definition.
|
|
511
|
+
const hasActions = props.columns.some(c => c.key === 'actions')
|
|
512
|
+
if (hasActions) {
|
|
513
|
+
if (targetSection === 'right') {
|
|
514
|
+
// Something was pinned right → force-pin actions too
|
|
515
|
+
t.pinColumn('actions', 'right')
|
|
516
|
+
} else if (from === 'right') {
|
|
517
|
+
// Something left the right section → unpin actions if no more right columns
|
|
518
|
+
if (columnsBySection.value.right.length === 0) t.pinColumn('actions', false)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Drop on a specific column row (handles both reorder + section change)
|
|
524
|
+
const onDrop = (targetKey, targetSection) => {
|
|
525
|
+
if (!draggedKey) { resetColDrag(); return }
|
|
526
|
+
|
|
527
|
+
if (draggedFromSection !== targetSection) {
|
|
528
|
+
// Change pinning
|
|
529
|
+
const pinVal = targetSection === 'left' ? 'left' : targetSection === 'right' ? 'right' : false
|
|
530
|
+
tableRef.value?.pinColumn(draggedKey, pinVal)
|
|
531
|
+
enforceAnchorPins(targetSection)
|
|
532
|
+
persistCurrentPrefs()
|
|
533
|
+
} else if (draggedKey !== targetKey) {
|
|
534
|
+
// Reorder within section
|
|
535
|
+
const ids = tableRef.value?.table.getAllLeafColumns().map(c => c.id) ?? []
|
|
536
|
+
const from = ids.indexOf(draggedKey)
|
|
537
|
+
const to = ids.indexOf(targetKey)
|
|
538
|
+
if (from >= 0 && to >= 0) {
|
|
539
|
+
ids.splice(from, 1)
|
|
540
|
+
ids.splice(to, 0, draggedKey)
|
|
541
|
+
const selIdx = ids.indexOf('select')
|
|
542
|
+
if (selIdx > 0) { ids.splice(selIdx, 1); ids.unshift('select') }
|
|
543
|
+
tableRef.value?.setColumnOrder(ids)
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
resetColDrag()
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Drop on the section zone itself (empty area) — only changes pinning
|
|
550
|
+
const onDropSection = (targetSection) => {
|
|
551
|
+
if (!draggedKey || draggedFromSection === targetSection) { resetColDrag(); return }
|
|
552
|
+
const pinVal = targetSection === 'left' ? 'left' : targetSection === 'right' ? 'right' : false
|
|
553
|
+
tableRef.value?.pinColumn(draggedKey, pinVal)
|
|
554
|
+
enforceAnchorPins(targetSection)
|
|
555
|
+
persistCurrentPrefs()
|
|
556
|
+
resetColDrag()
|
|
389
557
|
}
|
|
390
558
|
|
|
391
559
|
const onColumnPanelOutsideClick = (e) => {
|
|
@@ -397,29 +565,74 @@ const onColumnPanelOutsideClick = (e) => {
|
|
|
397
565
|
}
|
|
398
566
|
}
|
|
399
567
|
|
|
568
|
+
const updateColumnPanelPosition = () => {
|
|
569
|
+
const rect = columnButtonRef.value?.getBoundingClientRect()
|
|
570
|
+
if (rect) columnPanelStyle.value = { top: rect.bottom + 6 + 'px', right: window.innerWidth - rect.right + 'px' }
|
|
571
|
+
}
|
|
572
|
+
|
|
400
573
|
watch(showColumnPanel, async (v) => {
|
|
401
574
|
if (v) {
|
|
402
575
|
await nextTick()
|
|
403
|
-
|
|
404
|
-
if (rect) {
|
|
405
|
-
columnPanelStyle.value = {
|
|
406
|
-
top: rect.bottom + 6 + 'px',
|
|
407
|
-
right: window.innerWidth - rect.right + 'px',
|
|
408
|
-
}
|
|
409
|
-
}
|
|
576
|
+
updateColumnPanelPosition()
|
|
410
577
|
document.addEventListener('mousedown', onColumnPanelOutsideClick)
|
|
578
|
+
window.addEventListener('scroll', updateColumnPanelPosition, true)
|
|
579
|
+
window.addEventListener('resize', updateColumnPanelPosition)
|
|
411
580
|
} else {
|
|
412
581
|
document.removeEventListener('mousedown', onColumnPanelOutsideClick)
|
|
582
|
+
window.removeEventListener('scroll', updateColumnPanelPosition, true)
|
|
583
|
+
window.removeEventListener('resize', updateColumnPanelPosition)
|
|
413
584
|
}
|
|
414
585
|
})
|
|
415
586
|
|
|
587
|
+
// ─── Persist column preferences when they change ─────────────────────────────
|
|
588
|
+
const persistCurrentPrefs = () => {
|
|
589
|
+
if (!props.persistPreferences || !resolvedName.value || !prefsLoaded.value || !tableRef.value) return
|
|
590
|
+
const tanTable = tableRef.value.table
|
|
591
|
+
if (!tanTable) return
|
|
592
|
+
|
|
593
|
+
const visibility = Object.fromEntries(
|
|
594
|
+
tanTable.getAllLeafColumns()
|
|
595
|
+
.filter(c => c.id !== 'select')
|
|
596
|
+
.map(c => [c.id, c.getIsVisible()])
|
|
597
|
+
)
|
|
598
|
+
const order = tanTable.getAllLeafColumns().map(c => c.id).filter(id => id !== 'select')
|
|
599
|
+
const rawPinning = tableRef.value.columnPinning?.value ?? tanTable.getState().columnPinning
|
|
600
|
+
const pinning = rawPinning
|
|
601
|
+
? { left: rawPinning.left ?? [], right: rawPinning.right ?? [] }
|
|
602
|
+
: { left: [], right: [] }
|
|
603
|
+
|
|
604
|
+
savePrefs({ visibility, order, pinning })
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Watch column pinning changes via tableRef
|
|
608
|
+
watch(
|
|
609
|
+
() => tableRef.value?.columnPinning?.value,
|
|
610
|
+
() => { if (prefsLoaded.value) persistCurrentPrefs() },
|
|
611
|
+
{ deep: true }
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
// Watch column visibility changes
|
|
615
|
+
watch(
|
|
616
|
+
() => tableRef.value?.table?.getState()?.columnVisibility,
|
|
617
|
+
() => { if (prefsLoaded.value) persistCurrentPrefs() },
|
|
618
|
+
{ deep: true }
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
// Watch column order changes
|
|
622
|
+
watch(
|
|
623
|
+
() => tableRef.value?.table?.getState()?.columnOrder,
|
|
624
|
+
() => { if (prefsLoaded.value) persistCurrentPrefs() },
|
|
625
|
+
{ deep: true }
|
|
626
|
+
)
|
|
627
|
+
|
|
416
628
|
// ─── Expose ───────────────────────────────────────────────────────────────────
|
|
417
629
|
const getSelectedRows = () => tableRef.value?.getSelectedRows()
|
|
418
630
|
const reload = () => tableRef.value?.reload()
|
|
419
631
|
const clearCache = () => tableRef.value?.clearCache()
|
|
420
632
|
const exportTable = (format, allPages, filteredRows) => tableRef.value?.exportTable(format, allPages, filteredRows)
|
|
633
|
+
const pinColumn = (key, position) => tableRef.value?.pinColumn(key, position)
|
|
421
634
|
|
|
422
|
-
defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, closePreview })
|
|
635
|
+
defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, closePreview, pinColumn })
|
|
423
636
|
</script>
|
|
424
637
|
|
|
425
638
|
<template>
|
|
@@ -441,7 +654,7 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
441
654
|
:class="[
|
|
442
655
|
'inline-flex items-center gap-1.5 py-1.5 px-3 text-sm font-medium rounded-lg border transition-colors',
|
|
443
656
|
activeFilterList.length
|
|
444
|
-
? 'border-
|
|
657
|
+
? 'border-primary/40 bg-primary/10 text-primary'
|
|
445
658
|
: 'border-card-line bg-card text-muted-foreground-1 hover:bg-muted-hover'
|
|
446
659
|
]"
|
|
447
660
|
>
|
|
@@ -463,7 +676,7 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
463
676
|
:class="[
|
|
464
677
|
'p-1.5 inline-flex items-center justify-center rounded-lg border transition-colors',
|
|
465
678
|
showColumnPanel
|
|
466
|
-
? 'border-
|
|
679
|
+
? 'border-primary/40 bg-primary/10 text-primary'
|
|
467
680
|
: 'border-transparent text-muted-foreground hover:border-card-line hover:bg-muted-hover hover:text-foreground'
|
|
468
681
|
]"
|
|
469
682
|
>
|
|
@@ -486,7 +699,7 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
486
699
|
<button
|
|
487
700
|
type="button"
|
|
488
701
|
@click.stop="openEditFilter(chip.col)"
|
|
489
|
-
class="inline-flex items-center gap-1 px-2 py-1 text-
|
|
702
|
+
class="inline-flex items-center gap-1 px-2 py-1 text-primary font-medium hover:bg-primary/10 transition-colors border-x border-card-line"
|
|
490
703
|
>
|
|
491
704
|
{{ chip.displayVal }}
|
|
492
705
|
<IconChevronDown class="size-3 opacity-60" />
|
|
@@ -501,10 +714,8 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
501
714
|
</div>
|
|
502
715
|
</div>
|
|
503
716
|
|
|
504
|
-
<!--
|
|
505
|
-
<div class="
|
|
506
|
-
|
|
507
|
-
<!-- Tabla -->
|
|
717
|
+
<!-- Tabla -->
|
|
718
|
+
<div class="overflow-hidden border-t border-b border-card-line">
|
|
508
719
|
<Table
|
|
509
720
|
ref="tableRef"
|
|
510
721
|
:endpoint="resolvedEndpoint"
|
|
@@ -518,6 +729,7 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
518
729
|
:click-row-to-open="clickRowToOpen"
|
|
519
730
|
:preview-row-id="previewRow?.id ?? null"
|
|
520
731
|
:preview-mode="!!previewEnabled"
|
|
732
|
+
:pinned-columns="resolvedPinnedColumns"
|
|
521
733
|
@row-click="handleRowClick"
|
|
522
734
|
@loaded="handleLoaded"
|
|
523
735
|
@page-change="closePreview"
|
|
@@ -527,100 +739,134 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
527
739
|
<slot :name="name" v-bind="slotProps ?? {}" />
|
|
528
740
|
</template>
|
|
529
741
|
</Table>
|
|
742
|
+
</div>
|
|
530
743
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
744
|
+
<!-- Preview panel — overlay deslizante desde la derecha (cubre todo el componente) -->
|
|
745
|
+
<Transition
|
|
746
|
+
:enter-active-class="previewFromCache ? '' : 'transition ease-out duration-200'"
|
|
747
|
+
:enter-from-class="previewFromCache ? '' : 'opacity-0 translate-x-6'"
|
|
748
|
+
:enter-to-class="previewFromCache ? '' : 'opacity-100 translate-x-0'"
|
|
749
|
+
leave-active-class="transition ease-in duration-150"
|
|
750
|
+
leave-from-class="opacity-100 translate-x-0"
|
|
751
|
+
leave-to-class="opacity-0 translate-x-6"
|
|
752
|
+
>
|
|
753
|
+
<div
|
|
754
|
+
v-if="previewRow && previewEnabled"
|
|
755
|
+
ref="previewPanelRef"
|
|
756
|
+
class="absolute top-0 right-0 bottom-0 z-30 flex flex-col bg-card border border-card-line rounded-2xl shadow-xl overflow-hidden"
|
|
757
|
+
:style="{ width: (100 - currentRatio) + '%' }"
|
|
539
758
|
>
|
|
759
|
+
<!-- Resize handle — thin pill on left edge -->
|
|
540
760
|
<div
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
<!-- Preview -->
|
|
552
|
-
<div class="flex flex-col flex-1 overflow-hidden">
|
|
761
|
+
class="absolute left-1 top-1/2 -translate-y-1/2 h-12 w-1 cursor-col-resize rounded-full bg-border hover:bg-primary/50 transition-colors z-10"
|
|
762
|
+
@mousedown="startResize"
|
|
763
|
+
/>
|
|
764
|
+
|
|
765
|
+
<!-- Barra de acciones del preview -->
|
|
766
|
+
<div class="shrink-0 flex items-center gap-3 px-5 py-4 border-b border-card-line">
|
|
767
|
+
<!-- Título (reemplazable via slot) -->
|
|
768
|
+
<div class="flex-1 min-w-0">
|
|
769
|
+
<slot name="preview-header" :row="previewRow" :close="closePreview" />
|
|
770
|
+
</div>
|
|
553
771
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
type="button"
|
|
570
|
-
class="inline-flex items-center justify-center size-6 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors"
|
|
571
|
-
title="Cerrar"
|
|
572
|
-
@click.stop="closePreview"
|
|
573
|
-
>
|
|
574
|
-
<IconX class="size-3.5" />
|
|
575
|
-
</button>
|
|
576
|
-
</div>
|
|
772
|
+
<!-- Botones de acción — todos icon-only, mismo tamaño y estilo -->
|
|
773
|
+
<div class="flex items-center gap-0.5 shrink-0">
|
|
774
|
+
|
|
775
|
+
<!-- Acciones extra configurables desde el padre -->
|
|
776
|
+
<slot name="preview-actions" :row="previewRow" :close="closePreview" />
|
|
777
|
+
|
|
778
|
+
<!-- Abrir (configurable vía :preview-href) -->
|
|
779
|
+
<div v-if="resolvedPreviewHref" class="hs-tooltip [--placement:top] inline-block">
|
|
780
|
+
<NuxtLink
|
|
781
|
+
:to="resolvedPreviewHref"
|
|
782
|
+
class="hs-tooltip-toggle inline-flex items-center justify-center size-7 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors"
|
|
783
|
+
>
|
|
784
|
+
<IconExternalLink class="size-3.5" />
|
|
785
|
+
</NuxtLink>
|
|
786
|
+
<span class="hs-tooltip-content hs-tooltip-shown:opacity-100 hs-tooltip-shown:visible opacity-0 transition-opacity inline-block absolute invisible z-10 py-1 px-2 bg-tooltip border border-tooltip-line text-xs font-medium text-tooltip-foreground rounded-md shadow-2xs" role="tooltip">Abrir</span>
|
|
577
787
|
</div>
|
|
578
788
|
|
|
579
|
-
<!--
|
|
580
|
-
<div class="
|
|
581
|
-
<
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
789
|
+
<!-- Eliminar (configurable vía :preview-deletable) -->
|
|
790
|
+
<div v-if="previewDeletable" class="hs-tooltip [--placement:top] inline-block">
|
|
791
|
+
<button
|
|
792
|
+
type="button"
|
|
793
|
+
class="hs-tooltip-toggle inline-flex items-center justify-center size-7 rounded-md text-muted-foreground hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
|
794
|
+
@click.stop="emit('preview-delete', previewRow)"
|
|
795
|
+
>
|
|
796
|
+
<IconTrash class="size-3.5" />
|
|
797
|
+
</button>
|
|
798
|
+
<span class="hs-tooltip-content hs-tooltip-shown:opacity-100 hs-tooltip-shown:visible opacity-0 transition-opacity inline-block absolute invisible z-10 py-1 px-2 bg-tooltip border border-tooltip-line text-xs font-medium text-tooltip-foreground rounded-md shadow-2xs" role="tooltip">Eliminar</span>
|
|
586
799
|
</div>
|
|
587
800
|
|
|
588
|
-
<!--
|
|
589
|
-
<div
|
|
801
|
+
<!-- Minimizar (siempre) -->
|
|
802
|
+
<div class="hs-tooltip [--placement:top] inline-block">
|
|
590
803
|
<button
|
|
591
804
|
type="button"
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
'flex-1 py-2.5 text-xs font-semibold transition-colors border-r border-card-line border-t-2 -mt-px',
|
|
595
|
-
previewTab === 'datos'
|
|
596
|
-
? 'border-t-card text-foreground'
|
|
597
|
-
: 'border-t-transparent text-muted-foreground hover:text-foreground hover:bg-muted-hover'
|
|
598
|
-
]"
|
|
805
|
+
class="hs-tooltip-toggle inline-flex items-center justify-center size-7 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors"
|
|
806
|
+
@click.stop="minimizePreview"
|
|
599
807
|
>
|
|
600
|
-
|
|
808
|
+
<IconMinus class="size-3.5" />
|
|
601
809
|
</button>
|
|
810
|
+
<span class="hs-tooltip-content hs-tooltip-shown:opacity-100 hs-tooltip-shown:visible opacity-0 transition-opacity inline-block absolute invisible z-10 py-1 px-2 bg-tooltip border border-tooltip-line text-xs font-medium text-tooltip-foreground rounded-md shadow-2xs" role="tooltip">Minimizar</span>
|
|
811
|
+
</div>
|
|
812
|
+
|
|
813
|
+
<!-- Cerrar (siempre) -->
|
|
814
|
+
<div class="hs-tooltip [--placement:top] inline-block">
|
|
602
815
|
<button
|
|
603
816
|
type="button"
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
:class="[
|
|
607
|
-
'flex-1 py-2.5 text-xs font-semibold transition-colors border-t-2 -mt-px',
|
|
608
|
-
!resolvedHistoryEndpoint
|
|
609
|
-
? 'border-t-transparent text-muted-foreground/40 cursor-not-allowed'
|
|
610
|
-
: previewTab === 'bitacora'
|
|
611
|
-
? 'border-t-card text-foreground'
|
|
612
|
-
: 'border-t-transparent text-muted-foreground hover:text-foreground hover:bg-muted-hover'
|
|
613
|
-
]"
|
|
817
|
+
class="hs-tooltip-toggle inline-flex items-center justify-center size-7 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors"
|
|
818
|
+
@click.stop="closePreview"
|
|
614
819
|
>
|
|
615
|
-
|
|
820
|
+
<IconX class="size-3.5" />
|
|
616
821
|
</button>
|
|
822
|
+
<span class="hs-tooltip-content hs-tooltip-shown:opacity-100 hs-tooltip-shown:visible opacity-0 transition-opacity inline-block absolute invisible z-10 py-1 px-2 bg-tooltip border border-tooltip-line text-xs font-medium text-tooltip-foreground rounded-md shadow-2xs" role="tooltip">Cerrar</span>
|
|
617
823
|
</div>
|
|
618
824
|
|
|
619
825
|
</div>
|
|
620
826
|
</div>
|
|
621
|
-
</Transition>
|
|
622
827
|
|
|
623
|
-
|
|
828
|
+
<!-- Scrollable content -->
|
|
829
|
+
<div class="flex-1 overflow-y-auto min-h-0">
|
|
830
|
+
<slot v-if="previewTab === 'datos'" name="preview" :row="previewRow" :close="closePreview" />
|
|
831
|
+
<Table.PreviewTimeline
|
|
832
|
+
v-else-if="previewTab === 'bitacora' && resolvedHistoryEndpoint"
|
|
833
|
+
:endpoint="resolvedHistoryEndpoint"
|
|
834
|
+
/>
|
|
835
|
+
</div>
|
|
836
|
+
|
|
837
|
+
<!-- Tabs — bottom -->
|
|
838
|
+
<div v-if="hasHistory" class="shrink-0 flex border-t border-card-line">
|
|
839
|
+
<button
|
|
840
|
+
type="button"
|
|
841
|
+
@click="previewTab = 'datos'"
|
|
842
|
+
:class="[
|
|
843
|
+
'flex-1 py-2.5 text-xs font-semibold transition-colors border-r border-card-line border-t-2 -mt-px',
|
|
844
|
+
previewTab === 'datos'
|
|
845
|
+
? 'border-t-card text-foreground'
|
|
846
|
+
: 'border-t-transparent text-muted-foreground hover:text-foreground hover:bg-muted-hover'
|
|
847
|
+
]"
|
|
848
|
+
>
|
|
849
|
+
Datos
|
|
850
|
+
</button>
|
|
851
|
+
<button
|
|
852
|
+
type="button"
|
|
853
|
+
@click="resolvedHistoryEndpoint && (previewTab = 'bitacora')"
|
|
854
|
+
:disabled="!resolvedHistoryEndpoint"
|
|
855
|
+
:class="[
|
|
856
|
+
'flex-1 py-2.5 text-xs font-semibold transition-colors border-t-2 -mt-px',
|
|
857
|
+
!resolvedHistoryEndpoint
|
|
858
|
+
? 'border-t-transparent text-muted-foreground/40 cursor-not-allowed'
|
|
859
|
+
: previewTab === 'bitacora'
|
|
860
|
+
? 'border-t-card text-foreground'
|
|
861
|
+
: 'border-t-transparent text-muted-foreground hover:text-foreground hover:bg-muted-hover'
|
|
862
|
+
]"
|
|
863
|
+
>
|
|
864
|
+
Bitácora
|
|
865
|
+
</button>
|
|
866
|
+
</div>
|
|
867
|
+
|
|
868
|
+
</div>
|
|
869
|
+
</Transition>
|
|
624
870
|
|
|
625
871
|
<!-- ── Floating mini-preview (dock expand, estilo Gmail) ── -->
|
|
626
872
|
<Teleport to="body">
|
|
@@ -675,23 +921,28 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
675
921
|
<div
|
|
676
922
|
v-if="showFilterPanel"
|
|
677
923
|
ref="filterMenuRef"
|
|
678
|
-
class="fixed z-[60] bg-dropdown border border-dropdown-line rounded-xl shadow-2xl
|
|
924
|
+
class="fixed z-[60] bg-dropdown border border-dropdown-line rounded-xl shadow-2xl overflow-hidden"
|
|
679
925
|
:style="filterMenuStyle"
|
|
680
926
|
>
|
|
681
927
|
|
|
682
|
-
<!-- Step 1: column picker
|
|
928
|
+
<!-- Step 1: column picker -->
|
|
683
929
|
<template v-if="filterMenuStep === 'columns'">
|
|
684
|
-
<p class="text-[10px] font-
|
|
685
|
-
|
|
930
|
+
<p class="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest px-3 pt-3 pb-1.5">
|
|
931
|
+
Filtrar por
|
|
932
|
+
</p>
|
|
933
|
+
<div class="pb-2 min-w-48">
|
|
686
934
|
<template v-if="availableFilterColumns.length">
|
|
687
935
|
<button
|
|
688
936
|
v-for="col in availableFilterColumns"
|
|
689
937
|
:key="col.key"
|
|
690
938
|
type="button"
|
|
691
939
|
@click.stop="selectFilterColumn(col)"
|
|
692
|
-
class="w-full flex items-center gap-
|
|
940
|
+
class="w-full flex items-center justify-between gap-3 px-3 py-2 text-sm text-foreground hover:bg-muted-hover transition-colors text-left group"
|
|
693
941
|
>
|
|
694
|
-
{{ col.label }}
|
|
942
|
+
<span>{{ col.label }}</span>
|
|
943
|
+
<span class="text-[11px] text-muted-foreground-2 group-hover:text-muted-foreground transition-colors capitalize">
|
|
944
|
+
{{ col.filterType === 'daterange' ? 'fecha' : col.filterType === 'select' ? 'opción' : 'texto' }}
|
|
945
|
+
</span>
|
|
695
946
|
</button>
|
|
696
947
|
</template>
|
|
697
948
|
<p v-else class="px-3 py-3 text-xs text-muted-foreground italic">
|
|
@@ -700,84 +951,109 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
700
951
|
</div>
|
|
701
952
|
</template>
|
|
702
953
|
|
|
703
|
-
<!-- Step 2: value
|
|
954
|
+
<!-- Step 2: value config -->
|
|
704
955
|
<template v-else-if="filterMenuStep === 'value' && pendingCol">
|
|
705
|
-
|
|
956
|
+
|
|
957
|
+
<!-- Header: back + field name + operator selector -->
|
|
958
|
+
<div class="flex items-center gap-1.5 px-2 py-2 border-b border-dropdown-line">
|
|
706
959
|
<button
|
|
707
960
|
type="button"
|
|
708
961
|
@click.stop="filterMenuStep = 'columns'"
|
|
709
|
-
class="text-muted-foreground hover:text-foreground transition-colors"
|
|
962
|
+
class="inline-flex items-center justify-center size-6 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors shrink-0"
|
|
710
963
|
>
|
|
711
964
|
<IconChevronLeft class="size-4" />
|
|
712
965
|
</button>
|
|
713
966
|
<span class="text-sm font-medium text-foreground">{{ pendingCol.label }}</span>
|
|
967
|
+
|
|
968
|
+
<!-- Operator: native select for text (3 options) -->
|
|
969
|
+
<select
|
|
970
|
+
v-if="pendingCol.filterType === 'text'"
|
|
971
|
+
v-model="pendingOperator"
|
|
972
|
+
class="ml-auto text-xs bg-dropdown border border-dropdown-line rounded-md px-2 py-1 text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary/50 cursor-pointer"
|
|
973
|
+
>
|
|
974
|
+
<option v-for="op in textOps" :key="op.value" :value="op.value">{{ op.label }}</option>
|
|
975
|
+
</select>
|
|
976
|
+
|
|
977
|
+
<!-- Operator: segmented toggle for select (es / no es) -->
|
|
978
|
+
<div v-else-if="pendingCol.filterType === 'select'" class="ml-auto flex rounded-md border border-dropdown-line overflow-hidden text-xs">
|
|
979
|
+
<button
|
|
980
|
+
v-for="op in selectOps"
|
|
981
|
+
:key="op.value"
|
|
982
|
+
type="button"
|
|
983
|
+
@click.stop="pendingOperator = op.value"
|
|
984
|
+
:class="[
|
|
985
|
+
'px-2.5 py-1 transition-colors',
|
|
986
|
+
pendingOperator === op.value
|
|
987
|
+
? 'bg-primary/10 text-primary'
|
|
988
|
+
: 'text-muted-foreground hover:bg-muted-hover'
|
|
989
|
+
]"
|
|
990
|
+
>
|
|
991
|
+
{{ op.label }}
|
|
992
|
+
</button>
|
|
993
|
+
</div>
|
|
994
|
+
|
|
995
|
+
<!-- Operator: inline select for daterange -->
|
|
996
|
+
<select
|
|
997
|
+
v-else-if="pendingCol.filterType === 'daterange'"
|
|
998
|
+
v-model="pendingDateOp"
|
|
999
|
+
class="ml-auto text-xs bg-dropdown border border-dropdown-line rounded-md px-2 py-1 text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary/50 cursor-pointer"
|
|
1000
|
+
>
|
|
1001
|
+
<option v-for="op in dateOps" :key="op.value" :value="op.value">{{ op.label }}</option>
|
|
1002
|
+
</select>
|
|
714
1003
|
</div>
|
|
715
|
-
<div class="p-3 space-y-2.5">
|
|
716
1004
|
|
|
717
|
-
|
|
1005
|
+
<div class="p-3 min-w-56 space-y-2">
|
|
1006
|
+
|
|
1007
|
+
<!-- ── TEXT ── -->
|
|
718
1008
|
<input
|
|
719
1009
|
v-if="pendingCol.filterType === 'text'"
|
|
1010
|
+
ref="pendingValueInputRef"
|
|
720
1011
|
v-model="pendingValue"
|
|
721
1012
|
type="text"
|
|
722
|
-
autofocus
|
|
723
1013
|
@keydown.enter.stop="applyPendingFilter"
|
|
724
1014
|
@keydown.escape.stop="closeFilterMenu"
|
|
725
|
-
placeholder="
|
|
726
|
-
class="w-full rounded-lg border border-
|
|
1015
|
+
placeholder="Valor..."
|
|
1016
|
+
class="w-full rounded-lg border border-dropdown-line bg-card text-foreground py-1.5 px-3 text-sm focus:outline-none focus:ring-1 focus:ring-primary/60 focus:border-primary"
|
|
727
1017
|
/>
|
|
728
1018
|
|
|
729
|
-
<!--
|
|
730
|
-
<div v-else-if="pendingCol.filterType === 'select'" class="space-y-0.5">
|
|
1019
|
+
<!-- ── SELECT ── -->
|
|
1020
|
+
<div v-else-if="pendingCol.filterType === 'select'" class="space-y-0.5 max-h-52 overflow-y-auto -mx-1">
|
|
731
1021
|
<button
|
|
732
1022
|
v-for="opt in pendingCol.filterOptions"
|
|
733
1023
|
:key="opt.value"
|
|
734
1024
|
type="button"
|
|
735
1025
|
@click.stop="pendingValue = opt.value; applyPendingFilter()"
|
|
736
1026
|
:class="[
|
|
737
|
-
'w-full flex items-center gap-2 px-2.5 py-
|
|
1027
|
+
'w-full flex items-center gap-2 px-2.5 py-2 text-sm rounded-lg transition-colors text-left',
|
|
738
1028
|
pendingValue === opt.value
|
|
739
|
-
? 'bg-
|
|
1029
|
+
? 'bg-primary/10 text-primary'
|
|
740
1030
|
: 'hover:bg-muted-hover text-foreground'
|
|
741
1031
|
]"
|
|
742
1032
|
>
|
|
743
1033
|
<span class="flex-1">{{ opt.label }}</span>
|
|
744
|
-
<IconCheck v-if="pendingValue === opt.value" class="size-3.5 shrink-0 text-
|
|
1034
|
+
<IconCheck v-if="pendingValue === opt.value" class="size-3.5 shrink-0 text-primary/70" />
|
|
745
1035
|
</button>
|
|
746
1036
|
</div>
|
|
747
1037
|
|
|
748
|
-
<!--
|
|
749
|
-
<
|
|
750
|
-
<div class="flex gap-1">
|
|
751
|
-
<button
|
|
752
|
-
v-for="op in dateOps"
|
|
753
|
-
:key="op.value"
|
|
754
|
-
type="button"
|
|
755
|
-
@click.stop="pendingDateOp = op.value"
|
|
756
|
-
:class="[
|
|
757
|
-
'flex-1 py-1 text-xs rounded-lg border transition-colors',
|
|
758
|
-
pendingDateOp === op.value
|
|
759
|
-
? 'border-indigo-400 bg-indigo-50 text-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-300'
|
|
760
|
-
: 'border-card-line text-muted-foreground hover:bg-muted-hover'
|
|
761
|
-
]"
|
|
762
|
-
>
|
|
763
|
-
{{ op.label }}
|
|
764
|
-
</button>
|
|
765
|
-
</div>
|
|
1038
|
+
<!-- ── DATERANGE ── -->
|
|
1039
|
+
<template v-else-if="pendingCol.filterType === 'daterange'">
|
|
766
1040
|
<template v-if="pendingDateOp === 'between'">
|
|
767
|
-
<input type="date" v-model="pendingValue.from" class="w-full rounded-lg border border-
|
|
768
|
-
<input type="date" v-model="pendingValue.to"
|
|
1041
|
+
<input type="date" v-model="pendingValue.from" class="w-full rounded-lg border border-dropdown-line bg-card text-foreground py-1.5 px-3 text-sm focus:outline-none focus:ring-1 focus:ring-primary/60" />
|
|
1042
|
+
<input type="date" v-model="pendingValue.to" class="w-full rounded-lg border border-dropdown-line bg-card text-foreground py-1.5 px-3 text-sm focus:outline-none focus:ring-1 focus:ring-primary/60" />
|
|
769
1043
|
</template>
|
|
770
|
-
<input v-else type="date" v-model="pendingValue.singleDate" class="w-full rounded-lg border border-
|
|
771
|
-
</
|
|
1044
|
+
<input v-else type="date" v-model="pendingValue.singleDate" class="w-full rounded-lg border border-dropdown-line bg-card text-foreground py-1.5 px-3 text-sm focus:outline-none focus:ring-1 focus:ring-primary/60" />
|
|
1045
|
+
</template>
|
|
772
1046
|
|
|
1047
|
+
<!-- Apply (not for select — auto-applies on click) -->
|
|
773
1048
|
<button
|
|
774
1049
|
v-if="pendingCol.filterType !== 'select'"
|
|
775
1050
|
type="button"
|
|
776
1051
|
@click.stop="applyPendingFilter"
|
|
777
|
-
class="w-full py-1.5 text-sm font-medium
|
|
1052
|
+
class="w-full py-1.5 text-sm font-medium rounded-lg bg-primary text-white hover:bg-primary/90 active:bg-primary/80 transition-colors"
|
|
778
1053
|
>
|
|
779
1054
|
Aplicar
|
|
780
1055
|
</button>
|
|
1056
|
+
|
|
781
1057
|
</div>
|
|
782
1058
|
</template>
|
|
783
1059
|
|
|
@@ -798,35 +1074,142 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
798
1074
|
<div
|
|
799
1075
|
v-if="showColumnPanel"
|
|
800
1076
|
ref="columnPanelRef"
|
|
801
|
-
class="fixed z-50 bg-dropdown border border-dropdown-line rounded-xl shadow-2xl
|
|
1077
|
+
class="fixed z-50 bg-dropdown border border-dropdown-line rounded-xl shadow-2xl min-w-64 max-h-[480px] overflow-y-auto"
|
|
802
1078
|
:style="columnPanelStyle"
|
|
803
1079
|
>
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1080
|
+
|
|
1081
|
+
<!-- ── Sección: Fija a la izquierda ── -->
|
|
1082
|
+
<div class="p-2 pb-1">
|
|
1083
|
+
<p class="text-[10px] font-bold text-muted-foreground uppercase tracking-widest px-1 pb-1.5 flex items-center gap-1.5">
|
|
1084
|
+
<span class="size-1.5 rounded-full bg-indigo-400 inline-block"></span>
|
|
1085
|
+
Fija a la izquierda
|
|
1086
|
+
</p>
|
|
1087
|
+
<div
|
|
1088
|
+
class="rounded-lg min-h-[34px] transition-colors"
|
|
1089
|
+
:class="dragOverSection === 'left' && draggedKey && !columnsBySection.left.find(c => c.key === draggedKey)
|
|
1090
|
+
? 'bg-indigo-50 dark:bg-indigo-900/20 ring-1 ring-indigo-300 dark:ring-indigo-700'
|
|
1091
|
+
: columnsBySection.left.length === 0 ? 'border border-dashed border-card-line' : ''"
|
|
1092
|
+
@dragover.prevent="dragOverSection = 'left'"
|
|
1093
|
+
@dragleave="dragOverSection = null"
|
|
1094
|
+
@drop.stop="onDropSection('left')"
|
|
1095
|
+
>
|
|
1096
|
+
<p v-if="columnsBySection.left.length === 0" class="flex items-center justify-center h-[34px] text-xs text-muted-foreground-2 italic select-none">
|
|
1097
|
+
Arrastra columnas aquí
|
|
1098
|
+
</p>
|
|
1099
|
+
<div
|
|
1100
|
+
v-for="col in columnsBySection.left"
|
|
1101
|
+
:key="col.key"
|
|
1102
|
+
draggable="true"
|
|
1103
|
+
@dragstart="onDragStart(col.key, 'left')"
|
|
1104
|
+
@dragover.prevent="dragOverSection = 'left'; dragOverKey = col.key"
|
|
1105
|
+
@dragleave="dragOverKey = null"
|
|
1106
|
+
@drop.stop="onDrop(col.key, 'left')"
|
|
1107
|
+
class="flex items-center gap-2 py-1.5 px-2 rounded-lg select-none cursor-grab transition-colors"
|
|
1108
|
+
:class="dragOverKey === col.key ? 'bg-indigo-50 dark:bg-indigo-900/20' : 'hover:bg-muted-hover'"
|
|
1109
|
+
>
|
|
1110
|
+
<IconGripVertical class="size-4 text-muted-foreground-2 shrink-0" />
|
|
1111
|
+
<input
|
|
1112
|
+
type="checkbox"
|
|
1113
|
+
:checked="tableRef?.table.getColumn(col.key)?.getIsVisible() ?? true"
|
|
1114
|
+
@change="tableRef?.table.getColumn(col.key)?.toggleVisibility(); persistCurrentPrefs()"
|
|
1115
|
+
@click.stop
|
|
1116
|
+
class="rounded border-card-line bg-surface shrink-0 cursor-pointer"
|
|
1117
|
+
/>
|
|
1118
|
+
<span class="text-sm text-foreground truncate flex-1">{{ col.label }}</span>
|
|
1119
|
+
<span class="size-1.5 rounded-full bg-indigo-400 shrink-0 opacity-60" />
|
|
1120
|
+
</div>
|
|
1121
|
+
</div>
|
|
829
1122
|
</div>
|
|
1123
|
+
|
|
1124
|
+
<div class="mx-3 border-t border-dropdown-line" />
|
|
1125
|
+
|
|
1126
|
+
<!-- ── Sección: Columnas libres ── -->
|
|
1127
|
+
<div class="p-2 py-1">
|
|
1128
|
+
<p class="text-[10px] font-bold text-muted-foreground uppercase tracking-widest px-1 pb-1.5 flex items-center gap-1.5">
|
|
1129
|
+
<span class="size-1.5 rounded-full bg-muted-foreground-2 inline-block"></span>
|
|
1130
|
+
Columnas
|
|
1131
|
+
</p>
|
|
1132
|
+
<div
|
|
1133
|
+
class="rounded-lg min-h-[34px] transition-colors"
|
|
1134
|
+
:class="dragOverSection === 'center' && draggedKey && !columnsBySection.center.find(c => c.key === draggedKey)
|
|
1135
|
+
? 'bg-muted/60 ring-1 ring-border'
|
|
1136
|
+
: ''"
|
|
1137
|
+
@dragover.prevent="dragOverSection = 'center'"
|
|
1138
|
+
@dragleave="dragOverSection = null"
|
|
1139
|
+
@drop.stop="onDropSection('center')"
|
|
1140
|
+
>
|
|
1141
|
+
<p v-if="columnsBySection.center.length === 0" class="flex items-center justify-center h-[34px] text-xs text-muted-foreground-2 italic select-none">
|
|
1142
|
+
Sin columnas libres
|
|
1143
|
+
</p>
|
|
1144
|
+
<div
|
|
1145
|
+
v-for="col in columnsBySection.center"
|
|
1146
|
+
:key="col.key"
|
|
1147
|
+
draggable="true"
|
|
1148
|
+
@dragstart="onDragStart(col.key, 'center')"
|
|
1149
|
+
@dragover.prevent="dragOverSection = 'center'; dragOverKey = col.key"
|
|
1150
|
+
@dragleave="dragOverKey = null"
|
|
1151
|
+
@drop.stop="onDrop(col.key, 'center')"
|
|
1152
|
+
class="flex items-center gap-2 py-1.5 px-2 rounded-lg select-none cursor-grab transition-colors"
|
|
1153
|
+
:class="dragOverKey === col.key ? 'bg-blue-50 dark:bg-blue-900/20 ring-1 ring-blue-200 dark:ring-blue-700' : 'hover:bg-muted-hover'"
|
|
1154
|
+
>
|
|
1155
|
+
<IconGripVertical class="size-4 text-muted-foreground-2 shrink-0" />
|
|
1156
|
+
<input
|
|
1157
|
+
type="checkbox"
|
|
1158
|
+
:checked="tableRef?.table.getColumn(col.key)?.getIsVisible() ?? true"
|
|
1159
|
+
@change="tableRef?.table.getColumn(col.key)?.toggleVisibility(); persistCurrentPrefs()"
|
|
1160
|
+
@click.stop
|
|
1161
|
+
class="rounded border-card-line bg-surface shrink-0 cursor-pointer"
|
|
1162
|
+
/>
|
|
1163
|
+
<span class="text-sm text-foreground truncate flex-1">{{ col.label }}</span>
|
|
1164
|
+
</div>
|
|
1165
|
+
</div>
|
|
1166
|
+
</div>
|
|
1167
|
+
|
|
1168
|
+
<div class="mx-3 border-t border-dropdown-line" />
|
|
1169
|
+
|
|
1170
|
+
<!-- ── Sección: Fija a la derecha ── -->
|
|
1171
|
+
<div class="p-2 pt-1">
|
|
1172
|
+
<p class="text-[10px] font-bold text-muted-foreground uppercase tracking-widest px-1 pb-1.5 flex items-center gap-1.5">
|
|
1173
|
+
<span class="size-1.5 rounded-full bg-amber-400 inline-block"></span>
|
|
1174
|
+
Fija a la derecha
|
|
1175
|
+
</p>
|
|
1176
|
+
<div
|
|
1177
|
+
class="rounded-lg min-h-[34px] transition-colors"
|
|
1178
|
+
:class="dragOverSection === 'right' && draggedKey && !columnsBySection.right.find(c => c.key === draggedKey)
|
|
1179
|
+
? 'bg-amber-50 dark:bg-amber-900/20 ring-1 ring-amber-300 dark:ring-amber-700'
|
|
1180
|
+
: columnsBySection.right.length === 0 ? 'border border-dashed border-card-line' : ''"
|
|
1181
|
+
@dragover.prevent="dragOverSection = 'right'"
|
|
1182
|
+
@dragleave="dragOverSection = null"
|
|
1183
|
+
@drop.stop="onDropSection('right')"
|
|
1184
|
+
>
|
|
1185
|
+
<p v-if="columnsBySection.right.length === 0" class="flex items-center justify-center h-[34px] text-xs text-muted-foreground-2 italic select-none">
|
|
1186
|
+
Arrastra columnas aquí
|
|
1187
|
+
</p>
|
|
1188
|
+
<div
|
|
1189
|
+
v-for="col in columnsBySection.right"
|
|
1190
|
+
:key="col.key"
|
|
1191
|
+
draggable="true"
|
|
1192
|
+
@dragstart="onDragStart(col.key, 'right')"
|
|
1193
|
+
@dragover.prevent="dragOverSection = 'right'; dragOverKey = col.key"
|
|
1194
|
+
@dragleave="dragOverKey = null"
|
|
1195
|
+
@drop.stop="onDrop(col.key, 'right')"
|
|
1196
|
+
class="flex items-center gap-2 py-1.5 px-2 rounded-lg select-none cursor-grab transition-colors"
|
|
1197
|
+
:class="dragOverKey === col.key ? 'bg-amber-50 dark:bg-amber-900/20' : 'hover:bg-muted-hover'"
|
|
1198
|
+
>
|
|
1199
|
+
<IconGripVertical class="size-4 text-muted-foreground-2 shrink-0" />
|
|
1200
|
+
<input
|
|
1201
|
+
type="checkbox"
|
|
1202
|
+
:checked="tableRef?.table.getColumn(col.key)?.getIsVisible() ?? true"
|
|
1203
|
+
@change="tableRef?.table.getColumn(col.key)?.toggleVisibility(); persistCurrentPrefs()"
|
|
1204
|
+
@click.stop
|
|
1205
|
+
class="rounded border-card-line bg-surface shrink-0 cursor-pointer"
|
|
1206
|
+
/>
|
|
1207
|
+
<span class="text-sm text-foreground truncate flex-1">{{ col.label }}</span>
|
|
1208
|
+
<span class="size-1.5 rounded-full bg-amber-400 shrink-0 opacity-60" />
|
|
1209
|
+
</div>
|
|
1210
|
+
</div>
|
|
1211
|
+
</div>
|
|
1212
|
+
|
|
830
1213
|
</div>
|
|
831
1214
|
</Transition>
|
|
832
1215
|
</Teleport>
|