382 lines
13 KiB
Vue
382 lines
13 KiB
Vue
<template>
|
||
<div class="flex flex-col gap-6">
|
||
<UCard class="brand-card border border-transparent backdrop-blur-sm">
|
||
<template #header>
|
||
<div class="flex flex-col gap-2">
|
||
<h2 class="text-xl font-semibold text-[var(--brand-text)]">Explorador de Datos</h2>
|
||
<p class="text-sm text-[var(--brand-text-muted)]">
|
||
Explore y analice los datos de las tablas disponibles con funciones avanzadas de filtrado y ordenamiento.
|
||
</p>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Table Selection and Refresh Controls -->
|
||
<div class="flex flex-col gap-4">
|
||
<!-- Table Selector -->
|
||
<UFieldGroup>
|
||
<UButton
|
||
:label="selectedTable ? selectedTable.label : 'Seleccionar tabla'"
|
||
:icon="selectedTable ? 'i-lucide-table' : 'i-lucide-loader-circle'"
|
||
color="neutral"
|
||
variant="subtle"
|
||
:loading="metadataStore.loading && !metadataStore.hasMetadata"
|
||
/>
|
||
|
||
<UDropdownMenu :items="tableDropdownItems" :loading="metadataStore.loading && !metadataStore.hasMetadata">
|
||
<UButton
|
||
color="neutral"
|
||
variant="outline"
|
||
icon="i-lucide-chevron-down"
|
||
:disabled="metadataStore.loading && !metadataStore.hasMetadata"
|
||
/>
|
||
</UDropdownMenu>
|
||
</UFieldGroup>
|
||
|
||
<!-- Refresh Controls (only shown when table is selected) -->
|
||
<div v-if="selectedTable && currentTableStore" class="flex items-center justify-between p-3 rounded-lg bg-[#1c140c] border border-[#3a2a16]">
|
||
<div class="flex flex-col gap-1">
|
||
<span class="text-xs font-medium text-[var(--brand-text-muted)]">
|
||
Última actualización: {{ currentTableStore.formattedLastUpdated }}
|
||
</span>
|
||
<span v-if="currentTableStore.isStale" class="text-xs text-yellow-400">
|
||
⚠️ Los datos pueden estar desactualizados
|
||
</span>
|
||
</div>
|
||
|
||
<UButton
|
||
:loading="currentTableStore.isLoading"
|
||
:ui="{ base: 'bg-[#c08040] text-[#1b1209] border border-[#d99a56] hover:bg-[#d99a56] hover:border-[#f0c07c]' }"
|
||
size="sm"
|
||
@click="refreshTableData"
|
||
>
|
||
<template #leading>
|
||
<UIcon name="i-lucide-refresh-cw" :class="{ 'animate-spin': currentTableStore.isLoading }" />
|
||
</template>
|
||
Actualizar datos
|
||
</UButton>
|
||
</div>
|
||
</div>
|
||
</UCard>
|
||
|
||
<!-- Loading State -->
|
||
<UCard v-if="metadataStore.loading && !metadataStore.hasMetadata" class="brand-card border border-transparent">
|
||
<div class="flex items-center justify-center gap-3 py-10 text-[var(--brand-text-muted)]">
|
||
<span class="inline-flex h-8 w-8 animate-spin rounded-full border-2 border-[#c08040] border-t-transparent align-middle" aria-hidden="true" />
|
||
<span class="text-sm uppercase tracking-[0.3em]">Cargando metadatos...</span>
|
||
</div>
|
||
<p class="text-center text-sm text-[var(--brand-text-muted)] mt-2">
|
||
Por favor espera mientras se cargan los metadatos de las tablas disponibles.
|
||
</p>
|
||
</UCard>
|
||
|
||
<!-- Table Content -->
|
||
<UCard v-else-if="selectedTable && currentTableStore" class="brand-card border border-transparent">
|
||
<template #header>
|
||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||
<h2 class="text-lg font-semibold brand-section-title">
|
||
Tabla: {{ selectedTable.name }}
|
||
</h2>
|
||
<div class="flex flex-wrap gap-2 text-xs text-[var(--brand-text-muted)]">
|
||
<span class="brand-pill inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs">
|
||
{{ currentTableStore.recordCount }} registros cargados
|
||
</span>
|
||
<span v-if="selectedTable.rowCount" class="brand-pill inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs">
|
||
{{ formatNumber(selectedTable.rowCount) }} total en BD
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Loading State for Table Data -->
|
||
<div v-if="currentTableStore.isLoading && !currentTableStore.hasData" class="flex items-center justify-center gap-3 py-10 text-[var(--brand-text-muted)]">
|
||
<span class="inline-flex h-8 w-8 animate-spin rounded-full border-2 border-[#c08040] border-t-transparent align-middle" aria-hidden="true" />
|
||
<span class="text-sm uppercase tracking-[0.3em]">Cargando datos...</span>
|
||
</div>
|
||
|
||
<!-- Error State -->
|
||
<div v-else-if="currentTableStore.hasError" class="py-10 text-center">
|
||
<div class="rounded-lg border border-red-500/40 bg-red-500/18 p-4 text-sm text-red-200 max-w-md mx-auto">
|
||
{{ currentTableStore.error }}
|
||
</div>
|
||
<UButton
|
||
class="mt-4"
|
||
:ui="{ base: 'bg-[#c08040] text-[#1b1209] border border-[#d99a56] hover:bg-[#d99a56] hover:border-[#f0c07c]' }"
|
||
@click="refreshTableData"
|
||
>
|
||
Reintentar
|
||
</UButton>
|
||
</div>
|
||
|
||
<!-- NuxtUI Table -->
|
||
<div v-else-if="currentTableStore.hasData" class="flex-1 divide-y divide-accented w-full">
|
||
<!-- Table Controls -->
|
||
<div class="flex items-center gap-2 px-4 py-3.5 overflow-x-auto">
|
||
<UInput
|
||
:model-value="globalFilter"
|
||
class="max-w-sm min-w-[12ch]"
|
||
placeholder="Buscar en todos los campos..."
|
||
icon="i-lucide-search"
|
||
@update:model-value="updateGlobalFilter"
|
||
/>
|
||
|
||
<UDropdownMenu
|
||
v-if="table?.tableApi"
|
||
:items="columnVisibilityItems"
|
||
:content="{ align: 'end' }"
|
||
>
|
||
<UButton
|
||
label="Columnas"
|
||
color="neutral"
|
||
variant="outline"
|
||
trailing-icon="i-lucide-chevron-down"
|
||
class="ml-auto"
|
||
aria-label="Selector de columnas visibles"
|
||
/>
|
||
</UDropdownMenu>
|
||
</div>
|
||
|
||
<!-- Table Component -->
|
||
<UTable
|
||
ref="table"
|
||
:data="currentTableStore.allRecords"
|
||
:columns="tableColumns"
|
||
:global-filter="globalFilter"
|
||
sticky
|
||
class="h-96"
|
||
/>
|
||
|
||
<!-- Table Footer with Meta Information -->
|
||
<div class="px-4 py-3.5 text-sm text-muted">
|
||
{{ filteredRowCount }} de {{ totalRowCount }} filas mostradas.
|
||
<span v-if="selectedTable.primaryKey" class="ml-2">
|
||
Clave primaria: <code class="text-xs bg-elevated px-1 py-0.5 rounded">{{ selectedTable.primaryKey }}</code>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Empty State -->
|
||
<div v-else class="py-10 text-center text-sm text-[var(--brand-text-muted)]">
|
||
<UIcon name="i-lucide-inbox" class="mx-auto mb-4 size-12 text-[var(--brand-text-muted)]" />
|
||
<h3 class="text-lg font-semibold text-[var(--brand-text)] mb-2">No hay datos disponibles</h3>
|
||
<p class="text-sm text-[var(--brand-text-muted)] mb-4">
|
||
Haz clic en "Actualizar datos" para cargar la información de esta tabla.
|
||
</p>
|
||
<UButton
|
||
:loading="currentTableStore.isLoading"
|
||
:ui="{ base: 'bg-[#c08040] text-[#1b1209] border border-[#d99a56] hover:bg-[#d99a56] hover:border-[#f0c07c]' }"
|
||
@click="refreshTableData"
|
||
>
|
||
<template #leading>
|
||
<UIcon name="i-lucide-refresh-cw" />
|
||
</template>
|
||
Cargar datos
|
||
</UButton>
|
||
</div>
|
||
</UCard>
|
||
|
||
<!-- No Table Selected -->
|
||
<UCard v-else class="brand-card border border-transparent">
|
||
<div class="py-10 text-center text-sm text-[var(--brand-text-muted)]">
|
||
Selecciona una tabla para explorar sus datos.
|
||
</div>
|
||
</UCard>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { h, resolveComponent } from 'vue'
|
||
import { upperFirst } from 'scule'
|
||
import type { TableColumn, DropdownMenuItem } from '@nuxt/ui'
|
||
import { useMetadataStore } from '~/stores/metadata'
|
||
import { useTableDataStore } from '~/stores/tableDataFactory'
|
||
|
||
definePageMeta({
|
||
layout: 'dashboard',
|
||
title: 'Explorador de datos'
|
||
})
|
||
|
||
const UButton = resolveComponent('UButton')
|
||
|
||
// State
|
||
const selectedTableName = ref<string>('')
|
||
const globalFilter = ref('')
|
||
const currentTableStore = ref<ReturnType<typeof useTableDataStore> | null>(null)
|
||
|
||
const table = useTemplateRef('table')
|
||
const metadataStore = useMetadataStore()
|
||
const { $getTableStore } = useNuxtApp()
|
||
|
||
// Computed properties
|
||
const selectedTable = computed(() => {
|
||
if (!selectedTableName.value || !metadataStore.hasMetadata) return null
|
||
|
||
const metadata = metadataStore.getTableMetadata(selectedTableName.value)
|
||
if (!metadata) return null
|
||
|
||
return {
|
||
name: metadata.table,
|
||
label: `${metadata.table} (${metadata.rowCount || 0} registros)`,
|
||
rowCount: metadata.rowCount || 0,
|
||
primaryKey: metadata.primaryKey,
|
||
columns: metadata.columns || [],
|
||
...metadata
|
||
}
|
||
})
|
||
|
||
const tableDropdownItems = computed((): DropdownMenuItem[] => {
|
||
if (metadataStore.loading && !metadataStore.hasMetadata) {
|
||
return [{
|
||
type: 'label',
|
||
label: 'Cargando...'
|
||
}]
|
||
}
|
||
|
||
if (!metadataStore.hasMetadata) {
|
||
return [{
|
||
type: 'label',
|
||
label: 'No hay tablas disponibles'
|
||
}]
|
||
}
|
||
|
||
return metadataStore.allTables.map(metadata => ({
|
||
label: `${metadata.table} (${metadata.rowCount || 0})`,
|
||
icon: 'i-lucide-table',
|
||
onSelect: () => selectTable(metadata.table)
|
||
}))
|
||
})
|
||
|
||
const tableColumns = computed((): TableColumn<Record<string, unknown>>[] => {
|
||
if (!currentTableStore.value?.hasData) return []
|
||
|
||
const firstRow = currentTableStore.value.allRecords[0]
|
||
if (!firstRow) return []
|
||
|
||
const columns = Object.keys(firstRow)
|
||
|
||
return columns.map(column => ({
|
||
accessorKey: column,
|
||
header: ({ column: tableColumn }) => {
|
||
const isSorted = tableColumn.getIsSorted()
|
||
|
||
return h(UButton, {
|
||
color: 'neutral',
|
||
variant: 'ghost',
|
||
label: upperFirst(column),
|
||
icon: isSorted ? (isSorted === 'asc' ? 'i-lucide-arrow-up-narrow-wide' : 'i-lucide-arrow-down-wide-narrow') : 'i-lucide-arrow-up-down',
|
||
class: '-mx-2.5',
|
||
onClick: () => tableColumn.toggleSorting(tableColumn.getIsSorted() === 'asc')
|
||
})
|
||
},
|
||
cell: ({ row }) => formatCellValue(row.getValue(column))
|
||
}))
|
||
})
|
||
|
||
const columnVisibilityItems = computed(() => {
|
||
if (!table.value?.tableApi) return []
|
||
|
||
return table.value.tableApi
|
||
.getAllColumns()
|
||
.filter(column => column.getCanHide())
|
||
.map(column => ({
|
||
label: upperFirst(column.id),
|
||
type: 'checkbox' as const,
|
||
checked: column.getIsVisible(),
|
||
onUpdateChecked(checked: boolean) {
|
||
table.value?.tableApi?.getColumn(column.id)?.toggleVisibility(!!checked)
|
||
},
|
||
onSelect(e?: Event) {
|
||
e?.preventDefault()
|
||
}
|
||
}))
|
||
})
|
||
|
||
const filteredRowCount = computed(() => {
|
||
return table.value?.tableApi?.getFilteredRowModel().rows.length || 0
|
||
})
|
||
|
||
const totalRowCount = computed(() => {
|
||
return currentTableStore.value?.recordCount || 0
|
||
})
|
||
|
||
// Methods
|
||
function selectTable(tableName: string) {
|
||
if (selectedTableName.value === tableName) return
|
||
|
||
selectedTableName.value = tableName
|
||
|
||
// Get the table store using the plugin
|
||
if (typeof $getTableStore === 'function') {
|
||
const store = $getTableStore(tableName)
|
||
if (store) {
|
||
currentTableStore.value = store
|
||
// Initialize the store (loads from cache or fetches)
|
||
store.initialize()
|
||
}
|
||
} else {
|
||
// Fallback: create store directly
|
||
currentTableStore.value = useTableDataStore(tableName)
|
||
currentTableStore.value.initialize()
|
||
}
|
||
}
|
||
|
||
async function refreshTableData() {
|
||
if (currentTableStore.value) {
|
||
await currentTableStore.value.refreshData()
|
||
}
|
||
}
|
||
|
||
function updateGlobalFilter(value: string) {
|
||
globalFilter.value = value
|
||
}
|
||
|
||
function formatNumber(value: number): string {
|
||
return new Intl.NumberFormat('es-ES').format(value)
|
||
}
|
||
|
||
function formatCellValue(value: unknown): string {
|
||
if (value === null || value === undefined) {
|
||
return '—'
|
||
}
|
||
|
||
if (value instanceof Date) {
|
||
return value.toISOString()
|
||
}
|
||
|
||
if (typeof value === 'object') {
|
||
try {
|
||
return JSON.stringify(value)
|
||
} catch {
|
||
return '[objeto]'
|
||
}
|
||
}
|
||
|
||
const stringValue = String(value)
|
||
|
||
// Truncate long strings
|
||
if (stringValue.length > 100) {
|
||
return stringValue.substring(0, 100) + '...'
|
||
}
|
||
|
||
return stringValue
|
||
}
|
||
|
||
// Lifecycle
|
||
onMounted(async () => {
|
||
await metadataStore.initialize()
|
||
|
||
// Auto-select first table if available
|
||
if (metadataStore.hasMetadata && !selectedTableName.value) {
|
||
const firstTable = metadataStore.allTables[0]
|
||
if (firstTable) {
|
||
selectTable(firstTable.table)
|
||
}
|
||
}
|
||
})
|
||
|
||
// Auto-select first table when metadata becomes available
|
||
watch(() => metadataStore.hasMetadata, (hasMetadata) => {
|
||
if (hasMetadata && !selectedTableName.value && metadataStore.allTables.length > 0) {
|
||
selectTable(metadataStore.allTables[0].table)
|
||
}
|
||
})
|
||
</script> |