listo carga de datos en localstorage

This commit is contained in:
2025-09-30 01:40:21 -06:00
parent ab920c355a
commit a346e30777
7 changed files with 371 additions and 74 deletions

View File

@@ -39,27 +39,54 @@
Columnas detectadas ({{ metadata.columns?.length || 0 }}): {{ (metadata.columns || []).join(', ') || 'Ninguna' }} Columnas detectadas ({{ metadata.columns?.length || 0 }}): {{ (metadata.columns || []).join(', ') || 'Ninguna' }}
</div> </div>
<div class="flex items-center justify-between gap-3"> <div class="flex flex-col gap-3">
<div class="flex flex-col gap-1"> <div class="flex items-center justify-between gap-3">
<span class="text-xs text-[var(--brand-text-muted)]"> <div class="flex flex-col gap-1">
{{ tableStore ? 'Última carga: ' + tableStore.formattedLastUpdated : 'No hay datos cargados' }} <span class="text-xs text-[var(--brand-text-muted)]">
</span> {{ tableStore ? 'Última carga: ' + tableStore.formattedLastUpdated : 'No hay datos cargados' }}
<span v-if="tableStore?.isStale" class="text-xs text-yellow-400"> </span>
Los datos pueden estar desactualizados <span v-if="tableStore?.isStale" class="text-xs text-yellow-400">
</span> 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="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="loadAllData"
>
<template #leading>
<UIcon name="i-lucide-database" :class="{ 'animate-spin': isLoadingAll }" />
</template>
Obtener todos
</UButton>
</div>
</div> </div>
<UButton <!-- Progress Bar -->
:loading="tableStore?.isLoading" <UProgress
:ui="{ base: 'bg-[#c08040] text-[#1b1209] border border-[#d99a56] hover:bg-[#d99a56] hover:border-[#f0c07c]' }" v-if="isLoadingLatest || isLoadingAll"
:model-value="loadingProgress"
:max="100"
status
size="sm" size="sm"
@click="loadData" />
>
<template #leading>
<UIcon name="i-lucide-refresh-cw" :class="{ 'animate-spin': tableStore?.isLoading }" />
</template>
Cargar datos
</UButton>
</div> </div>
</div> </div>
</template> </template>
@@ -91,6 +118,11 @@ const props = defineProps<MetadataProps>()
const { $getTableStore } = useNuxtApp() const { $getTableStore } = useNuxtApp()
// Loading states
const isLoadingLatest = ref(false)
const isLoadingAll = ref(false)
const loadingProgress = ref(0)
// Get the table store for this specific datasource (using name, not table) // Get the table store for this specific datasource (using name, not table)
const tableStore = computed(() => { const tableStore = computed(() => {
if (typeof $getTableStore === 'function') { if (typeof $getTableStore === 'function') {
@@ -104,16 +136,49 @@ const recordCount = computed(() => {
return tableStore.value?.recordCount || 0 return tableStore.value?.recordCount || 0
}) })
async function loadData() { async function loadLatestData() {
const store = tableStore.value isLoadingLatest.value = true
if (store) { loadingProgress.value = 0
if (!store.hasData) {
// If no data, initialize (which loads from cache or fetches) try {
await store.initialize() const store = tableStore.value
} else { if (!store) return
// If already has data, refresh it
await store.refreshData() 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)
} }
} }

View File

@@ -32,29 +32,11 @@
</UDropdownMenu> </UDropdownMenu>
</UFieldGroup> </UFieldGroup>
<!-- Refresh Controls (only shown when table is selected) --> <!-- Metadata Card for selected table -->
<div v-if="selectedTable && currentTableStore" class="flex items-center justify-between p-3 rounded-lg bg-[#1c140c] border border-[#3a2a16]"> <MetadatosCard
<div class="flex flex-col gap-1"> v-if="selectedTable"
<span class="text-xs font-medium text-[var(--brand-text-muted)]"> :metadata="selectedTable"
Ú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> </div>
</UCard> </UCard>
@@ -214,12 +196,8 @@ const selectedTable = computed(() => {
if (!metadata) return null if (!metadata) return null
return { return {
name: metadata.table, ...metadata,
label: `${metadata.table} (${metadata.rowCount || 0} registros)`, label: `${metadata.table} (${metadata.rowCount || 0} registros)`
rowCount: metadata.rowCount || 0,
primaryKey: metadata.primaryKey,
columns: metadata.columns || [],
...metadata
} }
}) })
@@ -241,7 +219,7 @@ const tableDropdownItems = computed((): DropdownMenuItem[] => {
return metadataStore.allTables.map(metadata => ({ return metadataStore.allTables.map(metadata => ({
label: `${metadata.table} (${metadata.rowCount || 0})`, label: `${metadata.table} (${metadata.rowCount || 0})`,
icon: 'i-lucide-table', icon: 'i-lucide-table',
onSelect: () => selectTable(metadata.table) onSelect: () => selectTable(metadata.name)
})) }))
}) })

View File

@@ -29,6 +29,8 @@ export interface TableDataActions<T = Record<string, unknown>> {
initialize(): Promise<void> initialize(): Promise<void>
getRecord(id: string | number): T | undefined getRecord(id: string | number): T | undefined
filterRecords(predicate: (record: T) => boolean): T[] filterRecords(predicate: (record: T) => boolean): T[]
loadAllDataInBatches(onProgress?: (progress: number) => void): Promise<void>
loadLatestDataInBatches(onProgress?: (progress: number) => void): Promise<void>
} }
/** /**
@@ -57,36 +59,46 @@ export function createTableDataStore<T = Record<string, unknown>>(
/** /**
* Get all records * Get all records
*/ */
allRecords: (state): T[] => state.data, allRecords(): T[] {
return this.data as T[]
},
/** /**
* Check if data is available * Check if data is available
*/ */
hasData: (state): boolean => state.data.length > 0, hasData(): boolean {
return this.data.length > 0
},
/** /**
* Check if data is currently loading * Check if data is currently loading
*/ */
isLoading: (state): boolean => state.loading, isLoading(): boolean {
return this.loading
},
/** /**
* Check if there's an error * Check if there's an error
*/ */
hasError: (state): boolean => !!state.error, hasError(): boolean {
return !!this.error
},
/** /**
* Get total number of records * Get total number of records
*/ */
recordCount: (state): number => state.data.length, recordCount(): number {
return this.data.length
},
/** /**
* Get formatted last updated time * Get formatted last updated time
*/ */
formattedLastUpdated: (state): string => { formattedLastUpdated(): string {
if (!state.lastUpdated) return 'Nunca' if (!this.lastUpdated) return 'Nunca'
try { try {
return new Date(state.lastUpdated).toLocaleString('es-ES', { return new Date(this.lastUpdated).toLocaleString('es-ES', {
year: 'numeric', year: 'numeric',
month: 'long', month: 'long',
day: 'numeric', day: 'numeric',
@@ -101,10 +113,10 @@ export function createTableDataStore<T = Record<string, unknown>>(
/** /**
* Check if data is stale (older than 5 minutes) * Check if data is stale (older than 5 minutes)
*/ */
isStale: (state): boolean => { isStale(): boolean {
if (!state.lastUpdated) return true if (!this.lastUpdated) return true
const lastUpdate = new Date(state.lastUpdated) const lastUpdate = new Date(this.lastUpdated)
const now = new Date() const now = new Date()
const fiveMinutes = 5 * 60 * 1000 const fiveMinutes = 5 * 60 * 1000
@@ -186,19 +198,27 @@ export function createTableDataStore<T = Record<string, unknown>>(
* Load data from localStorage cache * Load data from localStorage cache
*/ */
loadFromCache(): void { loadFromCache(): void {
if (!process.client) return if (!process.client) {
console.log(`[${tableName}] loadFromCache: Not on client, skipping`)
return
}
try { try {
console.log(`[${tableName}] loadFromCache: Attempting to load from key: ${cacheKey}`)
const cached = localStorage.getItem(cacheKey) const cached = localStorage.getItem(cacheKey)
if (cached) { if (cached) {
const parsedCache = JSON.parse(cached) const parsedCache = JSON.parse(cached)
console.log(`[${tableName}] loadFromCache: Found ${parsedCache.data?.length || 0} records in cache`)
this.data = parsedCache.data || [] this.data = parsedCache.data || []
this.lastUpdated = parsedCache.lastUpdated || null this.lastUpdated = parsedCache.lastUpdated || null
this.limit = parsedCache.limit || defaultLimit this.limit = parsedCache.limit || defaultLimit
this.initialized = true this.initialized = true
console.log(`[${tableName}] loadFromCache: Successfully loaded, data.length: ${this.data.length}`)
} else {
console.log(`[${tableName}] loadFromCache: No cached data found`)
} }
} catch (error) { } catch (error) {
console.warn(`Failed to load ${tableName} data from cache:`, error) console.error(`[${tableName}] loadFromCache: Failed to load data from cache:`, error)
} }
}, },
@@ -259,7 +279,230 @@ export function createTableDataStore<T = Record<string, unknown>>(
* Filter records by a predicate function * Filter records by a predicate function
*/ */
filterRecords(predicate: (record: T) => boolean): T[] { filterRecords(predicate: (record: T) => boolean): T[] {
return this.data.filter(predicate) return (this.data as T[]).filter(predicate)
},
/**
* Load all data in batches with progress callback
*/
async loadAllDataInBatches(onProgress?: (progress: number) => void): Promise<void> {
console.log(`[${tableName}] loadAllDataInBatches: Starting...`)
this.loading = true
this.error = null
try {
// Clear existing data
console.log(`[${tableName}] loadAllDataInBatches: Clearing ${this.data.length} existing records`)
this.data.splice(0, this.data.length)
console.log(`[${tableName}] loadAllDataInBatches: Data cleared, length now: ${this.data.length}`)
let offset = 0
const limit = 500
let hasMore = true
let totalFetched = 0
let estimatedTotal = 0
while (hasMore) {
console.log(`[${tableName}] loadAllDataInBatches: Fetching batch at offset ${offset}`)
const response = await $fetch(`/api/data/${tableName}`, {
params: { limit, offset }
})
if (!response) {
console.log(`[${tableName}] loadAllDataInBatches: No response, stopping`)
hasMore = false
break
}
// Get total count from first response
if (offset === 0 && response.count) {
estimatedTotal = response.count
console.log(`[${tableName}] loadAllDataInBatches: Estimated total: ${estimatedTotal}`)
}
if (response.records && response.records.length > 0) {
console.log(`[${tableName}] loadAllDataInBatches: Got ${response.records.length} records`)
this.data.push(...(response.records as T[]))
totalFetched += response.records.length
console.log(`[${tableName}] loadAllDataInBatches: Total fetched so far: ${totalFetched}, data.length: ${this.data.length}`)
// Update progress
if (onProgress) {
if (estimatedTotal > 0) {
onProgress(Math.min(95, (totalFetched / estimatedTotal) * 100))
} else {
onProgress(Math.min(95, (totalFetched / 10000) * 100))
}
}
// Check if we got fewer records than requested (means we're at the end)
hasMore = response.records.length === limit
offset += limit
} else {
console.log(`[${tableName}] loadAllDataInBatches: No records in response, stopping`)
hasMore = false
}
}
this.lastUpdated = new Date().toISOString()
this.initialized = true
console.log(`[${tableName}] loadAllDataInBatches: Finished loading ${this.data.length} records`)
// Persist to localStorage
if (process.client) {
try {
console.log(`[${tableName}] loadAllDataInBatches: Persisting to localStorage with key: ${cacheKey}`)
const dataToSave = {
data: this.data,
lastUpdated: this.lastUpdated,
limit: this.limit
}
console.log(`[${tableName}] loadAllDataInBatches: Saving ${dataToSave.data.length} records`)
localStorage.setItem(cacheKey, JSON.stringify(dataToSave))
console.log(`[${tableName}] loadAllDataInBatches: Successfully saved to localStorage`)
// Verify it was saved
const saved = localStorage.getItem(cacheKey)
if (saved) {
const parsed = JSON.parse(saved)
console.log(`[${tableName}] loadAllDataInBatches: Verification - localStorage contains ${parsed.data?.length || 0} records`)
}
} catch (error) {
console.error(`[${tableName}] loadAllDataInBatches: Failed to persist data to localStorage:`, error)
}
} else {
console.log(`[${tableName}] loadAllDataInBatches: Not on client, skipping localStorage`)
}
if (onProgress) {
onProgress(100)
}
} catch (error: any) {
this.error = this.extractErrorMessage(error)
console.error(`[${tableName}] loadAllDataInBatches: Error:`, error)
throw error
} finally {
this.loading = false
console.log(`[${tableName}] loadAllDataInBatches: Completed`)
}
},
/**
* Load only latest data (incremental) in batches with progress callback
*/
async loadLatestDataInBatches(onProgress?: (progress: number) => void): Promise<void> {
console.log(`[${tableName}] loadLatestDataInBatches: Starting...`)
this.loading = true
this.error = null
try {
// Find the most recent created_at in memory
let lastCreatedAt: string | null = null
if (this.data.length > 0) {
const sortedRecords = [...(this.data as any[])].sort((a, b) => {
const dateA = new Date(a.created_at || 0).getTime()
const dateB = new Date(b.created_at || 0).getTime()
return dateB - dateA
})
lastCreatedAt = sortedRecords[0]?.created_at || null
console.log(`[${tableName}] loadLatestDataInBatches: Most recent created_at in memory: ${lastCreatedAt}`)
} else {
console.log(`[${tableName}] loadLatestDataInBatches: No data in memory, will fetch all`)
}
let offset = 0
const limit = 500
let hasMore = true
let newRecordsCount = 0
while (hasMore) {
console.log(`[${tableName}] loadLatestDataInBatches: Fetching batch at offset ${offset}`)
const response = await $fetch(`/api/data/${tableName}`, {
params: {
limit,
offset,
orderBy: 'created_at',
orderDirection: 'desc'
}
})
if (!response || !response.records || response.records.length === 0) {
console.log(`[${tableName}] loadLatestDataInBatches: No response or records, stopping`)
hasMore = false
break
}
console.log(`[${tableName}] loadLatestDataInBatches: Got ${response.records.length} records`)
// Filter only records newer than what we have
const newRecords = lastCreatedAt
? response.records.filter((r: any) => r.created_at > lastCreatedAt)
: response.records
console.log(`[${tableName}] loadLatestDataInBatches: ${newRecords.length} new records after filtering`)
if (newRecords.length > 0) {
this.data.unshift(...(newRecords as T[]))
newRecordsCount += newRecords.length
console.log(`[${tableName}] loadLatestDataInBatches: Total new records: ${newRecordsCount}, data.length: ${this.data.length}`)
}
// Update progress
if (onProgress) {
onProgress(Math.min(95, (newRecordsCount / 500) * 100))
}
// Stop if we found records older than our last one
if (lastCreatedAt && response.records.some((r: any) => r.created_at <= lastCreatedAt)) {
console.log(`[${tableName}] loadLatestDataInBatches: Found older records, stopping`)
hasMore = false
} else {
hasMore = response.records.length === limit
offset += limit
}
}
this.lastUpdated = new Date().toISOString()
console.log(`[${tableName}] loadLatestDataInBatches: Added ${newRecordsCount} new records, total: ${this.data.length}`)
// Persist to localStorage
if (process.client) {
try {
console.log(`[${tableName}] loadLatestDataInBatches: Persisting to localStorage with key: ${cacheKey}`)
const dataToSave = {
data: this.data,
lastUpdated: this.lastUpdated,
limit: this.limit
}
console.log(`[${tableName}] loadLatestDataInBatches: Saving ${dataToSave.data.length} records`)
localStorage.setItem(cacheKey, JSON.stringify(dataToSave))
console.log(`[${tableName}] loadLatestDataInBatches: Successfully saved to localStorage`)
// Verify it was saved
const saved = localStorage.getItem(cacheKey)
if (saved) {
const parsed = JSON.parse(saved)
console.log(`[${tableName}] loadLatestDataInBatches: Verification - localStorage contains ${parsed.data?.length || 0} records`)
}
} catch (error) {
console.error(`[${tableName}] loadLatestDataInBatches: Failed to persist data to localStorage:`, error)
}
} else {
console.log(`[${tableName}] loadLatestDataInBatches: Not on client, skipping localStorage`)
}
if (onProgress) {
onProgress(100)
}
} catch (error: any) {
this.error = this.extractErrorMessage(error)
console.error(`[${tableName}] loadLatestDataInBatches: Error:`, error)
throw error
} finally {
this.loading = false
console.log(`[${tableName}] loadLatestDataInBatches: Completed`)
}
} }
} }
}) })

View File

@@ -12,11 +12,15 @@ export default defineEventHandler(async (event) => {
const limitValue = Number.parseInt((query.limit as string) ?? '', 10) const limitValue = Number.parseInt((query.limit as string) ?? '', 10)
const limit = Number.isFinite(limitValue) ? Math.min(Math.max(limitValue, 1), 500) : undefined const limit = Number.isFinite(limitValue) ? Math.min(Math.max(limitValue, 1), 500) : undefined
const offsetValue = Number.parseInt((query.offset as string) ?? '', 10)
const offset = Number.isFinite(offsetValue) ? Math.max(offsetValue, 0) : undefined
const parsedQuery = parseQuerySegment(query.query as string | undefined) const parsedQuery = parseQuerySegment(query.query as string | undefined)
return await fetchTableData(table, { return await fetchTableData(table, {
parsedQuery, parsedQuery,
limit, limit,
offset,
filters: { filters: {
id: (query.id as string) || undefined, id: (query.id as string) || undefined,
createdFrom: (query.created_from as string) || undefined, createdFrom: (query.created_from as string) || undefined,

View File

@@ -3,5 +3,6 @@ import type { TableConfig } from '../types'
export const vistaResumenIngresosConfig: TableConfig = { export const vistaResumenIngresosConfig: TableConfig = {
name: 'vista_resumen_ingresos', name: 'vista_resumen_ingresos',
table: 'vista_resumen_ingresos', table: 'vista_resumen_ingresos',
primaryKey: 'id' primaryKey: 'id',
defaultSelect: '*, fecha as created_at'
} }

View File

@@ -3,5 +3,6 @@ import type { TableConfig } from '../types'
export const vistaResumenIngresosPorComercioConfig: TableConfig = { export const vistaResumenIngresosPorComercioConfig: TableConfig = {
name: 'vista_resumen_ingresos_por_comercio', name: 'vista_resumen_ingresos_por_comercio',
table: 'vista_resumen_ingresos_por_comercio', table: 'vista_resumen_ingresos_por_comercio',
primaryKey: 'id' primaryKey: 'id',
defaultSelect: '*, fecha as created_at'
} }

View File

@@ -23,6 +23,7 @@ interface DataOptions {
parsedQuery?: ParsedQuery | null parsedQuery?: ParsedQuery | null
filters?: DataFilters filters?: DataFilters
limit?: number limit?: number
offset?: number
} }
function serializeRow(row: unknown, config: TableConfig): GenericObject | null { function serializeRow(row: unknown, config: TableConfig): GenericObject | null {
@@ -261,6 +262,10 @@ export async function fetchTableData(tableName: string, options?: DataOptions) {
query = query.limit(limit) query = query.limit(limit)
} }
if (options?.offset !== undefined && options.offset > 0) {
query = query.range(options.offset, options.offset + limit - 1)
}
const { data, error, count } = await query const { data, error, count } = await query
if (error) { if (error) {