Files
cataRio/nuxt4/app/components/cata/FormularioMuestra.vue
josedario87 f6dc2c3bce
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m8s
Feat: Implementar asignación rápida de puntajes con botón de nube cáustica
- Crear ModalAsignacionRapida.vue con lógica de distribución por múltiplos de 8
  - Input numérico (8-80) para puntaje total deseado
  - Cálculo automático del múltiplo de 8 más cercano
  - Si múltiplo < valor: permite seleccionar categorías que sobresalen (+1)
  - Si múltiplo > valor: permite seleccionar categorías que palidecen (-1)
  - Si múltiplo = valor: asignación directa sin ajustes

- Crear BotonNubeCaustica.vue con diseño especial
  - Forma de nube usando SVG path
  - Animación de patrones de luz cáustica con gradientes animados
  - Efectos de brillo y ondas al hacer hover
  - Icono de rayo mágico con animación sparkle

- Integrar funcionalidad en FormularioMuestra.vue
  - Ubicar botón en esquina superior izquierda de sección Descriptiva/Afectiva
  - Aplicar puntajes calculados a todas las categorías descriptivas
  - Actualizar puntaje final automáticamente

- Corregir comentario: "Suma de valores descriptivos" (era "afectivos")
2025-10-19 00:56:20 -06:00

1215 lines
47 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="formulario-muestra p-4 space-y-6">
<!-- Tab 1: Organoléptica (solo selectores de familia) -->
<div v-if="tabActiva === 'organoleptica'" class="tab-content cata-fade-in">
<!-- Selector de Familia de Fragancia/Aroma -->
<div v-if="mostrarFraganciaAroma" class="form-section">
<CataSelectorFamilia
tipo="fragancia-aroma"
label="Familia de Fragancia y Aroma"
:model-value="muestra.fraganciaAromaNotas"
@update:model-value="actualizarFraganciaAroma"
/>
</div>
<!-- Selector de Familia de Sabor -->
<div v-if="mostrarSaborOrganoleptica" class="form-section">
<CataSelectorFamilia
tipo="sabor"
label="Familia de Sabor"
:model-value="muestra.saborNotas"
@update:model-value="actualizarSabor"
/>
</div>
<!-- Sensaciones en Boca (selección única) -->
<div v-if="mostrarSensacionBocaOrganoleptica" class="form-section">
<label class="block text-sm font-medium mb-2 cata-text">
Sensación en la Boca
</label>
<div class="sensaciones-grid">
<button
v-for="sensacion in sensacionesBoca"
:key="sensacion"
type="button"
:class="[
'sensacion-item',
'cata-checkbox',
{ 'cata-checkbox-checked': muestra.sensacionEnBoca === sensacion },
]"
@click="seleccionarSensacionBoca(sensacion)"
>
<span class="sensacion-text cata-text">{{ sensacion }}</span>
</button>
</div>
</div>
<!-- Gustos Predominantes (máx 2) -->
<div v-if="mostrarGustosPredominantes" class="form-section">
<label class="block text-sm font-medium mb-2 cata-text">
Gustos Predominantes (mín 1, máx 2)
</label>
<div class="gustos-grid">
<button
v-for="gusto in gustosPredominantes"
:key="gusto"
type="button"
:class="[
'gusto-item',
'cata-checkbox',
{ 'cata-checkbox-checked': muestra.gustosPredominantes.includes(gusto) },
]"
:disabled="!muestra.gustosPredominantes.includes(gusto) && muestra.gustosPredominantes.length >= 2"
@click="toggleGustoPredominante(gusto)"
>
<span class="gusto-text cata-text">{{ gusto }}</span>
</button>
</div>
</div>
</div>
<!-- Tab 2: Descriptiva/Afectiva (todos los sliders incluyendo impresión global) -->
<div v-if="tabActiva === 'descriptiva-afectiva'" class="tab-content cata-fade-in">
<!-- Botón de asignación rápida -->
<div class="flex justify-start mb-4">
<CataBotonNubeCaustica @click="modalAsignacionRapida = true" />
</div>
<!-- Sliders de Fragancia -->
<div v-if="mostrarFraganciaSlider" class="form-section">
<div class="flex items-center justify-between mb-2">
<h5 class="form-section-title cata-text flex items-center gap-2" :style="{ color: getCategoryColor('fragancia') }">
<UIcon :name="getCategoryIcon('fragancia')" class="w-5 h-5" />
Fragancia
</h5>
<div class="flex gap-2">
<UBadge :style="{ backgroundColor: getCategoryColor('fragancia'), opacity: 0.4 }" class="text-xs">
📊 {{ muestra.intensidades.fragancia.descriptiva ?? '-' }}
</UBadge>
<UBadge :style="{ backgroundColor: getCategoryColor('fragancia'), opacity: 0.7 }" class="text-xs">
{{ muestra.intensidades.fragancia.afectiva ?? '-' }}
</UBadge>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<CataSelectorIntensidad
v-if="mostrarFraganciaDescriptiva"
tipo="descriptiva"
:model-value="muestra.intensidades.fragancia.descriptiva"
:color="getCategoryColor('fragancia')"
@update:model-value="(v) => actualizarIntensidad('fragancia', 'descriptiva', v)"
/>
<CataSelectorIntensidad
v-if="mostrarFraganciaAfectiva"
tipo="afectiva"
:model-value="muestra.intensidades.fragancia.afectiva"
:color="getCategoryColor('fragancia')"
@update:model-value="(v) => actualizarIntensidad('fragancia', 'afectiva', v)"
/>
</div>
</div>
<!-- Sliders de Aroma -->
<div v-if="mostrarAromaSlider" class="form-section">
<div class="flex items-center justify-between mb-2">
<h5 class="form-section-title cata-text flex items-center gap-2" :style="{ color: getCategoryColor('aroma') }">
<UIcon :name="getCategoryIcon('aroma')" class="w-5 h-5" />
Aroma
</h5>
<div class="flex gap-2">
<UBadge :style="{ backgroundColor: getCategoryColor('aroma'), opacity: 0.4 }" class="text-xs">
📊 {{ muestra.intensidades.aroma.descriptiva ?? '-' }}
</UBadge>
<UBadge :style="{ backgroundColor: getCategoryColor('aroma'), opacity: 0.7 }" class="text-xs">
❤️ {{ muestra.intensidades.aroma.afectiva ?? '-' }}
</UBadge>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<CataSelectorIntensidad
v-if="mostrarAromaDescriptiva"
tipo="descriptiva"
:model-value="muestra.intensidades.aroma.descriptiva"
:color="getCategoryColor('aroma')"
@update:model-value="(v) => actualizarIntensidad('aroma', 'descriptiva', v)"
/>
<CataSelectorIntensidad
v-if="mostrarAromaAfectiva"
tipo="afectiva"
:model-value="muestra.intensidades.aroma.afectiva"
:color="getCategoryColor('aroma')"
@update:model-value="(v) => actualizarIntensidad('aroma', 'afectiva', v)"
/>
</div>
</div>
<!-- Sliders de Sabor -->
<div v-if="mostrarSaborSlider" class="form-section">
<div class="flex items-center justify-between mb-2">
<h5 class="form-section-title cata-text flex items-center gap-2" :style="{ color: getCategoryColor('sabor') }">
<UIcon :name="getCategoryIcon('sabor')" class="w-5 h-5" />
Sabor
</h5>
<div class="flex gap-2">
<UBadge :style="{ backgroundColor: getCategoryColor('sabor'), opacity: 0.4 }" class="text-xs">
📊 {{ muestra.intensidades.sabor.descriptiva ?? '-' }}
</UBadge>
<UBadge :style="{ backgroundColor: getCategoryColor('sabor'), opacity: 0.7 }" class="text-xs">
❤️ {{ muestra.intensidades.sabor.afectiva ?? '-' }}
</UBadge>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<CataSelectorIntensidad
v-if="mostrarSaborDescriptiva"
tipo="descriptiva"
:model-value="muestra.intensidades.sabor.descriptiva"
:color="getCategoryColor('sabor')"
@update:model-value="(v) => actualizarIntensidad('sabor', 'descriptiva', v)"
/>
<CataSelectorIntensidad
v-if="mostrarSaborAfectiva"
tipo="afectiva"
:model-value="muestra.intensidades.sabor.afectiva"
:color="getCategoryColor('sabor')"
@update:model-value="(v) => actualizarIntensidad('sabor', 'afectiva', v)"
/>
</div>
</div>
<!-- Sliders de Sabor Residual -->
<div v-if="mostrarSaborResidualSlider" class="form-section">
<div class="flex items-center justify-between mb-2">
<h5 class="form-section-title cata-text flex items-center gap-2" :style="{ color: getCategoryColor('saborResidual') }">
<UIcon :name="getCategoryIcon('saborResidual')" class="w-5 h-5" />
Sabor Residual
</h5>
<div class="flex gap-2">
<UBadge :style="{ backgroundColor: getCategoryColor('saborResidual'), opacity: 0.4 }" class="text-xs">
📊 {{ muestra.intensidades.saborResidual.descriptiva ?? '-' }}
</UBadge>
<UBadge :style="{ backgroundColor: getCategoryColor('saborResidual'), opacity: 0.7 }" class="text-xs">
❤️ {{ muestra.intensidades.saborResidual.afectiva ?? '-' }}
</UBadge>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<CataSelectorIntensidad
v-if="mostrarSaborResidualDescriptiva"
tipo="descriptiva"
:model-value="muestra.intensidades.saborResidual.descriptiva"
:color="getCategoryColor('saborResidual')"
@update:model-value="(v) => actualizarIntensidad('saborResidual', 'descriptiva', v)"
/>
<CataSelectorIntensidad
v-if="mostrarSaborResidualAfectiva"
tipo="afectiva"
:model-value="muestra.intensidades.saborResidual.afectiva"
:color="getCategoryColor('saborResidual')"
@update:model-value="(v) => actualizarIntensidad('saborResidual', 'afectiva', v)"
/>
</div>
</div>
<!-- Sliders de Acidez -->
<div v-if="mostrarAcidezSlider" class="form-section">
<div class="flex items-center justify-between mb-2">
<h5 class="form-section-title cata-text flex items-center gap-2" :style="{ color: getCategoryColor('acidez') }">
<UIcon :name="getCategoryIcon('acidez')" class="w-5 h-5" />
Acidez
</h5>
<div class="flex gap-2">
<UBadge :style="{ backgroundColor: getCategoryColor('acidez'), opacity: 0.4 }" class="text-xs">
📊 {{ muestra.intensidades.acidez.descriptiva ?? '-' }}
</UBadge>
<UBadge :style="{ backgroundColor: getCategoryColor('acidez'), opacity: 0.7 }" class="text-xs">
❤️ {{ muestra.intensidades.acidez.afectiva ?? '-' }}
</UBadge>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<CataSelectorIntensidad
v-if="mostrarAcidezDescriptiva"
tipo="descriptiva"
:model-value="muestra.intensidades.acidez.descriptiva"
:color="getCategoryColor('acidez')"
@update:model-value="(v) => actualizarIntensidad('acidez', 'descriptiva', v)"
/>
<CataSelectorIntensidad
v-if="mostrarAcidezAfectiva"
tipo="afectiva"
:model-value="muestra.intensidades.acidez.afectiva"
:color="getCategoryColor('acidez')"
@update:model-value="(v) => actualizarIntensidad('acidez', 'afectiva', v)"
/>
</div>
</div>
<!-- Sliders de Dulzor -->
<div v-if="mostrarDulzorSlider" class="form-section">
<div class="flex items-center justify-between mb-2">
<h5 class="form-section-title cata-text flex items-center gap-2" :style="{ color: getCategoryColor('dulzor') }">
<UIcon :name="getCategoryIcon('dulzor')" class="w-5 h-5" />
Dulzor
</h5>
<div class="flex gap-2">
<UBadge :style="{ backgroundColor: getCategoryColor('dulzor'), opacity: 0.4 }" class="text-xs">
📊 {{ muestra.intensidades.dulzor.descriptiva ?? '-' }}
</UBadge>
<UBadge :style="{ backgroundColor: getCategoryColor('dulzor'), opacity: 0.7 }" class="text-xs">
❤️ {{ muestra.intensidades.dulzor.afectiva ?? '-' }}
</UBadge>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<CataSelectorIntensidad
v-if="mostrarDulzorDescriptiva"
tipo="descriptiva"
:model-value="muestra.intensidades.dulzor.descriptiva"
:color="getCategoryColor('dulzor')"
@update:model-value="(v) => actualizarIntensidad('dulzor', 'descriptiva', v)"
/>
<CataSelectorIntensidad
v-if="mostrarDulzorAfectiva"
tipo="afectiva"
:model-value="muestra.intensidades.dulzor.afectiva"
:color="getCategoryColor('dulzor')"
@update:model-value="(v) => actualizarIntensidad('dulzor', 'afectiva', v)"
/>
</div>
</div>
<!-- Sliders de Sensación en Boca -->
<div v-if="mostrarSensacionBocaSlider" class="form-section">
<div class="flex items-center justify-between mb-2">
<h5 class="form-section-title cata-text flex items-center gap-2" :style="{ color: getCategoryColor('sensacionBoca') }">
<UIcon :name="getCategoryIcon('sensacionBoca')" class="w-5 h-5" />
Sensación en la Boca
</h5>
<div class="flex gap-2">
<UBadge :style="{ backgroundColor: getCategoryColor('sensacionBoca'), opacity: 0.4 }" class="text-xs">
📊 {{ muestra.intensidades.sensacionBoca.descriptiva ?? '-' }}
</UBadge>
<UBadge :style="{ backgroundColor: getCategoryColor('sensacionBoca'), opacity: 0.7 }" class="text-xs">
❤️ {{ muestra.intensidades.sensacionBoca.afectiva ?? '-' }}
</UBadge>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<CataSelectorIntensidad
v-if="mostrarSensacionBocaDescriptiva"
tipo="descriptiva"
:model-value="muestra.intensidades.sensacionBoca.descriptiva"
:color="getCategoryColor('sensacionBoca')"
@update:model-value="(v) => actualizarIntensidad('sensacionBoca', 'descriptiva', v)"
/>
<CataSelectorIntensidad
v-if="mostrarSensacionBocaAfectiva"
tipo="afectiva"
:model-value="muestra.intensidades.sensacionBoca.afectiva"
:color="getCategoryColor('sensacionBoca')"
@update:model-value="(v) => actualizarIntensidad('sensacionBoca', 'afectiva', v)"
/>
</div>
</div>
<!-- Sliders de Impresión Global -->
<div v-if="mostrarImpresionGlobalSlider" class="form-section">
<div class="flex items-center justify-between mb-2">
<h5 class="form-section-title cata-text flex items-center gap-2" :style="{ color: getCategoryColor('impresionGlobal') }">
<UIcon :name="getCategoryIcon('impresionGlobal')" class="w-5 h-5" />
Impresión Global
</h5>
<div class="flex gap-2">
<UBadge :style="{ backgroundColor: getCategoryColor('impresionGlobal'), opacity: 0.4 }" class="text-xs">
📊 {{ muestra.intensidades.impresionGlobal.descriptiva ?? '-' }}
</UBadge>
<UBadge :style="{ backgroundColor: getCategoryColor('impresionGlobal'), opacity: 0.7 }" class="text-xs">
❤️ {{ muestra.intensidades.impresionGlobal.afectiva ?? '-' }}
</UBadge>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<CataSelectorIntensidad
v-if="mostrarImpresionGlobalDescriptiva"
tipo="descriptiva"
:model-value="muestra.intensidades.impresionGlobal.descriptiva"
:color="getCategoryColor('impresionGlobal')"
@update:model-value="(v) => actualizarIntensidad('impresionGlobal', 'descriptiva', v)"
/>
<CataSelectorIntensidad
v-if="mostrarImpresionGlobalAfectiva"
tipo="afectiva"
:model-value="muestra.intensidades.impresionGlobal.afectiva"
:color="getCategoryColor('impresionGlobal')"
@update:model-value="(v) => actualizarIntensidad('impresionGlobal', 'afectiva', v)"
/>
</div>
</div>
</div>
<!-- Tab 3: Defectos (tazas y defectos) -->
<div v-if="tabActiva === 'defectos'" class="tab-content cata-fade-in">
<!-- Tazas No Uniformes -->
<div class="form-section">
<CataSelectorTazas
tipo="uniformes"
label="Tazas NO Uniformes"
:model-value="muestra.tazasNoUniformes"
@update:model-value="actualizarTazasNoUniformes"
/>
</div>
<!-- Tazas Defectuosas -->
<div class="form-section">
<CataSelectorTazas
tipo="defectuosas"
label="Tazas Defectuosas"
:model-value="muestra.tazasDefectuosas"
@update:model-value="actualizarTazasDefectuosas"
/>
</div>
<!-- Tipo de Defecto -->
<div v-if="muestra.tazasDefectuosas.length > 0" class="form-section">
<label class="block text-sm font-medium mb-2 cata-text">
Tipo de Defecto
</label>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2">
<button
v-for="tipo in tiposDefectos"
:key="tipo || 'ninguno'"
type="button"
:class="[
'cata-checkbox',
{ 'cata-checkbox-checked': muestra.defecto === tipo },
]"
@click="actualizarDefecto(tipo)"
>
<span class="cata-text">{{ tipo || 'Ninguno' }}</span>
</button>
</div>
</div>
</div>
<!-- Tab 4: Impresión Global (muestra TODOS los componentes) -->
<div v-if="tabActiva === 'impresion-global'" class="tab-content cata-fade-in">
<h4 class="tab-section-title cata-text mb-4">
Visión Global Completa
</h4>
<!-- Sección: Organoléptica -->
<div class="global-section mb-6 p-4 cata-outline-box rounded-lg">
<h5 class="global-section-title cata-text mb-4">Características Organolépticas</h5>
<!-- Selector de Familia de Fragancia/Aroma -->
<div class="form-section mb-4">
<CataSelectorFamilia
tipo="fragancia-aroma"
label="Familia de Fragancia y Aroma"
:model-value="muestra.fraganciaAromaNotas"
@update:model-value="actualizarFraganciaAroma"
/>
</div>
<!-- Selector de Familia de Sabor -->
<div class="form-section mb-4">
<CataSelectorFamilia
tipo="sabor"
label="Familia de Sabor"
:model-value="muestra.saborNotas"
@update:model-value="actualizarSabor"
/>
</div>
<!-- Sensaciones en Boca (selección única) -->
<div class="form-section mb-4">
<label class="block text-sm font-medium mb-2 cata-text">
Sensación en la Boca
</label>
<div class="sensaciones-grid">
<button
v-for="sensacion in sensacionesBoca"
:key="sensacion"
type="button"
:class="[
'sensacion-item',
'cata-checkbox',
{ 'cata-checkbox-checked': muestra.sensacionEnBoca === sensacion },
]"
@click="seleccionarSensacionBoca(sensacion)"
>
<span class="sensacion-text cata-text">{{ sensacion }}</span>
</button>
</div>
</div>
<!-- Gustos Predominantes (máx 2) -->
<div class="form-section">
<label class="block text-sm font-medium mb-2 cata-text">
Gustos Predominantes (mín 1, máx 2)
</label>
<div class="gustos-grid">
<button
v-for="gusto in gustosPredominantes"
:key="gusto"
type="button"
:class="[
'gusto-item',
'cata-checkbox',
{ 'cata-checkbox-checked': muestra.gustosPredominantes.includes(gusto) },
]"
:disabled="!muestra.gustosPredominantes.includes(gusto) && muestra.gustosPredominantes.length >= 2"
@click="toggleGustoPredominante(gusto)"
>
<span class="gusto-text cata-text">{{ gusto }}</span>
</button>
</div>
</div>
</div>
<!-- Sección: Intensidades -->
<div class="global-section mb-6 p-4 cata-outline-box rounded-lg">
<h5 class="global-section-title cata-text mb-4">Intensidades Descriptivas y Afectivas</h5>
<!-- Fragancia -->
<div class="form-section mb-4">
<div class="flex items-center justify-between mb-2">
<h6 class="form-subsection-title cata-text flex items-center gap-2" :style="{ color: getCategoryColor('fragancia') }">
<UIcon :name="getCategoryIcon('fragancia')" class="w-5 h-5" />
Fragancia
</h6>
<div class="flex gap-2">
<UBadge :style="{ backgroundColor: getCategoryColor('fragancia'), opacity: 0.4 }" class="text-xs">
📊 {{ muestra.intensidades.fragancia.descriptiva ?? '-' }}
</UBadge>
<UBadge :style="{ backgroundColor: getCategoryColor('fragancia'), opacity: 0.7 }" class="text-xs">
❤️ {{ muestra.intensidades.fragancia.afectiva ?? '-' }}
</UBadge>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<CataSelectorIntensidad
tipo="descriptiva"
:model-value="muestra.intensidades.fragancia.descriptiva"
:color="getCategoryColor('fragancia')"
@update:model-value="(v) => actualizarIntensidad('fragancia', 'descriptiva', v)"
/>
<CataSelectorIntensidad
tipo="afectiva"
:model-value="muestra.intensidades.fragancia.afectiva"
:color="getCategoryColor('fragancia')"
@update:model-value="(v) => actualizarIntensidad('fragancia', 'afectiva', v)"
/>
</div>
</div>
<!-- Aroma -->
<div class="form-section mb-4">
<div class="flex items-center justify-between mb-2">
<h6 class="form-subsection-title cata-text flex items-center gap-2" :style="{ color: getCategoryColor('aroma') }">
<UIcon :name="getCategoryIcon('aroma')" class="w-5 h-5" />
Aroma
</h6>
<div class="flex gap-2">
<UBadge :style="{ backgroundColor: getCategoryColor('aroma'), opacity: 0.4 }" class="text-xs">
📊 {{ muestra.intensidades.aroma.descriptiva ?? '-' }}
</UBadge>
<UBadge :style="{ backgroundColor: getCategoryColor('aroma'), opacity: 0.7 }" class="text-xs">
❤️ {{ muestra.intensidades.aroma.afectiva ?? '-' }}
</UBadge>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<CataSelectorIntensidad
tipo="descriptiva"
:model-value="muestra.intensidades.aroma.descriptiva"
:color="getCategoryColor('aroma')"
@update:model-value="(v) => actualizarIntensidad('aroma', 'descriptiva', v)"
/>
<CataSelectorIntensidad
tipo="afectiva"
:model-value="muestra.intensidades.aroma.afectiva"
:color="getCategoryColor('aroma')"
@update:model-value="(v) => actualizarIntensidad('aroma', 'afectiva', v)"
/>
</div>
</div>
<!-- Sabor -->
<div class="form-section mb-4">
<div class="flex items-center justify-between mb-2">
<h6 class="form-subsection-title cata-text flex items-center gap-2" :style="{ color: getCategoryColor('sabor') }">
<UIcon :name="getCategoryIcon('sabor')" class="w-5 h-5" />
Sabor
</h6>
<div class="flex gap-2">
<UBadge :style="{ backgroundColor: getCategoryColor('sabor'), opacity: 0.4 }" class="text-xs">
📊 {{ muestra.intensidades.sabor.descriptiva ?? '-' }}
</UBadge>
<UBadge :style="{ backgroundColor: getCategoryColor('sabor'), opacity: 0.7 }" class="text-xs">
❤️ {{ muestra.intensidades.sabor.afectiva ?? '-' }}
</UBadge>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<CataSelectorIntensidad
tipo="descriptiva"
:model-value="muestra.intensidades.sabor.descriptiva"
:color="getCategoryColor('sabor')"
@update:model-value="(v) => actualizarIntensidad('sabor', 'descriptiva', v)"
/>
<CataSelectorIntensidad
tipo="afectiva"
:model-value="muestra.intensidades.sabor.afectiva"
:color="getCategoryColor('sabor')"
@update:model-value="(v) => actualizarIntensidad('sabor', 'afectiva', v)"
/>
</div>
</div>
<!-- Sabor Residual -->
<div class="form-section mb-4">
<div class="flex items-center justify-between mb-2">
<h6 class="form-subsection-title cata-text flex items-center gap-2" :style="{ color: getCategoryColor('saborResidual') }">
<UIcon :name="getCategoryIcon('saborResidual')" class="w-5 h-5" />
Sabor Residual
</h6>
<div class="flex gap-2">
<UBadge :style="{ backgroundColor: getCategoryColor('saborResidual'), opacity: 0.4 }" class="text-xs">
📊 {{ muestra.intensidades.saborResidual.descriptiva ?? '-' }}
</UBadge>
<UBadge :style="{ backgroundColor: getCategoryColor('saborResidual'), opacity: 0.7 }" class="text-xs">
❤️ {{ muestra.intensidades.saborResidual.afectiva ?? '-' }}
</UBadge>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<CataSelectorIntensidad
tipo="descriptiva"
:model-value="muestra.intensidades.saborResidual.descriptiva"
:color="getCategoryColor('saborResidual')"
@update:model-value="(v) => actualizarIntensidad('saborResidual', 'descriptiva', v)"
/>
<CataSelectorIntensidad
tipo="afectiva"
:model-value="muestra.intensidades.saborResidual.afectiva"
:color="getCategoryColor('saborResidual')"
@update:model-value="(v) => actualizarIntensidad('saborResidual', 'afectiva', v)"
/>
</div>
</div>
<!-- Acidez -->
<div class="form-section mb-4">
<div class="flex items-center justify-between mb-2">
<h6 class="form-subsection-title cata-text flex items-center gap-2" :style="{ color: getCategoryColor('acidez') }">
<UIcon :name="getCategoryIcon('acidez')" class="w-5 h-5" />
Acidez
</h6>
<div class="flex gap-2">
<UBadge :style="{ backgroundColor: getCategoryColor('acidez'), opacity: 0.4 }" class="text-xs">
📊 {{ muestra.intensidades.acidez.descriptiva ?? '-' }}
</UBadge>
<UBadge :style="{ backgroundColor: getCategoryColor('acidez'), opacity: 0.7 }" class="text-xs">
❤️ {{ muestra.intensidades.acidez.afectiva ?? '-' }}
</UBadge>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<CataSelectorIntensidad
tipo="descriptiva"
:model-value="muestra.intensidades.acidez.descriptiva"
:color="getCategoryColor('acidez')"
@update:model-value="(v) => actualizarIntensidad('acidez', 'descriptiva', v)"
/>
<CataSelectorIntensidad
tipo="afectiva"
:model-value="muestra.intensidades.acidez.afectiva"
:color="getCategoryColor('acidez')"
@update:model-value="(v) => actualizarIntensidad('acidez', 'afectiva', v)"
/>
</div>
</div>
<!-- Dulzor -->
<div class="form-section mb-4">
<div class="flex items-center justify-between mb-2">
<h6 class="form-subsection-title cata-text flex items-center gap-2" :style="{ color: getCategoryColor('dulzor') }">
<UIcon :name="getCategoryIcon('dulzor')" class="w-5 h-5" />
Dulzor
</h6>
<div class="flex gap-2">
<UBadge :style="{ backgroundColor: getCategoryColor('dulzor'), opacity: 0.4 }" class="text-xs">
📊 {{ muestra.intensidades.dulzor.descriptiva ?? '-' }}
</UBadge>
<UBadge :style="{ backgroundColor: getCategoryColor('dulzor'), opacity: 0.7 }" class="text-xs">
❤️ {{ muestra.intensidades.dulzor.afectiva ?? '-' }}
</UBadge>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<CataSelectorIntensidad
tipo="descriptiva"
:model-value="muestra.intensidades.dulzor.descriptiva"
:color="getCategoryColor('dulzor')"
@update:model-value="(v) => actualizarIntensidad('dulzor', 'descriptiva', v)"
/>
<CataSelectorIntensidad
tipo="afectiva"
:model-value="muestra.intensidades.dulzor.afectiva"
:color="getCategoryColor('dulzor')"
@update:model-value="(v) => actualizarIntensidad('dulzor', 'afectiva', v)"
/>
</div>
</div>
<!-- Sensación en Boca -->
<div class="form-section mb-4">
<div class="flex items-center justify-between mb-2">
<h6 class="form-subsection-title cata-text flex items-center gap-2" :style="{ color: getCategoryColor('sensacionBoca') }">
<UIcon :name="getCategoryIcon('sensacionBoca')" class="w-5 h-5" />
Sensación en la Boca
</h6>
<div class="flex gap-2">
<UBadge :style="{ backgroundColor: getCategoryColor('sensacionBoca'), opacity: 0.4 }" class="text-xs">
📊 {{ muestra.intensidades.sensacionBoca.descriptiva ?? '-' }}
</UBadge>
<UBadge :style="{ backgroundColor: getCategoryColor('sensacionBoca'), opacity: 0.7 }" class="text-xs">
❤️ {{ muestra.intensidades.sensacionBoca.afectiva ?? '-' }}
</UBadge>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<CataSelectorIntensidad
tipo="descriptiva"
:model-value="muestra.intensidades.sensacionBoca.descriptiva"
:color="getCategoryColor('sensacionBoca')"
@update:model-value="(v) => actualizarIntensidad('sensacionBoca', 'descriptiva', v)"
/>
<CataSelectorIntensidad
tipo="afectiva"
:model-value="muestra.intensidades.sensacionBoca.afectiva"
:color="getCategoryColor('sensacionBoca')"
@update:model-value="(v) => actualizarIntensidad('sensacionBoca', 'afectiva', v)"
/>
</div>
</div>
<!-- Impresión Global -->
<div class="form-section">
<div class="flex items-center justify-between mb-2">
<h6 class="form-subsection-title cata-text flex items-center gap-2" :style="{ color: getCategoryColor('impresionGlobal') }">
<UIcon :name="getCategoryIcon('impresionGlobal')" class="w-5 h-5" />
Impresión Global
</h6>
<div class="flex gap-2">
<UBadge :style="{ backgroundColor: getCategoryColor('impresionGlobal'), opacity: 0.4 }" class="text-xs">
📊 {{ muestra.intensidades.impresionGlobal.descriptiva ?? '-' }}
</UBadge>
<UBadge :style="{ backgroundColor: getCategoryColor('impresionGlobal'), opacity: 0.7 }" class="text-xs">
❤️ {{ muestra.intensidades.impresionGlobal.afectiva ?? '-' }}
</UBadge>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<CataSelectorIntensidad
tipo="descriptiva"
:model-value="muestra.intensidades.impresionGlobal.descriptiva"
:color="getCategoryColor('impresionGlobal')"
@update:model-value="(v) => actualizarIntensidad('impresionGlobal', 'descriptiva', v)"
/>
<CataSelectorIntensidad
tipo="afectiva"
:model-value="muestra.intensidades.impresionGlobal.afectiva"
:color="getCategoryColor('impresionGlobal')"
@update:model-value="(v) => actualizarIntensidad('impresionGlobal', 'afectiva', v)"
/>
</div>
</div>
</div>
<!-- Sección: Defectos -->
<div class="global-section mb-6 p-4 cata-outline-box rounded-lg">
<h5 class="global-section-title cata-text mb-4">Defectos y Uniformidad</h5>
<!-- Tazas No Uniformes -->
<div class="form-section mb-4">
<CataSelectorTazas
tipo="uniformes"
label="Tazas NO Uniformes"
:model-value="muestra.tazasNoUniformes"
@update:model-value="actualizarTazasNoUniformes"
/>
</div>
<!-- Tazas Defectuosas -->
<div class="form-section mb-4">
<CataSelectorTazas
tipo="defectuosas"
label="Tazas Defectuosas"
:model-value="muestra.tazasDefectuosas"
@update:model-value="actualizarTazasDefectuosas"
/>
</div>
<!-- Tipo de Defecto -->
<div v-if="muestra.tazasDefectuosas.length > 0" class="form-section">
<label class="block text-sm font-medium mb-2 cata-text">
Tipo de Defecto
</label>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2">
<button
v-for="tipo in tiposDefectos"
:key="tipo || 'ninguno'"
type="button"
:class="[
'cata-checkbox',
{ 'cata-checkbox-checked': muestra.defecto === tipo },
]"
@click="actualizarDefecto(tipo)"
>
<span class="cata-text">{{ tipo || 'Ninguno' }}</span>
</button>
</div>
</div>
</div>
<!-- Sección: Detalles Adicionales -->
<div class="global-section mb-6 p-4 cata-outline-box rounded-lg">
<h5 class="global-section-title cata-text mb-4">Notas Adicionales</h5>
<!-- Otras Notas -->
<div class="form-section">
<label class="block text-sm font-medium mb-2 cata-text">
Otras Notas
</label>
<textarea
v-model="otrasNotasLocal"
class="cata-input w-full min-h-[100px] resize-y"
placeholder="Notas adicionales sobre el café, cuerpo, balance, etc..."
@blur="actualizarOtrasNotas"
/>
</div>
</div>
<!-- Puntaje Final (solo lectura) -->
<div class="global-section p-4 cata-outline-box rounded-lg">
<div class="puntaje-final">
<div class="flex items-baseline justify-between">
<span class="text-sm cata-text opacity-75">Puntaje Final:</span>
<span class="text-3xl font-bold cata-text">{{ muestra.puntajeFinal }}</span>
</div>
<p class="text-xs cata-text opacity-60 mt-1">
Suma de valores descriptivos
</p>
</div>
</div>
</div>
<!-- Modal de Asignación Rápida -->
<CataModalAsignacionRapida
v-model="modalAsignacionRapida"
:muestra="muestra"
@aplicar="aplicarAsignacionRapida"
/>
</div>
</template>
<script setup lang="ts">
import type { Muestra, NotaSeleccionada, TipoDefecto, SensacionBoca, GustoPredominante } from '~/types/catacion'
import type { TabCatacion, Subcategoria } from '~/composables/useCatacion'
import { SENSACIONES_BOCA, GUSTOS_PREDOMINANTES, TIPOS_DEFECTOS } from '~/types/catacion'
interface FormularioMuestraProps {
/** Muestra a editar */
muestra: Muestra
/** Tab activa */
tabActiva: TabCatacion
/** Subcategorías activas (filtros) */
subcategoriasActivas?: Subcategoria[]
}
const props = withDefaults(defineProps<FormularioMuestraProps>(), {
subcategoriasActivas: () => [],
})
const { actualizarIntensidad: actualizarIntensidadCatacion } = useCatacion()
const { getCategoryColor } = useCategoryColors()
// Función para obtener el icono de cada categoría
const getCategoryIcon = (category: string): string => {
const icons: Record<string, string> = {
fragancia: 'i-lucide-flower-2',
aroma: 'i-lucide-wind',
sabor: 'i-lucide-candy',
saborResidual: 'i-lucide-timer',
acidez: 'i-lucide-citrus',
dulzor: 'i-lucide-cookie',
sensacionBoca: 'i-lucide-droplets',
impresionGlobal: 'i-lucide-star',
}
return icons[category] || 'i-lucide-circle'
}
// Listas para los selectores
const sensacionesBoca = SENSACIONES_BOCA
const gustosPredominantes = GUSTOS_PREDOMINANTES
const tiposDefectos = TIPOS_DEFECTOS
// Helpers para filtrado por subcategorías
const deberMostrarSeccion = (subcategorias: Subcategoria[]): boolean => {
// Si no hay filtros activos, mostrar todo
if (!props.subcategoriasActivas || props.subcategoriasActivas.length === 0) return true
// Si hay filtros, verificar si alguno coincide
return subcategorias.some(sub => props.subcategoriasActivas?.includes(sub))
}
// Nueva función para filtrado restrictivo de sliders por tipo y categoría
const deberMostrarSlider = (tipo: 'descriptiva' | 'afectiva', categoria: Subcategoria): boolean => {
// Si no hay filtros activos, mostrar todo
if (!props.subcategoriasActivas || props.subcategoriasActivas.length === 0) return true
// Separar filtros en tipos y categorías
const filtrosTipo = props.subcategoriasActivas.filter(s => s === 'descriptiva' || s === 'afectiva')
const filtrosCategoria = props.subcategoriasActivas.filter(s =>
s !== 'descriptiva' && s !== 'afectiva' && s !== null
)
// Si hay filtros de tipo Y filtros de categoría: verificar ambos
if (filtrosTipo.length > 0 && filtrosCategoria.length > 0) {
return filtrosTipo.includes(tipo) && filtrosCategoria.includes(categoria)
}
// Si solo hay filtros de tipo: verificar que el tipo coincida
if (filtrosTipo.length > 0 && filtrosCategoria.length === 0) {
return filtrosTipo.includes(tipo)
}
// Si solo hay filtros de categoría: verificar que la categoría coincida
if (filtrosCategoria.length > 0 && filtrosTipo.length === 0) {
return filtrosCategoria.includes(categoria)
}
// Si no hay filtros relevantes, mostrar
return true
}
// Para Organoléptica
const mostrarFraganciaAroma = computed(() => deberMostrarSeccion(['fragancia-aroma']))
const mostrarSaborOrganoleptica = computed(() => deberMostrarSeccion(['sabor']))
const mostrarSensacionBocaOrganoleptica = computed(() => deberMostrarSeccion(['sensacion-boca']))
const mostrarGustosPredominantes = computed(() => deberMostrarSeccion(['gustos-predominantes']))
// Para Descriptiva/Afectiva - por categoría (mostrar sección si al menos un slider debe mostrarse)
const mostrarFraganciaSlider = computed(() =>
deberMostrarSlider('descriptiva', 'fragancia') || deberMostrarSlider('afectiva', 'fragancia')
)
const mostrarAromaSlider = computed(() =>
deberMostrarSlider('descriptiva', 'aroma') || deberMostrarSlider('afectiva', 'aroma')
)
const mostrarSaborSlider = computed(() =>
deberMostrarSlider('descriptiva', 'sabor') || deberMostrarSlider('afectiva', 'sabor')
)
const mostrarSaborResidualSlider = computed(() =>
deberMostrarSlider('descriptiva', 'sabor-residual') || deberMostrarSlider('afectiva', 'sabor-residual')
)
const mostrarAcidezSlider = computed(() =>
deberMostrarSlider('descriptiva', 'acidez') || deberMostrarSlider('afectiva', 'acidez')
)
const mostrarDulzorSlider = computed(() =>
deberMostrarSlider('descriptiva', 'dulzor') || deberMostrarSlider('afectiva', 'dulzor')
)
const mostrarSensacionBocaSlider = computed(() =>
deberMostrarSlider('descriptiva', 'sensacion-boca') || deberMostrarSlider('afectiva', 'sensacion-boca')
)
const mostrarImpresionGlobalSlider = computed(() =>
deberMostrarSlider('descriptiva', 'impresion-global') || deberMostrarSlider('afectiva', 'impresion-global')
)
// Para cada slider individual
const mostrarFraganciaDescriptiva = computed(() => deberMostrarSlider('descriptiva', 'fragancia'))
const mostrarFraganciaAfectiva = computed(() => deberMostrarSlider('afectiva', 'fragancia'))
const mostrarAromaDescriptiva = computed(() => deberMostrarSlider('descriptiva', 'aroma'))
const mostrarAromaAfectiva = computed(() => deberMostrarSlider('afectiva', 'aroma'))
const mostrarSaborDescriptiva = computed(() => deberMostrarSlider('descriptiva', 'sabor'))
const mostrarSaborAfectiva = computed(() => deberMostrarSlider('afectiva', 'sabor'))
const mostrarSaborResidualDescriptiva = computed(() => deberMostrarSlider('descriptiva', 'sabor-residual'))
const mostrarSaborResidualAfectiva = computed(() => deberMostrarSlider('afectiva', 'sabor-residual'))
const mostrarAcidezDescriptiva = computed(() => deberMostrarSlider('descriptiva', 'acidez'))
const mostrarAcidezAfectiva = computed(() => deberMostrarSlider('afectiva', 'acidez'))
const mostrarDulzorDescriptiva = computed(() => deberMostrarSlider('descriptiva', 'dulzor'))
const mostrarDulzorAfectiva = computed(() => deberMostrarSlider('afectiva', 'dulzor'))
const mostrarSensacionBocaDescriptiva = computed(() => deberMostrarSlider('descriptiva', 'sensacion-boca'))
const mostrarSensacionBocaAfectiva = computed(() => deberMostrarSlider('afectiva', 'sensacion-boca'))
const mostrarImpresionGlobalDescriptiva = computed(() => deberMostrarSlider('descriptiva', 'impresion-global'))
const mostrarImpresionGlobalAfectiva = computed(() => deberMostrarSlider('afectiva', 'impresion-global'))
// Estado local para otras notas
const otrasNotasLocal = ref(props.muestra.otrasNotas)
// Estado del modal de asignación rápida
const modalAsignacionRapida = ref(false)
// Aplicar asignación rápida de puntajes
const aplicarAsignacionRapida = async (puntajes: Record<string, number>) => {
// Aplicar cada puntaje a su categoría correspondiente
for (const [categoria, puntaje] of Object.entries(puntajes)) {
await actualizarIntensidad(categoria as keyof Muestra['intensidades'], 'descriptiva', puntaje)
}
}
// Actualizar intensidad
const actualizarIntensidad = async (
parametro: keyof Muestra['intensidades'],
tipo: 'descriptiva' | 'afectiva',
valor: number | null
) => {
await actualizarIntensidadCatacion(props.muestra.muestraId, parametro, tipo, valor)
}
// Actualizar fragancia/aroma
const { actualizarFraganciaAroma: actualizarFraganciaAromaCatacion } = useCatacion()
const actualizarFraganciaAroma = async (nota: NotaSeleccionada) => {
await actualizarFraganciaAromaCatacion(
props.muestra.muestraId,
nota.categorias,
nota.subcategorias,
nota.notaEspecifica
)
}
// Actualizar sabor
const { actualizarSabor: actualizarSaborCatacion } = useCatacion()
const actualizarSabor = async (nota: NotaSeleccionada) => {
await actualizarSaborCatacion(
props.muestra.muestraId,
nota.categorias,
nota.subcategorias,
nota.notaEspecifica
)
}
// Actualizar tazas
const { actualizarTazasNoUniformes: actualizarTazasNoUniformesCatacion, actualizarTazasDefectuosas: actualizarTazasDefectuosasCatacion } = useCatacion()
const actualizarTazasNoUniformes = async (tazas: number[]) => {
await actualizarTazasNoUniformesCatacion(props.muestra.muestraId, tazas)
}
const actualizarTazasDefectuosas = async (tazas: number[]) => {
await actualizarTazasDefectuosasCatacion(props.muestra.muestraId, tazas)
}
// Actualizar defecto
const { actualizarDefecto: actualizarDefectoCatacion } = useCatacion()
const actualizarDefecto = async (defecto: TipoDefecto) => {
await actualizarDefectoCatacion(props.muestra.muestraId, defecto)
}
// Seleccionar sensación en boca (selección única)
const { actualizarSensacionBoca } = useCatacion()
const seleccionarSensacionBoca = async (sensacion: SensacionBoca) => {
await actualizarSensacionBoca(props.muestra.muestraId, sensacion)
}
// Toggle gusto predominante
const { actualizarGustosPredominantes } = useCatacion()
const toggleGustoPredominante = async (gusto: GustoPredominante) => {
const gustos = [...props.muestra.gustosPredominantes]
const index = gustos.indexOf(gusto)
if (index > -1) {
gustos.splice(index, 1)
} else {
if (gustos.length >= 2) return // Máximo 2
gustos.push(gusto)
}
if (gustos.length === 0) return // Mínimo 1
await actualizarGustosPredominantes(props.muestra.muestraId, gustos)
}
// Actualizar otras notas
const { actualizarOtrasNotas: actualizarOtrasNotasCatacion } = useCatacion()
const actualizarOtrasNotas = async () => {
const notas = otrasNotasLocal.value.trim()
await actualizarOtrasNotasCatacion(props.muestra.muestraId, notas)
}
// Sincronizar otras notas cuando cambia la muestra
watch(() => props.muestra.otrasNotas, (newVal) => {
if (newVal !== otrasNotasLocal.value) {
otrasNotasLocal.value = newVal
}
})
</script>
<style scoped>
.formulario-muestra {
width: 100%;
}
.tab-section-title {
font-size: 1.125rem;
font-weight: 600;
border-bottom: 1px solid;
padding-bottom: 0.5rem;
border-color: color-mix(in srgb, var(--cata-primary) 30%, transparent);
}
.global-section-title {
font-size: 1rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
opacity: 0.9;
}
.form-section {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.form-section-title {
font-size: 0.875rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.form-subsection-title {
font-size: 0.8125rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.puntaje-final {
text-align: center;
}
/* Grid de sensaciones y gustos - estilo compacto como subcategorías */
.sensaciones-grid {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
}
.sensacion-item {
position: relative;
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.5rem;
min-height: 32px;
}
.sensacion-item:focus {
outline: 2px solid transparent;
outline-offset: 2px;
}
.sensacion-item:focus-visible {
box-shadow: 0 0 0 2px var(--cata-primary);
}
.sensacion-text {
font-size: 0.75rem;
}
.gustos-grid {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
}
.gusto-item {
position: relative;
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.5rem;
min-height: 32px;
}
.gusto-item:focus {
outline: 2px solid transparent;
outline-offset: 2px;
}
.gusto-item:focus-visible {
box-shadow: 0 0 0 2px var(--cata-primary);
}
.gusto-text {
font-size: 0.75rem;
}
/* Animaciones */
.sensacion-item.cata-checkbox-checked,
.gusto-item.cata-checkbox-checked {
transform: scale(1.02);
}
.sensacion-item:not(.disabled):hover,
.gusto-item:not(.disabled):hover {
transform: scale(1.02);
}
.sensacion-item:not(.disabled):active,
.gusto-item:not(.disabled):active {
transform: scale(0.98);
}
/* Responsive */
@media (max-width: 640px) {
.sensacion-item {
min-height: 28px;
padding: 0.25rem 0.375rem;
}
.sensacion-text {
font-size: 0.6875rem;
}
.gusto-item {
min-height: 28px;
padding: 0.25rem 0.375rem;
}
.gusto-text {
font-size: 0.6875rem;
}
}
/* Touch-friendly */
@media (hover: none) and (pointer: coarse) {
.sensacion-item {
min-height: 36px;
}
.gusto-item {
min-height: 36px;
}
}
@media (max-width: 768px) {
.formulario-muestra {
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.tab-section-title {
font-size: 1rem;
}
.global-section-title {
font-size: 0.9rem;
}
}
</style>