Files
cataRio/nuxt4/app/components/cata/FormularioMuestra.vue
josedario87 816a3e860a
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m7s
Fix: Refactorizar sensaciones en boca y hacer checkboxes ultra compactos
CAMBIOS EN SENSACIONES EN BOCA:
- Reducir opciones a solo 5: Áspero, Aceitoso, Metálico, Deja seca la boca, Suave
- Cambiar de selección múltiple a selección única
- Actualizar tipo de sensacionEnBoca: SensacionBoca[] → SensacionBoca | null

CAMBIOS EN CHECKBOXES (sensaciones y gustos):
- Hacer checkboxes tan compactos como subcategorías de SelectorFamilia
- Usar flex-wrap en todos los breakpoints (eliminar grid en desktop)
- Dimensiones ultra compactas:
  * Desktop: min-height 32px, padding 0.375rem 0.5rem, font-size 0.75rem
  * Mobile: min-height 28px, padding 0.25rem 0.375rem, font-size 0.6875rem
  * Touch: min-height 36px para dispositivos táctiles

ARCHIVOS MODIFICADOS:
- app/types/catacion.ts: Actualizar SensacionBoca y SENSACIONES_BOCA
- app/composables/useCatacion.ts: Cambiar actualizarSensacionBoca a selección única
- app/components/cata/FormularioMuestra.vue: UI compacta y selección única
- app/components/cata/ResumenMuestra.vue: Adaptar a sensacionEnBoca única
2025-10-18 16:55:37 -06:00

1067 lines
41 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">
<!-- Sliders de Fragancia -->
<div v-if="mostrarFraganciaSlider" class="form-section">
<div class="flex items-center justify-between mb-3">
<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-4">
<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>
<!-- Sliders de Aroma -->
<div v-if="mostrarAromaSlider" class="form-section">
<div class="flex items-center justify-between mb-3">
<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-4">
<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>
<!-- Sliders de Sabor -->
<div v-if="mostrarSaborSlider" class="form-section">
<div class="flex items-center justify-between mb-3">
<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-4">
<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>
<!-- Sliders de Sabor Residual -->
<div v-if="mostrarSaborResidualSlider" class="form-section">
<div class="flex items-center justify-between mb-3">
<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-4">
<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>
<!-- Sliders de Acidez -->
<div v-if="mostrarAcidezSlider" class="form-section">
<div class="flex items-center justify-between mb-3">
<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-4">
<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>
<!-- Sliders de Dulzor -->
<div v-if="mostrarDulzorSlider" class="form-section">
<div class="flex items-center justify-between mb-3">
<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-4">
<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>
<!-- Sliders de Sensación en Boca -->
<div v-if="mostrarSensacionBocaSlider" class="form-section">
<div class="flex items-center justify-between mb-3">
<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-4">
<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>
<!-- Sliders de Impresión Global -->
<div v-if="mostrarImpresionGlobalSlider" class="form-section">
<div class="flex items-center justify-between mb-3">
<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-4">
<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>
<!-- 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">
<CataSelectorFamilia
tipo="sabor"
label="Familia de Sabor"
:model-value="muestra.saborNotas"
@update:model-value="actualizarSabor"
/>
</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-3">
<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-4">
<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-3">
<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-4">
<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-3">
<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-4">
<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-3">
<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-4">
<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-3">
<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-4">
<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-3">
<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-4">
<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-3">
<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-4">
<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-3">
<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-4">
<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 afectivos
</p>
</div>
</div>
</div>
</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))
}
// 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
const mostrarFraganciaSlider = computed(() => deberMostrarSeccion(['descriptiva', 'afectiva', 'fragancia']))
const mostrarAromaSlider = computed(() => deberMostrarSeccion(['descriptiva', 'afectiva', 'aroma']))
const mostrarSaborSlider = computed(() => deberMostrarSeccion(['descriptiva', 'afectiva', 'sabor']))
const mostrarSaborResidualSlider = computed(() => deberMostrarSeccion(['descriptiva', 'afectiva', 'sabor-residual']))
const mostrarAcidezSlider = computed(() => deberMostrarSeccion(['descriptiva', 'afectiva', 'acidez']))
const mostrarDulzorSlider = computed(() => deberMostrarSeccion(['descriptiva', 'afectiva', 'dulzor']))
const mostrarSensacionBocaSlider = computed(() => deberMostrarSeccion(['descriptiva', 'afectiva', 'sensacion-boca']))
const mostrarImpresionGlobalSlider = computed(() => deberMostrarSeccion(['descriptiva', 'afectiva', 'impresion-global']))
// Estado local para otras notas
const otrasNotasLocal = ref(props.muestra.otrasNotas)
// 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>