Files
analiticaNucleo/nuxt4-app/app/components/MetadatosCard.vue
2025-10-01 05:04:00 -06:00

335 lines
11 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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: { padding: '0' }, header: { padding: 'px-3 py-2' }, footer: { padding: '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="2xs"
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="2xs"
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="2xs"
@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>