518 lines
17 KiB
TypeScript
518 lines
17 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { tableDataStorage } from '~/utils/storage'
|
|
|
|
export interface TableDataState<T = Record<string, unknown>> {
|
|
data: T[]
|
|
loading: boolean
|
|
error: string | null
|
|
lastUpdated: string | null
|
|
initialized: boolean
|
|
limit: number
|
|
}
|
|
|
|
export interface TableDataGetters<T = Record<string, unknown>> {
|
|
allRecords: (state: TableDataState<T>) => T[]
|
|
hasData: (state: TableDataState<T>) => boolean
|
|
isLoading: (state: TableDataState<T>) => boolean
|
|
hasError: (state: TableDataState<T>) => boolean
|
|
recordCount: (state: TableDataState<T>) => number
|
|
formattedLastUpdated: (state: TableDataState<T>) => string
|
|
isStale: (state: TableDataState<T>) => boolean
|
|
}
|
|
|
|
export interface TableDataActions<T = Record<string, unknown>> {
|
|
loadData(force?: boolean): Promise<void>
|
|
refreshData(): Promise<void>
|
|
fetchData(): Promise<void>
|
|
clearData(): Promise<void>
|
|
loadFromCache(): Promise<void>
|
|
extractErrorMessage(error: unknown): string
|
|
initialize(): Promise<void>
|
|
getRecord(id: string | number): T | undefined
|
|
filterRecords(predicate: (record: T) => boolean): T[]
|
|
loadAllDataInBatches(onProgress?: (progress: number) => void): Promise<void>
|
|
loadLatestDataInBatches(onProgress?: (progress: number) => void): Promise<void>
|
|
}
|
|
|
|
/**
|
|
* Factory function to create a Pinia store for table data
|
|
* @param tableName - The name of the table
|
|
* @param defaultLimit - Default limit for data fetching (default: 100)
|
|
*/
|
|
export function createTableDataStore<T = Record<string, unknown>>(
|
|
tableName: string,
|
|
defaultLimit: number = 100
|
|
) {
|
|
const storeId = `table-${tableName}`
|
|
const cacheKey = `table-data-${tableName}`
|
|
|
|
return defineStore(storeId, {
|
|
state: (): TableDataState<T> => ({
|
|
data: [],
|
|
loading: false,
|
|
error: null,
|
|
lastUpdated: null,
|
|
initialized: false,
|
|
limit: defaultLimit
|
|
}),
|
|
|
|
getters: {
|
|
/**
|
|
* Get all records
|
|
*/
|
|
allRecords(): T[] {
|
|
return this.data as T[]
|
|
},
|
|
|
|
/**
|
|
* Check if data is available
|
|
*/
|
|
hasData(): boolean {
|
|
return this.data.length > 0
|
|
},
|
|
|
|
/**
|
|
* Check if data is currently loading
|
|
*/
|
|
isLoading(): boolean {
|
|
return this.loading
|
|
},
|
|
|
|
/**
|
|
* Check if there's an error
|
|
*/
|
|
hasError(): boolean {
|
|
return !!this.error
|
|
},
|
|
|
|
/**
|
|
* Get total number of records
|
|
*/
|
|
recordCount(): number {
|
|
return this.data.length
|
|
},
|
|
|
|
/**
|
|
* Get formatted last updated time
|
|
*/
|
|
formattedLastUpdated(): string {
|
|
if (!this.lastUpdated) return 'Nunca'
|
|
|
|
try {
|
|
return new Date(this.lastUpdated).toLocaleString('es-ES', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
})
|
|
} catch {
|
|
return 'Fecha inválida'
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Check if data is stale (older than 5 minutes)
|
|
*/
|
|
isStale(): boolean {
|
|
if (!this.lastUpdated) return true
|
|
|
|
const lastUpdate = new Date(this.lastUpdated)
|
|
const now = new Date()
|
|
const fiveMinutes = 5 * 60 * 1000
|
|
|
|
return (now.getTime() - lastUpdate.getTime()) > fiveMinutes
|
|
}
|
|
},
|
|
|
|
actions: {
|
|
/**
|
|
* Load data lazily (only if not already loaded)
|
|
*/
|
|
async loadData(force = false): Promise<void> {
|
|
// Don't load if already loading
|
|
if (this.loading) return
|
|
|
|
// Don't load if already initialized and not forced
|
|
if (this.initialized && !force && !this.isStale) return
|
|
|
|
await this.fetchData()
|
|
},
|
|
|
|
/**
|
|
* Force refresh data
|
|
*/
|
|
async refreshData(): Promise<void> {
|
|
await this.fetchData()
|
|
},
|
|
|
|
/**
|
|
* Internal method to fetch data from API
|
|
*/
|
|
async fetchData(): Promise<void> {
|
|
this.loading = true
|
|
this.error = null
|
|
|
|
try {
|
|
const response = await $fetch(`/api/data/${tableName}`, {
|
|
query: { limit: String(this.limit) }
|
|
})
|
|
|
|
if (response && typeof response === 'object' && 'records' in response) {
|
|
const dataset = response as { records?: any[] }
|
|
this.data = Array.isArray(dataset.records) ? dataset.records : []
|
|
} else if (Array.isArray(response)) {
|
|
this.data = response
|
|
} else {
|
|
this.data = []
|
|
}
|
|
|
|
this.lastUpdated = new Date().toISOString()
|
|
this.initialized = true
|
|
|
|
// Persist to IndexedDB for offline access
|
|
if (process.client) {
|
|
try {
|
|
await tableDataStorage.setItem(cacheKey, {
|
|
data: this.data,
|
|
lastUpdated: this.lastUpdated,
|
|
limit: this.limit
|
|
})
|
|
} catch (error) {
|
|
console.warn(`Failed to persist ${tableName} data to IndexedDB:`, error)
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
this.error = this.extractErrorMessage(error)
|
|
console.error(`Error fetching ${tableName} data:`, error)
|
|
|
|
// Try to load from cache if available
|
|
if (process.client && !this.hasData) {
|
|
this.loadFromCache()
|
|
}
|
|
} finally {
|
|
this.loading = false
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load data from IndexedDB cache
|
|
*/
|
|
async loadFromCache(): Promise<void> {
|
|
if (!process.client) {
|
|
console.log(`[${tableName}] loadFromCache: Not on client, skipping`)
|
|
return
|
|
}
|
|
|
|
try {
|
|
console.log(`[${tableName}] loadFromCache: Attempting to load from key: ${cacheKey}`)
|
|
const parsedCache = await tableDataStorage.getItem(cacheKey)
|
|
if (parsedCache) {
|
|
console.log(`[${tableName}] loadFromCache: Found ${parsedCache.data?.length || 0} records in cache`)
|
|
this.data = parsedCache.data || []
|
|
this.lastUpdated = parsedCache.lastUpdated || null
|
|
this.limit = parsedCache.limit || defaultLimit
|
|
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) {
|
|
console.error(`[${tableName}] loadFromCache: Failed to load data from cache:`, error)
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Clear all data
|
|
*/
|
|
async clearData(): Promise<void> {
|
|
this.data = []
|
|
this.error = null
|
|
this.lastUpdated = null
|
|
this.initialized = false
|
|
|
|
if (process.client) {
|
|
try {
|
|
await tableDataStorage.removeItem(cacheKey)
|
|
} catch (error) {
|
|
console.warn(`Failed to clear ${tableName} data cache:`, error)
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Extract error message from various error types
|
|
*/
|
|
extractErrorMessage(error: unknown): string {
|
|
if (error && typeof error === 'object' && 'statusMessage' in error) {
|
|
return String((error as { statusMessage: string }).statusMessage)
|
|
}
|
|
|
|
if (error instanceof Error) {
|
|
return error.message
|
|
}
|
|
|
|
return `Error inesperado al cargar datos de ${tableName}`
|
|
},
|
|
|
|
/**
|
|
* Initialize store (called on app startup)
|
|
*/
|
|
async initialize(): Promise<void> {
|
|
// Load from cache first for immediate availability
|
|
await this.loadFromCache()
|
|
|
|
// Then try to fetch fresh data
|
|
await this.loadData()
|
|
},
|
|
|
|
/**
|
|
* Get a specific record by ID
|
|
*/
|
|
getRecord(id: string | number): T | undefined {
|
|
return this.data.find((record: any) => {
|
|
return record.id === id || String(record.id) === String(id)
|
|
})
|
|
},
|
|
|
|
/**
|
|
* Filter records by a predicate function
|
|
*/
|
|
filterRecords(predicate: (record: T) => boolean): T[] {
|
|
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)
|
|
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 IndexedDB
|
|
if (process.client) {
|
|
try {
|
|
console.log(`[${tableName}] loadAllDataInBatches: Persisting to IndexedDB with key: ${cacheKey}`)
|
|
const dataToSave = {
|
|
data: this.data,
|
|
lastUpdated: this.lastUpdated,
|
|
limit: this.limit
|
|
}
|
|
console.log(`[${tableName}] loadAllDataInBatches: Saving ${dataToSave.data.length} records`)
|
|
await tableDataStorage.setItem(cacheKey, dataToSave)
|
|
console.log(`[${tableName}] loadAllDataInBatches: Successfully saved to IndexedDB`)
|
|
|
|
// Verify it was saved
|
|
const saved = await tableDataStorage.getItem(cacheKey)
|
|
if (saved) {
|
|
console.log(`[${tableName}] loadAllDataInBatches: Verification - IndexedDB contains ${saved.data?.length || 0} records`)
|
|
}
|
|
} catch (error) {
|
|
console.error(`[${tableName}] loadAllDataInBatches: Failed to persist data to IndexedDB:`, error)
|
|
}
|
|
} else {
|
|
console.log(`[${tableName}] loadAllDataInBatches: Not on client, skipping IndexedDB`)
|
|
}
|
|
|
|
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)
|
|
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 IndexedDB
|
|
if (process.client) {
|
|
try {
|
|
console.log(`[${tableName}] loadLatestDataInBatches: Persisting to IndexedDB with key: ${cacheKey}`)
|
|
const dataToSave = {
|
|
data: this.data,
|
|
lastUpdated: this.lastUpdated,
|
|
limit: this.limit
|
|
}
|
|
console.log(`[${tableName}] loadLatestDataInBatches: Saving ${dataToSave.data.length} records`)
|
|
await tableDataStorage.setItem(cacheKey, dataToSave)
|
|
console.log(`[${tableName}] loadLatestDataInBatches: Successfully saved to IndexedDB`)
|
|
|
|
// Verify it was saved
|
|
const saved = await tableDataStorage.getItem(cacheKey)
|
|
if (saved) {
|
|
console.log(`[${tableName}] loadLatestDataInBatches: Verification - IndexedDB contains ${saved.data?.length || 0} records`)
|
|
}
|
|
} catch (error) {
|
|
console.error(`[${tableName}] loadLatestDataInBatches: Failed to persist data to IndexedDB:`, error)
|
|
}
|
|
} else {
|
|
console.log(`[${tableName}] loadLatestDataInBatches: Not on client, skipping IndexedDB`)
|
|
}
|
|
|
|
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`)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Helper to get or create a table data store
|
|
*/
|
|
export function useTableDataStore<T = Record<string, unknown>>(
|
|
tableName: string,
|
|
limit?: number
|
|
) {
|
|
const store = createTableDataStore<T>(tableName, limit)
|
|
return store()
|
|
} |