import { defineStore } from 'pinia' import { tableDataStorage } from '~/utils/storage' export interface TableDataState> { data: T[] loading: boolean error: string | null lastUpdated: string | null initialized: boolean limit: number } export interface TableDataGetters> { allRecords: (state: TableDataState) => T[] hasData: (state: TableDataState) => boolean isLoading: (state: TableDataState) => boolean hasError: (state: TableDataState) => boolean recordCount: (state: TableDataState) => number formattedLastUpdated: (state: TableDataState) => string isStale: (state: TableDataState) => boolean } export interface TableDataActions> { loadData(force?: boolean): Promise refreshData(): Promise fetchData(): Promise clearData(): Promise loadFromCache(): Promise extractErrorMessage(error: unknown): string initialize(): Promise getRecord(id: string | number): T | undefined filterRecords(predicate: (record: T) => boolean): T[] loadAllDataInBatches(onProgress?: (progress: number) => void): Promise loadLatestDataInBatches(onProgress?: (progress: number) => void): Promise } /** * 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>( tableName: string, defaultLimit: number = 100 ) { const storeId = `table-${tableName}` const cacheKey = `table-data-${tableName}` return defineStore(storeId, { state: (): TableDataState => ({ 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 { // 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 { await this.fetchData() }, /** * Internal method to fetch data from API */ async fetchData(): Promise { 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 { 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 { 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 { // 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 { 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 { 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>( tableName: string, limit?: number ) { const store = createTableDataStore(tableName, limit) return store() }