Feat: Implementar UI completa de RioCata - Sistema de catación de café
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m3s

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
This commit is contained in:
2025-10-18 01:39:27 -06:00
parent 801b650891
commit 87fb92d210
12 changed files with 3776 additions and 0 deletions

315
nuxt4/app/types/catacion.ts Normal file
View File

@@ -0,0 +1,315 @@
/**
* Tipos y definiciones para el sistema de catación de café
*/
// ============================================================================
// INTENSIDADES
// ============================================================================
export interface IntensidadValor {
/** Valor descriptivo: qué tan intensa es la característica (1-10) */
descriptiva: number | null
/** Valor afectivo: qué tan buena/mala es la característica (1-15) */
afectiva: number | null
}
export interface Intensidades {
fragancia: IntensidadValor
aroma: IntensidadValor
sabor: IntensidadValor
saborResidual: IntensidadValor
acidez: IntensidadValor
dulzor: IntensidadValor
sensacionBoca: IntensidadValor
impresionGlobal: IntensidadValor
}
// ============================================================================
// FAMILIAS DE NOTAS
// ============================================================================
export type CategoriaNotaPrincipal =
| 'Floral'
| 'Afrutado'
| 'Ácido/Fermentado'
| 'Verde Vegetal'
| 'Otro'
| 'Tostado'
| 'Nueces/Cacao'
| 'Especias'
| 'Dulce'
export interface FamiliasNotas {
Floral: Record<string, never> // Sin subcategorías
Afrutado: {
Bayas: string[]
'Frutas Deshidratadas': string[]
Cítricos: string[]
}
'Ácido/Fermentado': {
Ácido: string[]
Fermentado: string[]
}
'Verde Vegetal': Record<string, never>
Otro: {
Químico: string[]
'Humedad/Tierra': string[]
Madera: string[]
}
Tostado: {
Cereal: string[]
Quemado: string[]
Tabaco: string[]
}
'Nueces/Cacao': {
Nueces: string[]
Cacao: string[]
}
Especias: Record<string, never>
Dulce: {
Vainilla: string[]
'Azúcar Morena': string[]
}
}
export interface NotaSeleccionada {
/** Categoría principal seleccionada */
categoria: CategoriaNotaPrincipal | null
/** Subcategoría seleccionada (si aplica) */
subcategoria: string | null
/** Nota específica seleccionada o escrita libremente */
notaEspecifica: string | null
}
// ============================================================================
// DEFECTOS Y CARACTERÍSTICAS
// ============================================================================
export type TipoDefecto = 'Mohoso' | 'Fenólico' | 'Papa' | null
export type SensacionBoca =
| 'Áspero'
| 'Arenoso'
| 'Rugoso'
| 'Rasposo'
| 'Suave'
| 'Aterciopelado'
| 'Sedoso'
| 'Almibarado'
| 'Aceitoso'
| 'Metálico'
| 'Deja seca la boca'
| 'Astringente'
export type GustoPredominante = 'Salado' | 'Amargo' | 'Ácido' | 'Dulce' | 'Umami'
// ============================================================================
// MUESTRA
// ============================================================================
export interface Muestra {
/** ID único de la muestra */
muestraId: number
/** Nombre o código de la muestra */
nombre: string
/** Valores de intensidad para cada parámetro */
intensidades: Intensidades
/** Notas de fragancia y aroma */
fraganciaAromaNotas: NotaSeleccionada
/** Notas de sabor */
saborNotas: NotaSeleccionada
/** Tazas no uniformes (números 1-5) */
tazasNoUniformes: number[]
/** Tazas defectuosas (números 1-5) */
tazasDefectuosas: number[]
/** Tipo de defecto encontrado */
defecto: TipoDefecto
/** Sensaciones en la boca (múltiples selecciones) */
sensacionEnBoca: SensacionBoca[]
/** Gustos predominantes (máximo 2, mínimo 1) */
gustosPredominantes: GustoPredominante[]
/** Notas adicionales en texto libre */
otrasNotas: string
/** Puntaje final (suma de valores afectivos) */
puntajeFinal: number
}
// ============================================================================
// SESIÓN DE CATACIÓN
// ============================================================================
export interface SesionCatacion {
/** ID único de la sesión */
sessionId: string
/** Fecha de la catación */
fecha: string
/** Nombre del catador */
catador: string
/** Cantidad de muestras en esta sesión */
cantidadMuestras: number
/** Listado de muestras */
muestras: Muestra[]
/** Timestamp de creación */
creadoEn: number
/** Timestamp de última modificación */
modificadoEn: number
}
// ============================================================================
// HELPERS Y UTILIDADES
// ============================================================================
/**
* Crea una muestra vacía con valores por defecto
*/
export function crearMuestraVacia(id: number): Muestra {
return {
muestraId: id,
nombre: `Muestra ${id}`,
intensidades: {
fragancia: { descriptiva: null, afectiva: null },
aroma: { descriptiva: null, afectiva: null },
sabor: { descriptiva: null, afectiva: null },
saborResidual: { descriptiva: null, afectiva: null },
acidez: { descriptiva: null, afectiva: null },
dulzor: { descriptiva: null, afectiva: null },
sensacionBoca: { descriptiva: null, afectiva: null },
impresionGlobal: { descriptiva: null, afectiva: null },
},
fraganciaAromaNotas: {
categoria: null,
subcategoria: null,
notaEspecifica: null,
},
saborNotas: {
categoria: null,
subcategoria: null,
notaEspecifica: null,
},
tazasNoUniformes: [],
tazasDefectuosas: [],
defecto: null,
sensacionEnBoca: [],
gustosPredominantes: [],
otrasNotas: '',
puntajeFinal: 0,
}
}
/**
* Calcula el puntaje final sumando todos los valores afectivos
*/
export function calcularPuntajeFinal(muestra: Muestra): number {
const { intensidades } = muestra
let total = 0
// Sumar todos los valores afectivos que no sean null
Object.values(intensidades).forEach((intensidad) => {
if (intensidad.afectiva !== null) {
total += intensidad.afectiva
}
})
return total
}
/**
* Crea una sesión de catación vacía
*/
export function crearSesionVacia(
catador: string,
cantidadMuestras: number
): SesionCatacion {
const sessionId = `session-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
const ahora = Date.now()
const fecha = new Date().toISOString().split('T')[0] || '' // YYYY-MM-DD
const muestras: Muestra[] = []
for (let i = 1; i <= cantidadMuestras; i++) {
muestras.push(crearMuestraVacia(i))
}
return {
sessionId,
fecha,
catador,
cantidadMuestras,
muestras,
creadoEn: ahora,
modificadoEn: ahora,
}
}
// ============================================================================
// CONSTANTES
// ============================================================================
/**
* Estructura jerárquica completa de familias de notas
*/
export const FAMILIAS_NOTAS_ESTRUCTURA: FamiliasNotas = {
Floral: {},
Afrutado: {
Bayas: [],
'Frutas Deshidratadas': [],
Cítricos: [],
},
'Ácido/Fermentado': {
Ácido: [],
Fermentado: [],
},
'Verde Vegetal': {},
Otro: {
Químico: [],
'Humedad/Tierra': [],
Madera: [],
},
Tostado: {
Cereal: [],
Quemado: [],
Tabaco: [],
},
'Nueces/Cacao': {
Nueces: [],
Cacao: [],
},
Especias: {},
Dulce: {
Vainilla: [],
'Azúcar Morena': [],
},
}
/**
* Lista de todas las sensaciones en boca disponibles
*/
export const SENSACIONES_BOCA: SensacionBoca[] = [
'Áspero',
'Arenoso',
'Rugoso',
'Rasposo',
'Suave',
'Aterciopelado',
'Sedoso',
'Almibarado',
'Aceitoso',
'Metálico',
'Deja seca la boca',
'Astringente',
]
/**
* Lista de todos los gustos predominantes disponibles
*/
export const GUSTOS_PREDOMINANTES: GustoPredominante[] = [
'Salado',
'Amargo',
'Ácido',
'Dulce',
'Umami',
]
/**
* Lista de tipos de defectos disponibles
*/
export const TIPOS_DEFECTOS: TipoDefecto[] = ['Mohoso', 'Fenólico', 'Papa', null]