Files
cataRio/nuxt4/app/composables/useIndexedDB.ts
josedario87 87fb92d210
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m3s
Feat: Implementar UI completa de RioCata - Sistema de catación de café
Agregar sistema completo de catación de café con las siguientes características:

- Tipos TypeScript completos para sesiones, muestras, intensidades y notas
- Composable useIndexedDB para gestión de sesión activa en cliente
- Composable useCatacion con lógica de negocio para actualización de muestras
- Componentes reutilizables:
  * SliderIntensidad: Slider dual para valores descriptivos (1-10) y afectivos (1-15)
  * SelectorFamilia: Selector jerárquico de familias de notas (3 niveles)
  * SelectorTazas: Selector de tazas (1-5) para uniformidad y defectos
  * ResumenMuestra: Header de accordion con progreso y estadísticas
  * FormularioMuestra: Formulario completo con 3 tabs (Fragancia/Aroma, Sabor, Impresión Global)
- Páginas:
  * /cata: Gestión de sesiones (crear nueva o continuar existente)
  * /cata/sesion: Interfaz principal de catación con accordions y tabs
- Tema dual:
  * Modo claro: Fondo blanco, texto negro, outlines azules
  * Modo oscuro: Fondo negro, texto verde terminal, estilo monospace
- Diseño mobile-first responsive con CSS vanilla (sin @apply de Tailwind)
- Configuración PWA con almacenamiento en IndexedDB
2025-10-18 01:39:27 -06:00

289 lines
7.5 KiB
TypeScript

/**
* 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<IDBDatabase> {
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<void> {
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<void>((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<void>((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
}
}
/**
* Carga la sesión activa desde IndexedDB
*/
async function loadSession(): Promise<SesionCatacion | null> {
try {
const db = await openDatabase()
const transaction = db.transaction([STORE_NAME], 'readonly')
const objectStore = transaction.objectStore(STORE_NAME)
const sesion = await new Promise<SesionCatacion | null>((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)
const sesion = sesiones.length > 0 ? sesiones[0] : null
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<boolean> {
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<void> {
try {
const db = await openDatabase()
const transaction = db.transaction([STORE_NAME], 'readwrite')
const objectStore = transaction.objectStore(STORE_NAME)
await new Promise<void>((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<void> {
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<void>((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<SesionCatacion | null>('sesion-activa', () => null)
const cargando = useState<boolean>('sesion-cargando', () => false)
const error = useState<Error | null>('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,
}
}