Files
cataRio/nuxt4/app/components/cata/FormularioMuestra.vue
josedario87 6c9ad356b1
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m6s
Fix: Actualizar modal a sintaxis Nuxt UI v4 y aplicar estilos consistentes
- Actualizar ModalAsignacionRapida a sintaxis correcta de Nuxt UI v4
  - Usar v-model:open en lugar de v-model
  - Usar slots #body y #footer directamente
  - Eliminar UCard (deprecado)

- Aplicar clases cata-* para mantener consistencia con el resto de la app
  - Usar cata-text para textos
  - Usar cata-button para botones
  - Usar cata-input para inputs
  - Usar cata-outline-box para bordes

- Respetar colores personalizados del usuario mediante variables CSS
  - selected-category usa var(--cata-primary)
  - Soporte para modo oscuro con efectos de sombra

- Eliminar icono de estrella/rayo del BotonNubeCaustica
  - Simplificar diseño dejando solo la nube con patrones cáusticos
  - Eliminar animación sparkle asociada

- Integrar modal correctamente en FormularioMuestra
  - Mover botón dentro del slot por defecto del modal
  - Eliminar modal duplicado del template
2025-10-19 01:01:55 -06:00

1214 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">
<!-- Modal de Asignación Rápida -->
<div class="flex justify-start mb-4">
<CataModalAsignacionRapida
v-model="modalAsignacionRapida"
:muestra="muestra"
@aplicar="aplicarAsignacionRapida"
>
<CataBotonNubeCaustica />
</CataModalAsignacionRapida>
</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>
</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>