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
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:
315
nuxt4/app/types/catacion.ts
Normal file
315
nuxt4/app/types/catacion.ts
Normal 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]
|
||||
Reference in New Issue
Block a user