diff --git a/nuxt4-app/app.html b/nuxt4-app/app.html new file mode 100644 index 0000000..2bbf135 --- /dev/null +++ b/nuxt4-app/app.html @@ -0,0 +1,245 @@ + + + + + + {{ HEAD }} + + + + +
+ +
Analítica Núcleo
+
Data Studio
+
+
+
+
+
+ Iniciando aplicación +
+
+
+ + {{ APP }} + + + + \ No newline at end of file diff --git a/nuxt4-app/app/app.vue b/nuxt4-app/app/app.vue index 815669a..8a9d8f2 100644 --- a/nuxt4-app/app/app.vue +++ b/nuxt4-app/app/app.vue @@ -8,3 +8,16 @@ + + diff --git a/nuxt4-app/app/assets/css/main.css b/nuxt4-app/app/assets/css/main.css index 7fbd254..a5ac597 100644 --- a/nuxt4-app/app/assets/css/main.css +++ b/nuxt4-app/app/assets/css/main.css @@ -12,10 +12,14 @@ --brand-text-muted: #d8c7a6; } +/* Critical: Prevent white flash on load */ html, body { - background-color: var(--brand-bg); + margin: 0 !important; + padding: 0 !important; + background-color: #1b1209 !important; color: var(--brand-text); + min-height: 100vh; } .brand-shell { diff --git a/nuxt4-app/app/pages/explorer.vue b/nuxt4-app/app/pages/explorer.vue index ce74a2f..5ad0f49 100644 --- a/nuxt4-app/app/pages/explorer.vue +++ b/nuxt4-app/app/pages/explorer.vue @@ -10,25 +10,52 @@ - - - - - + +
+ + - - + + + + + + + +
+
+ + Última actualización: {{ currentTableStore.formattedLastUpdated }} + + + ⚠️ Los datos pueden estar desactualizados + +
+ + + + Actualizar datos + +
+
@@ -43,7 +70,7 @@ - + -
+
+ +
+
+ {{ currentTableStore.error }} +
+ + Reintentar + +
+ -
+
- No se encontraron datos en esta tabla. + +

No hay datos disponibles

+

+ Haz clic en "Actualizar datos" para cargar la información de esta tabla. +

+ + + Cargar datos +
@@ -129,8 +187,8 @@ import { h, resolveComponent } from 'vue' import { upperFirst } from 'scule' import type { TableColumn, DropdownMenuItem } from '@nuxt/ui' -import { useRequestFetch } from '#imports' import { useMetadataStore } from '~/stores/metadata' +import { useTableDataStore } from '~/stores/tableDataFactory' definePageMeta({ layout: 'dashboard', @@ -141,13 +199,12 @@ const UButton = resolveComponent('UButton') // State const selectedTableName = ref('') -const tableData = ref[]>([]) -const tableDataLoading = ref(false) const globalFilter = ref('') +const currentTableStore = ref | null>(null) -const requestFetch = useRequestFetch() const table = useTemplateRef('table') const metadataStore = useMetadataStore() +const { $getTableStore } = useNuxtApp() // Computed properties const selectedTable = computed(() => { @@ -189,9 +246,11 @@ const tableDropdownItems = computed((): DropdownMenuItem[] => { }) const tableColumns = computed((): TableColumn>[] => { - if (!tableData.value.length) return [] + if (!currentTableStore.value?.hasData) return [] + + const firstRow = currentTableStore.value.allRecords[0] + if (!firstRow) return [] - const firstRow = tableData.value[0] const columns = Object.keys(firstRow) return columns.map(column => ({ @@ -236,42 +295,33 @@ const filteredRowCount = computed(() => { }) const totalRowCount = computed(() => { - return tableData.value.length + return currentTableStore.value?.recordCount || 0 }) // Methods - -async function selectTable(tableName: string) { +function selectTable(tableName: string) { if (selectedTableName.value === tableName) return selectedTableName.value = tableName - await loadTableData(tableName) + + // Get the table store using the plugin + if (typeof $getTableStore === 'function') { + const store = $getTableStore(tableName) + if (store) { + currentTableStore.value = store + // Initialize the store (loads from cache or fetches) + store.initialize() + } + } else { + // Fallback: create store directly + currentTableStore.value = useTableDataStore(tableName) + currentTableStore.value.initialize() + } } -async function loadTableData(tableName: string) { - try { - tableDataLoading.value = true - - const response = await requestFetch(`/api/data/${tableName}`, { - query: { limit: '100' } - }) - - if (response && typeof response === 'object' && 'records' in response) { - const dataset = response as { - records?: Record[] - } - tableData.value = Array.isArray(dataset.records) ? dataset.records : [] - } else if (Array.isArray(response)) { - tableData.value = response - } else { - tableData.value = [] - } - - } catch (error) { - console.error('Error loading table data:', error) - tableData.value = [] - } finally { - tableDataLoading.value = false +async function refreshTableData() { + if (currentTableStore.value) { + await currentTableStore.value.refreshData() } } @@ -279,6 +329,10 @@ function updateGlobalFilter(value: string) { globalFilter.value = value } +function formatNumber(value: number): string { + return new Intl.NumberFormat('es-ES').format(value) +} + function formatCellValue(value: unknown): string { if (value === null || value === undefined) { return '—' @@ -306,22 +360,6 @@ function formatCellValue(value: unknown): string { return stringValue } -// Watchers -watch(selectedTableName, async (newTableName) => { - if (newTableName) { - await loadTableData(newTableName) - } else { - tableData.value = [] - } -}) - -// Auto-select first table when metadata becomes available -watch(() => metadataStore.hasMetadata, (hasMetadata) => { - if (hasMetadata && !selectedTableName.value && metadataStore.allTables.length > 0) { - selectedTableName.value = metadataStore.allTables[0].table - } -}) - // Lifecycle onMounted(async () => { await metadataStore.initialize() @@ -330,8 +368,15 @@ onMounted(async () => { if (metadataStore.hasMetadata && !selectedTableName.value) { const firstTable = metadataStore.allTables[0] if (firstTable) { - selectedTableName.value = firstTable.table + selectTable(firstTable.table) } } }) + +// Auto-select first table when metadata becomes available +watch(() => metadataStore.hasMetadata, (hasMetadata) => { + if (hasMetadata && !selectedTableName.value && metadataStore.allTables.length > 0) { + selectTable(metadataStore.allTables[0].table) + } +}) \ No newline at end of file diff --git a/nuxt4-app/app/stores/README.md b/nuxt4-app/app/stores/README.md new file mode 100644 index 0000000..f3e5130 --- /dev/null +++ b/nuxt4-app/app/stores/README.md @@ -0,0 +1,794 @@ +# 📚 Sistema de Stores - Analítica Núcleo + +Este directorio contiene el sistema de gestión de estado de la aplicación usando **Pinia** y **localStorage** para caché persistente. + +## 📋 Índice + +- [Arquitectura General](#-arquitectura-general) +- [Stores Disponibles](#-stores-disponibles) +- [Metadata Store](#-metadata-store) +- [Table Data Factory](#-table-data-factory) +- [Flujo de Datos](#-flujo-de-datos) +- [Uso en Componentes](#-uso-en-componentes) +- [API Reference](#-api-reference) +- [Cache y Persistencia](#-cache-y-persistencia) +- [Ejemplos Avanzados](#-ejemplos-avanzados) + +--- + +## 🏗️ Arquitectura General + +El sistema de stores se compone de dos partes principales: + +``` +app/stores/ +├── metadata.ts # Store de metadatos de tablas +├── tableDataFactory.ts # Factory para crear stores de datos +└── README.md # Este archivo +``` + +### Flujo de Datos + +```mermaid +graph TB + A[App Startup] --> B[Metadata Store] + B --> C{¿Tiene cache?} + C -->|Sí| D[Carga desde localStorage] + C -->|No| E[Fetch desde API] + E --> F[/api/metadata] + F --> G[Guarda en localStorage] + D --> H[Metadata disponible] + G --> H + + H --> I[Plugin Auto-registro] + I --> J[Crea stores por tabla] + + K[Usuario selecciona tabla] --> L{¿Store existe?} + L -->|Sí| M{¿Tiene cache?} + L -->|No| N[Crea store] + N --> M + M -->|Sí| O[Carga desde cache] + M -->|No| P[Fetch desde API] + P --> Q[/api/data/:table] + Q --> R[Guarda en cache] + O --> S[Datos disponibles] + R --> S +``` + +--- + +## 🗂️ Stores Disponibles + +### 1. **Metadata Store** (`metadata.ts`) + +Store global que gestiona la información de todas las tablas disponibles. + +**Responsabilidades:** +- Mantener lista de tablas disponibles +- Información estructural (columnas, claves primarias, etc.) +- Estadísticas de cada tabla +- Cache persistente de metadatos + +### 2. **Table Data Stores** (Factory pattern) + +Stores dinámicos, uno por cada tabla de la base de datos. + +**Responsabilidades:** +- Almacenar registros de una tabla específica +- Gestionar estado de carga +- Cache persistente por tabla +- Operaciones de filtrado y búsqueda + +--- + +## 🔍 Metadata Store + +### Estructura de Datos + +```typescript +interface TableMetadata { + table: string // Nombre de la tabla + rowCount: number // Total de registros + primaryKey: string // Clave primaria + approxSizeBytes: number // Tamaño aproximado en bytes + columns: string[] // Lista de columnas + createdAtRange?: { + from: string // Fecha de creación más antigua + to: string // Fecha de creación más reciente + } + lastRefreshed?: string // Última actualización de metadatos + sampleRow?: Record // Registro de ejemplo +} +``` + +### Estado del Store + +```typescript +{ + metadata: TableMetadata[] // Array de metadatos por tabla + loading: boolean // Estado de carga + error: string | null // Mensaje de error + lastUpdated: string | null // Timestamp última actualización + initialized: boolean // Si el store está inicializado +} +``` + +### Getters Disponibles + +| Getter | Tipo | Descripción | +|--------|------|-------------| +| `allTables` | `TableMetadata[]` | Todas las tablas disponibles | +| `getTableMetadata(name)` | `TableMetadata \| undefined` | Metadatos de una tabla específica | +| `totalTables` | `number` | Cantidad total de tablas | +| `totalRecords` | `number` | Suma de registros de todas las tablas | +| `tableNames` | `string[]` | Lista de nombres de tablas | +| `hasMetadata` | `boolean` | Si hay metadatos cargados | +| `isLoading` | `boolean` | Si está cargando actualmente | +| `hasError` | `boolean` | Si hay un error | +| `formattedLastUpdated` | `string` | Fecha formateada de última actualización | +| `isStale` | `boolean` | Si los datos tienen > 5 minutos | + +### Actions Disponibles + +| Action | Parámetros | Descripción | +|--------|------------|-------------| +| `loadMetadata(force?)` | `force: boolean` | Carga lazy, solo si necesario | +| `refreshMetadata()` | - | Actualización forzada | +| `clearMetadata()` | - | Limpia cache y datos | +| `initialize()` | - | Inicializa el store (cache + fetch) | + +### Fuente de Datos + +**Endpoint:** `GET /api/metadata` + +**Respuesta:** +```json +[ + { + "table": "usuarios", + "rowCount": 1523, + "primaryKey": "id", + "approxSizeBytes": 245760, + "columns": ["id", "nombre", "email", "created_at"], + "createdAtRange": { + "from": "2024-01-01T00:00:00Z", + "to": "2025-01-20T15:30:00Z" + } + }, + // ... más tablas +] +``` + +### Uso Básico + +```typescript +import { useMetadataStore } from '~/stores/metadata' + +// En un componente +const metadataStore = useMetadataStore() + +// Inicializar (carga desde cache o API) +await metadataStore.initialize() + +// Acceder a los datos +console.log(metadataStore.allTables) +console.log(metadataStore.totalTables) + +// Obtener metadatos de una tabla específica +const usuariosMetadata = metadataStore.getTableMetadata('usuarios') + +// Refrescar metadatos +await metadataStore.refreshMetadata() + +// Verificar frescura de datos +if (metadataStore.isStale) { + console.log('Los metadatos tienen más de 5 minutos') +} +``` + +--- + +## 🏭 Table Data Factory + +Sistema factory que crea stores dinámicos para cada tabla. + +### Estructura de Datos + +```typescript +interface TableDataState> { + data: T[] // Registros cacheados + loading: boolean // Estado de carga + error: string | null // Mensaje de error + lastUpdated: string | null // Timestamp última actualización + initialized: boolean // Si el store está inicializado + limit: number // Límite de registros +} +``` + +### Getters Disponibles + +| Getter | Tipo | Descripción | +|--------|------|-------------| +| `allRecords` | `T[]` | Todos los registros cacheados | +| `hasData` | `boolean` | Si hay datos disponibles | +| `isLoading` | `boolean` | Si está cargando actualmente | +| `hasError` | `boolean` | Si hay un error | +| `recordCount` | `number` | Cantidad de registros | +| `formattedLastUpdated` | `string` | Fecha formateada | +| `isStale` | `boolean` | Si los datos tienen > 5 minutos | + +### Actions Disponibles + +| Action | Parámetros | Descripción | +|--------|------------|-------------| +| `loadData(force?)` | `force: boolean` | Carga lazy | +| `refreshData()` | - | Actualización forzada | +| `clearData()` | - | Limpia cache y datos | +| `initialize()` | - | Inicializa (cache + fetch) | +| `getRecord(id)` | `id: string \| number` | Obtiene un registro por ID | +| `filterRecords(predicate)` | `predicate: (record: T) => boolean` | Filtra registros | + +### Fuente de Datos + +**Endpoint:** `GET /api/data/:tableName?limit=100` + +**Respuesta:** +```json +{ + "table": "usuarios", + "count": 100, + "limit": 100, + "records": [ + { + "id": 1, + "nombre": "Juan Pérez", + "email": "juan@example.com", + "created_at": "2024-01-15T10:30:00Z" + }, + // ... más registros + ] +} +``` + +### Creación de Stores + +#### Método 1: Usando la Factory Directamente + +```typescript +import { createTableDataStore } from '~/stores/tableDataFactory' + +// Crear un store para la tabla "usuarios" +const useUsuariosStore = createTableDataStore('usuarios', 100) + +// Usar el store +const usuariosStore = useUsuariosStore() +await usuariosStore.initialize() + +console.log(usuariosStore.allRecords) +``` + +#### Método 2: Usando el Helper + +```typescript +import { useTableDataStore } from '~/stores/tableDataFactory' + +// Crear y usar el store en un solo paso +const usuariosStore = useTableDataStore('usuarios', 100) +await usuariosStore.initialize() +``` + +#### Método 3: Usando el Plugin (Recomendado) + +```typescript +// El plugin auto-registra todos los stores +const { $getTableStore } = useNuxtApp() + +// Obtener un store ya registrado +const usuariosStore = $getTableStore('usuarios') + +if (usuariosStore) { + await usuariosStore.initialize() + console.log(usuariosStore.allRecords) +} +``` + +--- + +## 🔄 Flujo de Datos + +### Inicialización de la App + +```typescript +// 1. App.vue o plugin se ejecuta +onMounted(async () => { + // 2. Metadata store se inicializa + const metadataStore = useMetadataStore() + await metadataStore.initialize() + + // 3. Plugin auto-registra stores (tableStores.client.ts) + // Se crean stores para cada tabla encontrada en metadataStore.allTables + + // 4. Los stores están listos pero NO han cargado datos aún + // Esto es lazy loading - solo cargan cuando se necesitan +}) +``` + +### Carga de Datos de una Tabla + +```typescript +// Usuario selecciona tabla "usuarios" +const selectTable = async (tableName: string) => { + // 1. Obtener o crear store + const store = $getTableStore(tableName) || useTableDataStore(tableName) + + // 2. Inicializar (intenta cache primero) + await store.initialize() + + // Flujo interno de initialize(): + // ├─ loadFromCache() + // │ └─ Lee localStorage['table-data-usuarios'] + // │ ├─ Si existe → usa datos cacheados + // │ └─ Si no existe → continúa + // └─ loadData() + // └─ fetchData() + // └─ $fetch('/api/data/usuarios?limit=100') + // └─ Guarda en localStorage + // └─ Actualiza store + + // 3. Datos disponibles + console.log(store.allRecords) +} +``` + +### Actualización Manual + +```typescript +// Usuario hace click en "Actualizar datos" +const refreshTable = async () => { + // 1. Ejecutar refreshData() + await store.refreshData() + + // Flujo interno de refreshData(): + // └─ fetchData() + // ├─ loading = true + // ├─ $fetch('/api/data/usuarios?limit=100') + // ├─ Actualiza store.data + // ├─ Actualiza lastUpdated + // ├─ Guarda en localStorage + // └─ loading = false +} +``` + +--- + +## 💻 Uso en Componentes + +### Ejemplo Completo: Explorador de Tablas + +```vue + + + +``` + +### Ejemplo: Búsqueda y Filtrado + +```typescript +import { useTableDataStore } from '~/stores/tableDataFactory' + +const usuariosStore = useTableDataStore('usuarios') +await usuariosStore.initialize() + +// Filtrar usuarios activos +const usuariosActivos = usuariosStore.filterRecords( + user => user.activo === true +) + +// Buscar usuario por ID +const usuario = usuariosStore.getRecord(123) + +// Búsqueda personalizada +const usuariosPorEmail = usuariosStore.filterRecords( + user => user.email.includes('@gmail.com') +) + +console.log(`Encontrados ${usuariosPorEmail.length} usuarios con Gmail`) +``` + +--- + +## 📖 API Reference + +### Metadata Store API + +```typescript +interface MetadataStore { + // State + metadata: TableMetadata[] + loading: boolean + error: string | null + lastUpdated: string | null + initialized: boolean + + // Getters + allTables: TableMetadata[] + getTableMetadata(name: string): TableMetadata | undefined + totalTables: number + totalRecords: number + tableNames: string[] + hasMetadata: boolean + isLoading: boolean + hasError: boolean + formattedLastUpdated: string + isStale: boolean + + // Actions + loadMetadata(force?: boolean): Promise + refreshMetadata(): Promise + clearMetadata(): void + initialize(): Promise +} +``` + +### Table Data Store API + +```typescript +interface TableDataStore { + // State + data: T[] + loading: boolean + error: string | null + lastUpdated: string | null + initialized: boolean + limit: number + + // Getters + allRecords: T[] + hasData: boolean + isLoading: boolean + hasError: boolean + recordCount: number + formattedLastUpdated: string + isStale: boolean + + // Actions + loadData(force?: boolean): Promise + refreshData(): Promise + clearData(): void + initialize(): Promise + getRecord(id: string | number): T | undefined + filterRecords(predicate: (record: T) => boolean): T[] +} +``` + +--- + +## 💾 Cache y Persistencia + +### Estrategia de Cache + +El sistema usa **localStorage** para persistir datos: + +```typescript +// Estructura de cache en localStorage +{ + // Metadatos + "metadata-cache": { + metadata: TableMetadata[], + lastUpdated: "2025-01-20T10:30:00Z" + }, + + // Datos de tabla usuarios + "table-data-usuarios": { + data: [...registros], + lastUpdated: "2025-01-20T10:35:00Z", + limit: 100 + }, + + // Datos de tabla productos + "table-data-productos": { + data: [...registros], + lastUpdated: "2025-01-20T10:40:00Z", + limit: 100 + } +} +``` + +### Políticas de Cache + +1. **Freshness Check**: Los datos se consideran "stale" después de 5 minutos +2. **Lazy Loading**: Los stores solo cargan cuando se accede a ellos +3. **Cache-First**: Siempre intenta cargar desde cache primero +4. **Auto-Refresh**: El usuario controla manualmente cuándo refrescar +5. **Offline Support**: Datos disponibles incluso sin conexión + +### Limpieza de Cache + +```typescript +// Limpiar cache de metadatos +const metadataStore = useMetadataStore() +metadataStore.clearMetadata() + +// Limpiar cache de una tabla +const usuariosStore = useTableDataStore('usuarios') +usuariosStore.clearData() + +// Limpiar TODO el cache (localStorage) +if (process.client) { + localStorage.clear() +} +``` + +--- + +## 🎯 Ejemplos Avanzados + +### Sincronización Automática + +```typescript +// Auto-refrescar cada 5 minutos si los datos están stale +const autoRefresh = () => { + const store = useTableDataStore('usuarios') + + setInterval(async () => { + if (store.isStale && !store.isLoading) { + console.log('Refrescando datos automáticamente...') + await store.refreshData() + } + }, 5 * 60 * 1000) // 5 minutos +} +``` + +### Validación de Datos + +```typescript +// Verificar integridad de datos cacheados +const validateCache = async (tableName: string) => { + const store = useTableDataStore(tableName) + const metadata = metadataStore.getTableMetadata(tableName) + + if (!metadata) return false + + // Verificar que el número de registros sea razonable + if (store.recordCount > metadata.rowCount) { + console.warn('Cache corrupto, limpiando...') + store.clearData() + await store.refreshData() + return false + } + + return true +} +``` + +### Composable Personalizado + +```typescript +// composables/useTableData.ts +export function useTableData(tableName: string) { + const store = useTableDataStore(tableName) + const initialized = ref(false) + + onMounted(async () => { + await store.initialize() + initialized.value = true + }) + + // Auto-refrescar si stale + watch(() => store.isStale, async (isStale) => { + if (isStale && initialized.value) { + await store.refreshData() + } + }) + + return { + data: computed(() => store.allRecords), + loading: computed(() => store.isLoading), + error: computed(() => store.error), + refresh: () => store.refreshData() + } +} +``` + +### TypeScript Tipado + +```typescript +// Definir tipos para tus tablas +interface Usuario { + id: number + nombre: string + email: string + activo: boolean + created_at: string +} + +interface Producto { + id: number + nombre: string + precio: number + stock: number +} + +// Usar stores tipados +const usuariosStore = useTableDataStore('usuarios') +const productosStore = useTableDataStore('productos') + +// TypeScript ahora conoce la estructura +usuariosStore.allRecords.forEach(user => { + console.log(user.email) // ✅ TypeScript sabe que existe + // console.log(user.foo) // ❌ Error: Property 'foo' does not exist +}) +``` + +--- + +## 🐛 Debugging + +### Inspeccionar Estado + +```typescript +// En DevTools Console +import { useMetadataStore } from '~/stores/metadata' +import { useTableDataStore } from '~/stores/tableDataFactory' + +const metadataStore = useMetadataStore() +const usuariosStore = useTableDataStore('usuarios') + +// Ver estado completo +console.log('Metadata:', metadataStore.$state) +console.log('Usuarios:', usuariosStore.$state) + +// Ver cache +console.log('Cache:', { + metadata: localStorage.getItem('metadata-cache'), + usuarios: localStorage.getItem('table-data-usuarios') +}) +``` + +### Logs Detallados + +Descomenta los `console.log` en los stores para ver el flujo completo: + +```typescript +// En tableDataFactory.ts línea 144 +console.log(`[${tableName}] Fetching data...`) + +// En tableDataFactory.ts línea 158 +console.log(`[${tableName}] Data fetched: ${this.recordCount} records`) + +// En tableDataFactory.ts línea 164 +console.log(`[${tableName}] Saved to cache`) +``` + +--- + +## 📝 Notas Importantes + +### ⚠️ Limitaciones + +1. **Límite de localStorage**: ~5-10MB por dominio + - Solución: Implementar estrategia LRU si se excede + +2. **Límite de registros**: Por defecto 100 registros por tabla + - Modificable en la factory: `useTableDataStore('tabla', 500)` + +3. **Sin paginación**: Todos los registros se cargan de una vez + - Para tablas grandes, considerar implementar paginación + +4. **Sin sincronización real-time**: Los datos se actualizan manualmente + - Para real-time, considerar WebSockets + +### 🚀 Performance Tips + +1. **Lazy Loading**: Los stores no cargan datos hasta que se necesitan +2. **Cache-First**: Usa cache para respuesta instantánea +3. **Selectores Computados**: Usa `computed()` para filtros complejos +4. **Debounce**: Para búsquedas, usa debounce para evitar renders excesivos + +### 🔒 Seguridad + +- Los datos en localStorage NO están encriptados +- No almacenes información sensible (contraseñas, tokens) +- El API backend debe validar permisos y autenticación + +--- + +## 📚 Referencias + +- [Pinia Documentation](https://pinia.vuejs.org/) +- [Nuxt 4 Documentation](https://nuxt.com/) +- [LocalStorage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) + +--- + +**Última actualización:** 2025-01-20 +**Versión:** 1.0.0 +**Autor:** Equipo Núcleo \ No newline at end of file diff --git a/nuxt4-app/app/stores/tableDataFactory.ts b/nuxt4-app/app/stores/tableDataFactory.ts new file mode 100644 index 0000000..f0d454b --- /dev/null +++ b/nuxt4-app/app/stores/tableDataFactory.ts @@ -0,0 +1,277 @@ +import { defineStore } from 'pinia' + +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(): void + loadFromCache(): void + extractErrorMessage(error: unknown): string + initialize(): Promise + 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>( + 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: (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 { + // 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?: 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 { + // 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>( + tableName: string, + limit?: number +) { + const store = createTableDataStore(tableName, limit) + return store() +} \ No newline at end of file diff --git a/nuxt4-app/nuxt.config.ts b/nuxt4-app/nuxt.config.ts index b4f8e71..04802f9 100644 --- a/nuxt4-app/nuxt.config.ts +++ b/nuxt4-app/nuxt.config.ts @@ -5,6 +5,27 @@ export default defineNuxtConfig({ devtools: { enabled: true }, css: ['~/assets/css/main.css'], modules: ['@nuxt/image', '@nuxt/ui', '@nuxt/test-utils', '@vite-pwa/nuxt', '@pinia/nuxt'], + + // Performance optimizations + experimental: { + payloadExtraction: false, + viewTransition: true + }, + + // Optimize build + vite: { + build: { + cssCodeSplit: true, + rollupOptions: { + output: { + manualChunks: { + 'vendor-ui': ['@nuxt/ui'], + 'vendor-pinia': ['pinia', '@pinia/nuxt'] + } + } + } + } + }, app: { head: { link: [ @@ -13,7 +34,7 @@ export default defineNuxtConfig({ { rel: 'manifest', href: '/manifest.webmanifest' } ], meta: [ - { name: 'theme-color', content: '#14100b' }, + { name: 'theme-color', content: '#1b1209' }, { name: 'apple-mobile-web-app-capable', content: 'yes' }, { name: 'mobile-web-app-capable', content: 'yes' }, { name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' } diff --git a/nuxt4-app/plugins/loading.client.ts b/nuxt4-app/plugins/loading.client.ts new file mode 100644 index 0000000..1f2b564 --- /dev/null +++ b/nuxt4-app/plugins/loading.client.ts @@ -0,0 +1,22 @@ +export default defineNuxtPlugin(() => { + // This plugin ensures the loading screen is properly hidden when the app is ready + + if (process.client) { + // Listen for when the app is fully hydrated + const checkReady = () => { + // Wait for next tick to ensure DOM is painted + requestAnimationFrame(() => { + setTimeout(() => { + document.documentElement.classList.add('nuxt-ready') + }, 50) + }) + } + + // Multiple triggers to ensure loading screen is hidden + if (document.readyState === 'complete') { + checkReady() + } else { + window.addEventListener('load', checkReady) + } + } +}) \ No newline at end of file diff --git a/nuxt4-app/plugins/tableStores.client.ts b/nuxt4-app/plugins/tableStores.client.ts new file mode 100644 index 0000000..5b035d3 --- /dev/null +++ b/nuxt4-app/plugins/tableStores.client.ts @@ -0,0 +1,42 @@ +import { useMetadataStore } from '~/stores/metadata' +import { createTableDataStore } from '~/stores/tableDataFactory' + +export default defineNuxtPlugin(async (nuxtApp) => { + // Wait for metadata to be available + const metadataStore = useMetadataStore() + + // Initialize metadata first + await metadataStore.initialize() + + // Create stores for all available tables + const tableStores = new Map>() + + metadataStore.allTables.forEach((table) => { + const storeName = table.table + const storeFactory = createTableDataStore(storeName, 100) + + // Register the store + tableStores.set(storeName, storeFactory) + + // Optionally initialize stores in the background (lazy loading) + // You can uncomment this to preload all data + // const storeInstance = storeFactory() + // storeInstance.loadFromCache() + }) + + // Provide access to table stores + return { + provide: { + tableStores, + // Helper function to get a table store + getTableStore: (tableName: string) => { + const storeFactory = tableStores.get(tableName) + if (!storeFactory) { + console.warn(`Table store for "${tableName}" not found`) + return null + } + return storeFactory() + } + } + } +}) \ No newline at end of file