Files
analiticaNucleo/nuxt4-app/app/stores/tableDataFactory.ts

277 lines
7.4 KiB
TypeScript

import { defineStore } from 'pinia'
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(): void
loadFromCache(): void
extractErrorMessage(error: unknown): string
initialize(): Promise<void>
getRecord(id: string | number): T | undefined
filterRecords(predicate: (record: T) => boolean): T[]
}
/**
* 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: (state): T[] => state.data,
/**
* Check if data is available
*/
hasData: (state): boolean => state.data.length > 0,
/**
* Check if data is currently loading
*/
isLoading: (state): boolean => state.loading,
/**
* Check if there's an error
*/
hasError: (state): boolean => !!state.error,
/**
* Get total number of records
*/
recordCount: (state): number => state.data.length,
/**
* Get formatted last updated time
*/
formattedLastUpdated: (state): string => {
if (!state.lastUpdated) return 'Nunca'
try {
return new Date(state.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: (state): boolean => {
if (!state.lastUpdated) return true
const lastUpdate = new Date(state.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?: T[] }
this.data = Array.isArray(dataset.records) ? dataset.records : []
} else if (Array.isArray(response)) {
this.data = response as T[]
} else {
this.data = []
}
this.lastUpdated = new Date().toISOString()
this.initialized = true
// Persist to localStorage for offline access
if (process.client) {
try {
localStorage.setItem(cacheKey, JSON.stringify({
data: this.data,
lastUpdated: this.lastUpdated,
limit: this.limit
}))
} catch (error) {
console.warn(`Failed to persist ${tableName} data to localStorage:`, 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 localStorage cache
*/
loadFromCache(): void {
if (!process.client) return
try {
const cached = localStorage.getItem(cacheKey)
if (cached) {
const parsedCache = JSON.parse(cached)
this.data = parsedCache.data || []
this.lastUpdated = parsedCache.lastUpdated || null
this.limit = parsedCache.limit || defaultLimit
this.initialized = true
}
} catch (error) {
console.warn(`Failed to load ${tableName} data from cache:`, error)
}
},
/**
* Clear all data
*/
clearData(): void {
this.data = []
this.error = null
this.lastUpdated = null
this.initialized = false
if (process.client) {
try {
localStorage.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
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.filter(predicate)
}
}
})
}
/**
* 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()
}