/** * Composable para manejo de sesiones de catación en IndexedDB * Maneja solo una sesión activa a la vez */ import type { SesionCatacion } from '~/types/catacion' const DB_NAME = 'RioCataDB' const DB_VERSION = 1 const STORE_NAME = 'sesiones' const ACTIVE_SESSION_KEY = 'sesion-activa' /** * Inicializa y retorna la base de datos IndexedDB */ function openDatabase(): Promise { return new Promise((resolve, reject) => { // Verificar si estamos en el cliente if (typeof window === 'undefined' || !window.indexedDB) { reject(new Error('IndexedDB no está disponible en este entorno')) return } const request = window.indexedDB.open(DB_NAME, DB_VERSION) request.onerror = () => { reject(new Error('Error al abrir la base de datos')) } request.onsuccess = () => { resolve(request.result) } request.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result // Crear object store si no existe if (!db.objectStoreNames.contains(STORE_NAME)) { const objectStore = db.createObjectStore(STORE_NAME, { keyPath: 'sessionId' }) // Crear índice por fecha para consultas futuras objectStore.createIndex('fecha', 'fecha', { unique: false }) } } }) } /** * Guarda o actualiza la sesión activa en IndexedDB */ async function saveSession(sesion: SesionCatacion): Promise { try { const db = await openDatabase() const transaction = db.transaction([STORE_NAME], 'readwrite') const objectStore = transaction.objectStore(STORE_NAME) // Eliminar todas las sesiones anteriores await new Promise((resolve, reject) => { const clearRequest = objectStore.clear() clearRequest.onsuccess = () => resolve() clearRequest.onerror = () => reject(new Error('Error al limpiar sesiones anteriores')) }) // Guardar la nueva sesión await new Promise((resolve, reject) => { const addRequest = objectStore.add(sesion) addRequest.onsuccess = () => resolve() addRequest.onerror = () => reject(new Error('Error al guardar la sesión')) }) db.close() } catch (error) { console.error('Error en saveSession:', error) throw error } } /** * Migra una sesión del formato antiguo al nuevo */ function migrateSesion(sesion: any): SesionCatacion { // Migrar cada muestra const muestrasMigradas = sesion.muestras.map((muestra: any) => { const muestraMigrada = { ...muestra } // Migrar fraganciaAromaNotas if (muestra.fraganciaAromaNotas) { const notas = muestra.fraganciaAromaNotas // Si tiene el formato antiguo (categoria como string) if ('categoria' in notas && typeof notas.categoria === 'string') { muestraMigrada.fraganciaAromaNotas = { categorias: notas.categoria ? [notas.categoria] : [], subcategorias: notas.subcategoria ? [notas.subcategoria] : [], notaEspecifica: notas.notaEspecifica, } } // Si ya tiene el formato nuevo pero es null else if (notas.categorias === null || notas.categorias === undefined) { muestraMigrada.fraganciaAromaNotas = { categorias: [], subcategorias: [], notaEspecifica: notas.notaEspecifica || null, } } } // Migrar saborNotas if (muestra.saborNotas) { const notas = muestra.saborNotas // Si tiene el formato antiguo (categoria como string) if ('categoria' in notas && typeof notas.categoria === 'string') { muestraMigrada.saborNotas = { categorias: notas.categoria ? [notas.categoria] : [], subcategorias: notas.subcategoria ? [notas.subcategoria] : [], notaEspecifica: notas.notaEspecifica, } } // Si ya tiene el formato nuevo pero es null else if (notas.categorias === null || notas.categorias === undefined) { muestraMigrada.saborNotas = { categorias: [], subcategorias: [], notaEspecifica: notas.notaEspecifica || null, } } } return muestraMigrada }) return { ...sesion, muestras: muestrasMigradas, } } /** * Carga la sesión activa desde IndexedDB */ async function loadSession(): Promise { try { const db = await openDatabase() const transaction = db.transaction([STORE_NAME], 'readonly') const objectStore = transaction.objectStore(STORE_NAME) const sesion = await new Promise((resolve, reject) => { // Obtener todas las claves const getAllRequest = objectStore.getAll() getAllRequest.onsuccess = () => { const sesiones = getAllRequest.result as SesionCatacion[] // Retornar la primera sesión (debería haber solo una) let sesion = sesiones.length > 0 ? sesiones[0] : null // Migrar sesión si es necesario if (sesion) { sesion = migrateSesion(sesion) } resolve(sesion || null) } getAllRequest.onerror = () => { reject(new Error('Error al cargar la sesión')) } }) db.close() return sesion } catch (error) { console.error('Error en loadSession:', error) return null } } /** * Verifica si existe una sesión activa */ async function hasActiveSession(): Promise { try { const sesion = await loadSession() return sesion !== null } catch (error) { console.error('Error en hasActiveSession:', error) return false } } /** * Elimina la sesión activa */ async function deleteSession(): Promise { try { const db = await openDatabase() const transaction = db.transaction([STORE_NAME], 'readwrite') const objectStore = transaction.objectStore(STORE_NAME) await new Promise((resolve, reject) => { const clearRequest = objectStore.clear() clearRequest.onsuccess = () => resolve() clearRequest.onerror = () => reject(new Error('Error al eliminar la sesión')) }) db.close() } catch (error) { console.error('Error en deleteSession:', error) throw error } } /** * Actualiza una sesión existente (timestamp de modificación) */ async function updateSession(sesion: SesionCatacion): Promise { try { // Actualizar timestamp de modificación sesion.modificadoEn = Date.now() const db = await openDatabase() const transaction = db.transaction([STORE_NAME], 'readwrite') const objectStore = transaction.objectStore(STORE_NAME) await new Promise((resolve, reject) => { const putRequest = objectStore.put(sesion) putRequest.onsuccess = () => resolve() putRequest.onerror = () => reject(new Error('Error al actualizar la sesión')) }) db.close() } catch (error) { console.error('Error en updateSession:', error) throw error } } /** * Composable principal para manejo de IndexedDB */ export const useIndexedDB = () => { // Estado reactivo de la sesión const sesionActiva = useState('sesion-activa', () => null) const cargando = useState('sesion-cargando', () => false) const error = useState('sesion-error', () => null) /** * Inicializa el composable cargando la sesión activa */ const inicializar = async () => { if (import.meta.server) { // No hacer nada en el servidor return } try { cargando.value = true error.value = null const sesion = await loadSession() sesionActiva.value = sesion } catch (err) { error.value = err as Error console.error('Error al inicializar IndexedDB:', err) } finally { cargando.value = false } } /** * Guarda la sesión activa */ const guardar = async (sesion: SesionCatacion) => { if (import.meta.server) { console.warn('No se puede guardar en IndexedDB desde el servidor') return } try { cargando.value = true error.value = null await saveSession(sesion) sesionActiva.value = sesion } catch (err) { error.value = err as Error console.error('Error al guardar sesión:', err) throw err } finally { cargando.value = false } } /** * Actualiza la sesión activa */ const actualizar = async (sesion: SesionCatacion) => { if (import.meta.server) { console.warn('No se puede actualizar en IndexedDB desde el servidor') return } try { cargando.value = true error.value = null await updateSession(sesion) sesionActiva.value = sesion } catch (err) { error.value = err as Error console.error('Error al actualizar sesión:', err) throw err } finally { cargando.value = false } } /** * Elimina la sesión activa */ const eliminar = async () => { if (import.meta.server) { console.warn('No se puede eliminar de IndexedDB desde el servidor') return } try { cargando.value = true error.value = null await deleteSession() sesionActiva.value = null } catch (err) { error.value = err as Error console.error('Error al eliminar sesión:', err) throw err } finally { cargando.value = false } } /** * Verifica si hay una sesión activa */ const tieneSecion = computed(() => sesionActiva.value !== null) return { // Estado sesionActiva: readonly(sesionActiva), cargando: readonly(cargando), error: readonly(error), tieneSecion, // Métodos inicializar, guardar, actualizar, eliminar, } }