335 lines
11 KiB
Vue
335 lines
11 KiB
Vue
<template>
|
||
<UCard
|
||
class="brand-card cursor-pointer transition-all duration-300"
|
||
:class="[
|
||
compact ? 'border-0 !p-0' : 'border border-transparent',
|
||
compact && tableStore?.isStale ? 'bg-yellow-500/10 border-l-4 !border-l-yellow-500' : ''
|
||
]"
|
||
@click="toggleCompact"
|
||
:ui="compact ? { body: 'p-0', header: 'px-3 py-2', footer: 'p-0' } : {}"
|
||
>
|
||
<template v-if="compact" #header>
|
||
<div class="flex items-center justify-between gap-3">
|
||
<!-- Stale Data Warning (when applicable) -->
|
||
<div v-if="tableStore?.isStale" class="flex items-center gap-2 min-w-0 flex-1">
|
||
<div class="flex items-center gap-2 bg-yellow-500/20 border border-yellow-500/50 rounded px-2 py-0.5 animate-pulse">
|
||
<UIcon name="i-lucide-alert-triangle" class="text-yellow-400 text-sm flex-shrink-0" />
|
||
<span class="text-xs font-bold text-yellow-300 whitespace-nowrap">
|
||
DATOS DESACTUALIZADOS
|
||
</span>
|
||
</div>
|
||
<span class="text-[10px] text-[var(--brand-text-muted)] font-medium truncate">
|
||
{{ metadata.table }}
|
||
</span>
|
||
</div>
|
||
|
||
<!-- Normal State -->
|
||
<div v-else class="flex items-center gap-2 min-w-0 flex-1">
|
||
<h2 class="text-xs font-medium brand-section-title truncate">
|
||
{{ metadata.table }}
|
||
</h2>
|
||
<span class="text-[10px] text-[var(--brand-text-muted)] font-medium whitespace-nowrap">
|
||
{{ formatNumber(recordCount) }} reg
|
||
</span>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-1.5 flex-shrink-0">
|
||
<UButton
|
||
:loading="isLoadingLatest"
|
||
:disabled="isLoadingAll"
|
||
:ui="{ base: tableStore?.isStale ? 'bg-yellow-500 text-black border-0 hover:bg-yellow-400 font-bold' : 'bg-[#c08040] text-[#1b1209] border-0 hover:bg-[#d99a56]' }"
|
||
size="xs"
|
||
icon="i-lucide-clock"
|
||
@click.stop="loadLatestData"
|
||
:class="{ 'animate-spin': isLoadingLatest, 'animate-bounce': tableStore?.isStale && !isLoadingLatest }"
|
||
:title="tableStore?.isStale ? '¡Actualizar ahora!' : 'Últimos'"
|
||
/>
|
||
<UButton
|
||
:loading="isLoadingAll"
|
||
:disabled="isLoadingLatest"
|
||
:ui="{ base: tableStore?.isStale ? 'bg-yellow-500 text-black border-0 hover:bg-yellow-400 font-bold' : 'bg-[#c08040] text-[#1b1209] border-0 hover:bg-[#d99a56]' }"
|
||
size="xs"
|
||
icon="i-lucide-database"
|
||
@click.stop="loadAllData"
|
||
:class="{ 'animate-spin': isLoadingAll }"
|
||
:title="tableStore?.isStale ? '¡Actualizar ahora!' : 'Todos'"
|
||
/>
|
||
<UButton
|
||
icon="i-lucide-chevron-down"
|
||
color="neutral"
|
||
variant="ghost"
|
||
size="xs"
|
||
@click.stop="toggleCompact"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else #header>
|
||
<div class="flex flex-col gap-2">
|
||
<div class="flex items-center justify-between">
|
||
<h2 class="text-lg font-semibold brand-section-title">
|
||
Tabla {{ metadata.table }}
|
||
</h2>
|
||
<div class="flex items-center gap-2">
|
||
<span class="brand-badge inline-flex items-center gap-1 rounded-full px-3 py-1 text-xs font-semibold tracking-wide">
|
||
{{ formatNumber(recordCount) }} registros
|
||
</span>
|
||
<UButton
|
||
icon="i-lucide-chevron-up"
|
||
color="neutral"
|
||
variant="ghost"
|
||
size="xs"
|
||
@click.stop="toggleCompact"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<Transition
|
||
enter-active-class="transition-all duration-300 ease-out"
|
||
enter-from-class="opacity-0 max-h-0"
|
||
enter-to-class="opacity-100 max-h-20"
|
||
leave-active-class="transition-all duration-200 ease-in"
|
||
leave-from-class="opacity-100 max-h-20"
|
||
leave-to-class="opacity-0 max-h-0"
|
||
>
|
||
<p v-if="metadata.description" class="text-sm text-[var(--brand-text-muted)] overflow-hidden">
|
||
{{ metadata.description }}
|
||
</p>
|
||
</Transition>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Normal Mode Body -->
|
||
<dl v-if="!compact" class="grid grid-cols-2 gap-3 text-sm text-[var(--brand-text-muted)]">
|
||
<div>
|
||
<dt class="uppercase tracking-wide text-xs">Clave primaria</dt>
|
||
<dd class="font-medium text-[var(--brand-text)]">{{ metadata.primaryKey || '—' }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt class="uppercase tracking-wide text-xs">Tamaño aprox.</dt>
|
||
<dd class="font-medium text-[var(--brand-text)]">{{ formatSize(metadata.approxSizeBytes) }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt class="uppercase tracking-wide text-xs">Creación desde</dt>
|
||
<dd class="font-medium text-[var(--brand-text)]">{{ formatDate(metadata.createdAtRange?.from) }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt class="uppercase tracking-wide text-xs">Creación hasta</dt>
|
||
<dd class="font-medium text-[var(--brand-text)]">{{ formatDate(metadata.createdAtRange?.to) }}</dd>
|
||
</div>
|
||
</dl>
|
||
|
||
<!-- Compact Mode: Progress bar only -->
|
||
<div v-if="compact && (isLoadingLatest || isLoadingAll)" class="px-3 pb-2">
|
||
<UProgress
|
||
:model-value="loadingProgress"
|
||
:max="100"
|
||
status
|
||
size="xs"
|
||
/>
|
||
</div>
|
||
|
||
<template v-if="!compact" #footer>
|
||
<div class="flex flex-col gap-3">
|
||
<div class="brand-divider pt-3 text-xs text-[var(--brand-text-muted)]">
|
||
Columnas detectadas ({{ metadata.columns?.length || 0 }}): {{ (metadata.columns || []).join(', ') || 'Ninguna' }}
|
||
</div>
|
||
|
||
<div class="flex flex-col gap-3">
|
||
<div class="flex items-center justify-between gap-3">
|
||
<div class="flex flex-col gap-1">
|
||
<span class="text-xs text-[var(--brand-text-muted)]">
|
||
{{ tableStore ? 'Última carga: ' + tableStore.formattedLastUpdated : 'No hay datos cargados' }}
|
||
</span>
|
||
<span v-if="tableStore?.isStale" class="text-xs text-yellow-400">
|
||
⚠️ Los datos pueden estar desactualizados
|
||
</span>
|
||
</div>
|
||
|
||
<div class="flex gap-2">
|
||
<UButton
|
||
:loading="isLoadingLatest"
|
||
:disabled="isLoadingAll"
|
||
:ui="{ base: 'bg-[#c08040] text-[#1b1209] border border-[#d99a56] hover:bg-[#d99a56] hover:border-[#f0c07c]' }"
|
||
size="sm"
|
||
@click.stop="loadLatestData"
|
||
>
|
||
<template #leading>
|
||
<UIcon name="i-lucide-clock" :class="{ 'animate-spin': isLoadingLatest }" />
|
||
</template>
|
||
Últimos datos
|
||
</UButton>
|
||
|
||
<UButton
|
||
:loading="isLoadingAll"
|
||
:disabled="isLoadingLatest"
|
||
:ui="{ base: 'bg-[#c08040] text-[#1b1209] border border-[#d99a56] hover:bg-[#d99a56] hover:border-[#f0c07c]' }"
|
||
size="sm"
|
||
@click.stop="loadAllData"
|
||
>
|
||
<template #leading>
|
||
<UIcon name="i-lucide-database" :class="{ 'animate-spin': isLoadingAll }" />
|
||
</template>
|
||
Obtener todos
|
||
</UButton>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Progress Bar -->
|
||
<UProgress
|
||
v-if="isLoadingLatest || isLoadingAll"
|
||
:model-value="loadingProgress"
|
||
:max="100"
|
||
status
|
||
size="sm"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</UCard>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { useTableDataStore } from '~/stores/tableDataFactory'
|
||
|
||
interface MetadataProps {
|
||
metadata: {
|
||
name: string
|
||
table: string
|
||
primaryKey?: string
|
||
rowCount?: number
|
||
approxSizeBytes?: number | null
|
||
columns?: string[]
|
||
createdAtRange?: {
|
||
from: string | null
|
||
to: string | null
|
||
}
|
||
sampleRow?: any
|
||
lastRefreshed?: string
|
||
description?: string
|
||
}
|
||
compact?: boolean
|
||
}
|
||
|
||
const props = withDefaults(defineProps<MetadataProps>(), {
|
||
compact: true
|
||
})
|
||
|
||
const { $getTableStore } = useNuxtApp()
|
||
|
||
// Compact mode state (persistent per table)
|
||
const compact = useState(`metadata-compact-${props.metadata.name}`, () => props.compact)
|
||
|
||
// Loading states
|
||
const isLoadingLatest = ref(false)
|
||
const isLoadingAll = ref(false)
|
||
const loadingProgress = ref(0)
|
||
|
||
// Toggle compact mode
|
||
function toggleCompact() {
|
||
compact.value = !compact.value
|
||
}
|
||
|
||
// Get the table store for this specific datasource (using name, not table)
|
||
// The plugin has already loaded all caches, so this just retrieves the existing instance
|
||
const tableStore = computed(() => {
|
||
if (typeof $getTableStore === 'function') {
|
||
return $getTableStore(props.metadata.name)
|
||
}
|
||
return useTableDataStore(props.metadata.name)
|
||
})
|
||
|
||
// Calculate record count from in-memory data
|
||
const recordCount = computed(() => {
|
||
return tableStore.value?.recordCount || 0
|
||
})
|
||
|
||
async function loadLatestData() {
|
||
isLoadingLatest.value = true
|
||
loadingProgress.value = 0
|
||
|
||
try {
|
||
const store = tableStore.value
|
||
if (!store) return
|
||
|
||
await store.loadLatestDataInBatches((progress) => {
|
||
loadingProgress.value = progress
|
||
})
|
||
|
||
loadingProgress.value = 100
|
||
} catch (error) {
|
||
console.error('Error loading latest data:', error)
|
||
} finally {
|
||
setTimeout(() => {
|
||
isLoadingLatest.value = false
|
||
loadingProgress.value = 0
|
||
}, 500)
|
||
}
|
||
}
|
||
|
||
async function loadAllData() {
|
||
isLoadingAll.value = true
|
||
loadingProgress.value = 0
|
||
|
||
try {
|
||
const store = tableStore.value
|
||
if (!store) return
|
||
|
||
await store.loadAllDataInBatches((progress) => {
|
||
loadingProgress.value = progress
|
||
})
|
||
|
||
loadingProgress.value = 100
|
||
} catch (error) {
|
||
console.error('Error loading all data:', error)
|
||
} finally {
|
||
setTimeout(() => {
|
||
isLoadingAll.value = false
|
||
loadingProgress.value = 0
|
||
}, 500)
|
||
}
|
||
}
|
||
|
||
function formatSize(bytes: number | null | undefined): string {
|
||
if (!bytes) {
|
||
return 'No disponible'
|
||
}
|
||
|
||
if (bytes < 1024) {
|
||
return `${bytes} B`
|
||
}
|
||
|
||
const units = ['KB', 'MB', 'GB', 'TB']
|
||
let size = bytes / 1024
|
||
let unitIndex = 0
|
||
|
||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||
size /= 1024
|
||
unitIndex += 1
|
||
}
|
||
|
||
return `${size.toFixed(1)} ${units[unitIndex]}`
|
||
}
|
||
|
||
function formatDate(value: string | null | undefined): string {
|
||
if (!value) {
|
||
return '—'
|
||
}
|
||
|
||
const date = new Date(value)
|
||
|
||
if (Number.isNaN(date.getTime())) {
|
||
return value
|
||
}
|
||
|
||
return date.toLocaleString('es-ES', {
|
||
year: 'numeric',
|
||
month: 'short',
|
||
day: 'numeric'
|
||
})
|
||
}
|
||
|
||
function formatNumber(value: number): string {
|
||
return new Intl.NumberFormat('es-ES').format(value)
|
||
}
|
||
</script> |