All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m8s
Implementa la funcionalidad para que las subcategorías seleccionadas persistan: - Entre cambios de tabs (cada tab mantiene sus propias subcategorías) - Entre refrescos de página (guardado en localStorage) Cambios técnicos: - Modificado useCatacion.ts para guardar subcategorías por tab en lugar de array global - Agregada función actualizarSubcategorias() que persiste en localStorage - subcategoriasActivas ahora es computed basado en la tab activa - Removida limpieza de subcategorías al cambiar de tab en sesion.vue - Actualizado toggleSubcategoria para usar la nueva función de persistencia
385 lines
11 KiB
TypeScript
385 lines
11 KiB
TypeScript
/**
|
|
* Composable para manejar la lógica de negocio de catación
|
|
*/
|
|
|
|
import type { SesionCatacion, Muestra, IntensidadValor } from '~/types/catacion'
|
|
import { crearSesionVacia, calcularPuntajeFinal } from '~/types/catacion'
|
|
|
|
export type TabCatacion = 'organoleptica' | 'descriptiva-afectiva' | 'defectos' | 'impresion-global'
|
|
|
|
// Subcategorías para Organoléptica
|
|
export type SubcategoriaOrganoleptica = 'fragancia-aroma' | 'sabor' | 'sensacion-boca' | 'gustos-predominantes'
|
|
|
|
// Subcategorías para Descriptiva/Afectiva
|
|
export type SubcategoriaDescriptivaAfectiva = 'descriptiva' | 'afectiva' | 'fragancia' | 'aroma' | 'sabor' | 'sabor-residual' | 'acidez' | 'dulzor' | 'sensacion-boca' | 'impresion-global'
|
|
|
|
// Tipo unión de todas las subcategorías
|
|
export type Subcategoria = SubcategoriaOrganoleptica | SubcategoriaDescriptivaAfectiva | null
|
|
|
|
// Tipo para las subcategorías por tab
|
|
export type SubcategoriasPorTab = {
|
|
'organoleptica': Subcategoria[]
|
|
'descriptiva-afectiva': Subcategoria[]
|
|
'defectos': Subcategoria[]
|
|
'impresion-global': Subcategoria[]
|
|
}
|
|
|
|
// Clave para localStorage
|
|
const STORAGE_KEY_SUBCATEGORIAS = 'riocata-subcategorias-activas'
|
|
|
|
export const useCatacion = () => {
|
|
const { sesionActiva, cargando, error, guardar, actualizar, eliminar } = useIndexedDB()
|
|
|
|
// Cargar subcategorías desde localStorage
|
|
const cargarSubcategoriasDesdeStorage = (): SubcategoriasPorTab => {
|
|
if (process.client) {
|
|
try {
|
|
const stored = localStorage.getItem(STORAGE_KEY_SUBCATEGORIAS)
|
|
if (stored) {
|
|
return JSON.parse(stored)
|
|
}
|
|
} catch (err) {
|
|
console.warn('Error al cargar subcategorías desde localStorage:', err)
|
|
}
|
|
}
|
|
return {
|
|
'organoleptica': [],
|
|
'descriptiva-afectiva': [],
|
|
'defectos': [],
|
|
'impresion-global': [],
|
|
}
|
|
}
|
|
|
|
// Estado de la UI
|
|
const tabActiva = useState<TabCatacion>('tab-activa', () => 'organoleptica')
|
|
const subcategoriasPorTab = useState<SubcategoriasPorTab>('subcategorias-por-tab', cargarSubcategoriasDesdeStorage)
|
|
const accordionAbierto = useState<string[]>('accordion-abierto', () => [])
|
|
|
|
// Computed para obtener las subcategorías de la tab actual
|
|
const subcategoriasActivas = computed(() => subcategoriasPorTab.value[tabActiva.value])
|
|
|
|
/**
|
|
* Actualiza las subcategorías de la tab actual y las guarda en localStorage
|
|
*/
|
|
const actualizarSubcategorias = (subcategorias: Subcategoria[]) => {
|
|
subcategoriasPorTab.value[tabActiva.value] = subcategorias
|
|
if (process.client) {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY_SUBCATEGORIAS, JSON.stringify(subcategoriasPorTab.value))
|
|
} catch (err) {
|
|
console.warn('Error al guardar subcategorías en localStorage:', err)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Crea una nueva sesión de catación
|
|
*/
|
|
const crearNuevaSesion = async (catador: string, cantidadMuestras: number) => {
|
|
try {
|
|
const nuevaSesion = crearSesionVacia(catador, cantidadMuestras)
|
|
await guardar(nuevaSesion)
|
|
// Resetear estado de UI
|
|
tabActiva.value = 'organoleptica'
|
|
accordionAbierto.value = []
|
|
return nuevaSesion
|
|
} catch (err) {
|
|
console.error('Error al crear nueva sesión:', err)
|
|
throw err
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Actualiza una muestra específica y guarda en IndexedDB
|
|
*/
|
|
const actualizarMuestra = async (muestraId: number, muestraActualizada: Partial<Muestra>) => {
|
|
if (!sesionActiva.value) {
|
|
throw new Error('No hay sesión activa')
|
|
}
|
|
|
|
try {
|
|
const muestra = sesionActiva.value.muestras.find(m => m.muestraId === muestraId)
|
|
|
|
if (!muestra) {
|
|
throw new Error(`Muestra con ID ${muestraId} no encontrada`)
|
|
}
|
|
|
|
// Actualizar muestra directamente (mutación)
|
|
Object.assign(muestra, muestraActualizada)
|
|
|
|
// Recalcular puntaje final si hay cambios en intensidades
|
|
if (muestraActualizada.intensidades) {
|
|
muestra.puntajeFinal = calcularPuntajeFinal(muestra)
|
|
}
|
|
|
|
// Guardar en IndexedDB
|
|
await actualizar(sesionActiva.value)
|
|
} catch (err) {
|
|
console.error('Error al actualizar muestra:', err)
|
|
throw err
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Actualiza un valor de intensidad específico
|
|
*/
|
|
const actualizarIntensidad = async (
|
|
muestraId: number,
|
|
parametro: keyof Muestra['intensidades'],
|
|
tipo: 'descriptiva' | 'afectiva',
|
|
valor: number | null
|
|
) => {
|
|
if (!sesionActiva.value) {
|
|
throw new Error('No hay sesión activa')
|
|
}
|
|
|
|
try {
|
|
const muestra = sesionActiva.value.muestras.find(m => m.muestraId === muestraId)
|
|
|
|
if (!muestra) {
|
|
throw new Error(`Muestra con ID ${muestraId} no encontrada`)
|
|
}
|
|
|
|
// Actualizar valor de intensidad directamente (mutación)
|
|
muestra.intensidades[parametro][tipo] = valor
|
|
|
|
// Recalcular puntaje final
|
|
muestra.puntajeFinal = calcularPuntajeFinal(muestra)
|
|
|
|
// Guardar en IndexedDB (pasa la sesión actual, no un clon)
|
|
await actualizar(sesionActiva.value)
|
|
} catch (err) {
|
|
console.error('Error al actualizar intensidad:', err)
|
|
throw err
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Actualiza el nombre de una muestra
|
|
*/
|
|
const actualizarNombreMuestra = async (muestraId: number, nombre: string) => {
|
|
await actualizarMuestra(muestraId, { nombre })
|
|
}
|
|
|
|
/**
|
|
* Actualiza las notas de fragancia/aroma
|
|
*/
|
|
const actualizarFraganciaAroma = async (
|
|
muestraId: number,
|
|
categorias: string[],
|
|
subcategorias: string[],
|
|
notaEspecifica: string | null
|
|
) => {
|
|
await actualizarMuestra(muestraId, {
|
|
fraganciaAromaNotas: { categorias: categorias as any, subcategorias, notaEspecifica },
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Actualiza las notas de sabor
|
|
*/
|
|
const actualizarSabor = async (
|
|
muestraId: number,
|
|
categorias: string[],
|
|
subcategorias: string[],
|
|
notaEspecifica: string | null
|
|
) => {
|
|
await actualizarMuestra(muestraId, {
|
|
saborNotas: { categorias: categorias as any, subcategorias, notaEspecifica },
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Actualiza tazas no uniformes
|
|
*/
|
|
const actualizarTazasNoUniformes = async (muestraId: number, tazas: number[]) => {
|
|
await actualizarMuestra(muestraId, { tazasNoUniformes: tazas })
|
|
}
|
|
|
|
/**
|
|
* Actualiza tazas defectuosas
|
|
*/
|
|
const actualizarTazasDefectuosas = async (muestraId: number, tazas: number[]) => {
|
|
await actualizarMuestra(muestraId, { tazasDefectuosas: tazas })
|
|
}
|
|
|
|
/**
|
|
* Actualiza el tipo de defecto
|
|
*/
|
|
const actualizarDefecto = async (muestraId: number, defecto: string | null) => {
|
|
await actualizarMuestra(muestraId, { defecto: defecto as any })
|
|
}
|
|
|
|
/**
|
|
* Actualiza sensaciones en boca
|
|
*/
|
|
const actualizarSensacionBoca = async (muestraId: number, sensaciones: string[]) => {
|
|
await actualizarMuestra(muestraId, { sensacionEnBoca: sensaciones as any })
|
|
}
|
|
|
|
/**
|
|
* Actualiza gustos predominantes (máximo 2)
|
|
*/
|
|
const actualizarGustosPredominantes = async (muestraId: number, gustos: string[]) => {
|
|
if (gustos.length > 2) {
|
|
throw new Error('Máximo 2 gustos predominantes permitidos')
|
|
}
|
|
if (gustos.length < 1) {
|
|
throw new Error('Mínimo 1 gusto predominante requerido')
|
|
}
|
|
await actualizarMuestra(muestraId, { gustosPredominantes: gustos as any })
|
|
}
|
|
|
|
/**
|
|
* Actualiza notas adicionales
|
|
*/
|
|
const actualizarOtrasNotas = async (muestraId: number, notas: string) => {
|
|
await actualizarMuestra(muestraId, { otrasNotas: notas })
|
|
}
|
|
|
|
/**
|
|
* Obtiene una muestra específica
|
|
*/
|
|
const obtenerMuestra = (muestraId: number): Muestra | null => {
|
|
if (!sesionActiva.value) return null
|
|
const muestra = sesionActiva.value.muestras.find(m => m.muestraId === muestraId)
|
|
return muestra || null
|
|
}
|
|
|
|
/**
|
|
* Verifica si una muestra está completa (tiene todos los campos requeridos)
|
|
*/
|
|
const esMuestraCompleta = (muestra: Muestra): boolean => {
|
|
// Verificar intensidades afectivas (son las que cuentan para el puntaje)
|
|
const intensidadesCompletas = Object.values(muestra.intensidades).every(
|
|
(intensidad) => intensidad.afectiva !== null
|
|
)
|
|
|
|
// Verificar notas de fragancia/aroma (al menos una categoría)
|
|
const fraganciaAromaCompleta = muestra.fraganciaAromaNotas.categorias.length > 0
|
|
|
|
// Verificar notas de sabor (al menos una categoría)
|
|
const saborCompleto = muestra.saborNotas.categorias.length > 0
|
|
|
|
// Verificar gustos predominantes (mínimo 1, máximo 2)
|
|
const gustosCompletos = muestra.gustosPredominantes.length >= 1 && muestra.gustosPredominantes.length <= 2
|
|
|
|
return intensidadesCompletas && fraganciaAromaCompleta && saborCompleto && gustosCompletos
|
|
}
|
|
|
|
/**
|
|
* Obtiene el porcentaje de completitud de una muestra
|
|
*/
|
|
const porcentajeCompletitud = (muestra: Muestra): number => {
|
|
let total = 0
|
|
let completados = 0
|
|
|
|
// Intensidades afectivas (8 campos)
|
|
total += 8
|
|
completados += Object.values(muestra.intensidades).filter(
|
|
(intensidad) => intensidad.afectiva !== null
|
|
).length
|
|
|
|
// Intensidades descriptivas (8 campos)
|
|
total += 8
|
|
completados += Object.values(muestra.intensidades).filter(
|
|
(intensidad) => intensidad.descriptiva !== null
|
|
).length
|
|
|
|
// Notas fragancia/aroma (al menos una categoría)
|
|
total += 1
|
|
if (muestra.fraganciaAromaNotas.categorias.length > 0) completados += 1
|
|
|
|
// Notas sabor (al menos una categoría)
|
|
total += 1
|
|
if (muestra.saborNotas.categorias.length > 0) completados += 1
|
|
|
|
// Gustos predominantes
|
|
total += 1
|
|
if (muestra.gustosPredominantes.length >= 1) completados += 1
|
|
|
|
return Math.round((completados / total) * 100)
|
|
}
|
|
|
|
/**
|
|
* Elimina la sesión actual
|
|
*/
|
|
const eliminarSesionActual = async () => {
|
|
try {
|
|
await eliminar()
|
|
tabActiva.value = 'organoleptica'
|
|
accordionAbierto.value = []
|
|
} catch (err) {
|
|
console.error('Error al eliminar sesión:', err)
|
|
throw err
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exporta la sesión actual como JSON
|
|
*/
|
|
const exportarSesion = (): string | null => {
|
|
if (!sesionActiva.value) return null
|
|
return JSON.stringify(sesionActiva.value, null, 2)
|
|
}
|
|
|
|
/**
|
|
* Calcula estadísticas generales de la sesión
|
|
*/
|
|
const estadisticasSesion = computed(() => {
|
|
if (!sesionActiva.value) return null
|
|
|
|
const muestras = sesionActiva.value.muestras
|
|
const totalMuestras = muestras.length
|
|
const muestrasCompletas = muestras.filter(esMuestraCompleta).length
|
|
const promedioCompletitud = muestras.reduce((acc, m) => acc + porcentajeCompletitud(m), 0) / totalMuestras
|
|
const puntajePromedio = muestras.reduce((acc, m) => acc + m.puntajeFinal, 0) / totalMuestras
|
|
|
|
return {
|
|
totalMuestras,
|
|
muestrasCompletas,
|
|
porcentajeCompletitud: Math.round(promedioCompletitud),
|
|
puntajePromedio: Math.round(puntajePromedio),
|
|
}
|
|
})
|
|
|
|
return {
|
|
// Estado de la sesión
|
|
sesionActiva,
|
|
cargando,
|
|
error,
|
|
|
|
// Estado de la UI
|
|
tabActiva,
|
|
subcategoriasActivas,
|
|
accordionAbierto,
|
|
actualizarSubcategorias,
|
|
|
|
// Estadísticas
|
|
estadisticasSesion,
|
|
|
|
// Métodos CRUD de sesión
|
|
crearNuevaSesion,
|
|
eliminarSesionActual,
|
|
exportarSesion,
|
|
|
|
// Métodos de actualización de muestras
|
|
actualizarMuestra,
|
|
actualizarIntensidad,
|
|
actualizarNombreMuestra,
|
|
actualizarFraganciaAroma,
|
|
actualizarSabor,
|
|
actualizarTazasNoUniformes,
|
|
actualizarTazasDefectuosas,
|
|
actualizarDefecto,
|
|
actualizarSensacionBoca,
|
|
actualizarGustosPredominantes,
|
|
actualizarOtrasNotas,
|
|
|
|
// Utilidades
|
|
obtenerMuestra,
|
|
esMuestraCompleta,
|
|
porcentajeCompletitud,
|
|
}
|
|
}
|