All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m7s
CAMBIOS: 1. Blur aumentado de 4px a 10px: - Antes: backdrop-filter: blur(4px) - Ahora: backdrop-filter: blur(10px) - Efecto más agresivo en el fondo 2. Opacidad aumentada de 0.85 a 0.90: - Fondo ligeramente más oscuro - Mejor enfoque en el modal 3. Compatibilidad mejorada: - Agregado -webkit-backdrop-filter para WebKit - Mejor soporte en Safari y navegadores basados en Chromium RESULTADO: El modal ahora tiene un efecto de blur medianamente agresivo que ayuda a enfocar la atención en el contenido del modal.
410 lines
13 KiB
Vue
410 lines
13 KiB
Vue
<template>
|
||
<UModal
|
||
v-model:open="isOpen"
|
||
:ui="{
|
||
overlay: 'modal-overlay',
|
||
content: 'cata-bg cata-outline-box',
|
||
header: 'cata-bg',
|
||
body: 'cata-bg',
|
||
footer: 'cata-bg',
|
||
title: 'cata-text'
|
||
}"
|
||
>
|
||
<!-- Trigger (slot por defecto) -->
|
||
<slot />
|
||
|
||
<!-- Título personalizado -->
|
||
<template #title>
|
||
<span class="cata-text">Asignación Rápida de Puntajes</span>
|
||
</template>
|
||
|
||
<!-- Contenido del modal -->
|
||
<template #body>
|
||
<div class="space-y-4">
|
||
<!-- Paso 1: Ingresar puntaje total deseado -->
|
||
<div v-if="paso === 1" class="space-y-3">
|
||
<!-- Input de Sumatoria Afectiva -->
|
||
<div>
|
||
<label class="block text-xs cata-text opacity-75 mb-1 text-center">
|
||
Sumatoria Afectiva
|
||
</label>
|
||
<UInputNumber
|
||
v-model="puntajeDeseado"
|
||
:min="8"
|
||
:max="72"
|
||
:step="1"
|
||
placeholder="8-72"
|
||
class="w-full"
|
||
@update:model-value="onSumatoriaChangeFromButtons"
|
||
@blur="onSumatoriaBlur"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Input de SCAA Score -->
|
||
<div>
|
||
<label class="block text-xs cata-text opacity-75 mb-1 text-center">
|
||
SCAA Score
|
||
</label>
|
||
<UInputNumber
|
||
v-model="scaaDeseado"
|
||
:min="scaaMin"
|
||
:max="scaaMax"
|
||
:step="0.25"
|
||
placeholder="58.00-100.00"
|
||
class="w-full"
|
||
:format-options="{ minimumFractionDigits: 2, maximumFractionDigits: 2 }"
|
||
@update:model-value="onScaaChangeFromButtons"
|
||
@blur="onScaaBlur"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Paso 2: Seleccionar categorías que sobresalen o palidecen -->
|
||
<div v-if="paso === 2">
|
||
<!-- Subtítulo con icono y contador -->
|
||
<div class="flex items-center gap-2 mb-3 cata-text font-medium">
|
||
<UIcon
|
||
:name="multiploMasCercano < puntajeDeseado ? 'i-heroicons-arrow-up-circle' : 'i-heroicons-arrow-down-circle'"
|
||
class="w-5 h-5"
|
||
:style="{ color: 'var(--cata-primary)' }"
|
||
/>
|
||
<span>{{ multiploMasCercano < puntajeDeseado ? 'Sobresale' : 'Palidece' }}</span>
|
||
<span class="ml-auto opacity-60">{{ categoriasSeleccionadas.length }}/{{ diferencia }}</span>
|
||
</div>
|
||
|
||
<div class="space-y-2">
|
||
<button
|
||
v-for="cat in categoriasDisponibles"
|
||
:key="cat.key"
|
||
type="button"
|
||
:class="[
|
||
'w-full px-3 py-2 rounded-md transition-all text-left flex items-center gap-2',
|
||
'cata-outline-box',
|
||
categoriasSeleccionadas.includes(cat.key)
|
||
? 'selected-category'
|
||
: ''
|
||
]"
|
||
:disabled="!categoriasSeleccionadas.includes(cat.key) && categoriasSeleccionadas.length >= diferencia"
|
||
@click="toggleCategoria(cat.key)"
|
||
>
|
||
<UIcon :name="cat.icon" class="w-4 h-4" :style="{ color: cat.color }" />
|
||
<span class="cata-text">{{ cat.label }}</span>
|
||
<UIcon
|
||
v-if="categoriasSeleccionadas.includes(cat.key)"
|
||
:name="multiploMasCercano < puntajeDeseado ? 'i-heroicons-arrow-up-circle-solid' : 'i-heroicons-arrow-down-circle-solid'"
|
||
class="w-5 h-5 ml-auto"
|
||
:style="{ color: cat.color }"
|
||
/>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Footer con botones -->
|
||
<template #footer>
|
||
<div class="flex items-center justify-between w-full">
|
||
<!-- Puntaje target a la izquierda -->
|
||
<div v-if="paso === 2" class="flex items-center gap-2 cata-text font-semibold">
|
||
<UIcon name="i-lucide-target" class="w-5 h-5" :style="{ color: 'var(--cata-primary)' }" />
|
||
<span>{{ puntajeDeseado }}</span>
|
||
</div>
|
||
<div v-else></div>
|
||
|
||
<!-- Botones a la derecha -->
|
||
<div class="flex items-center gap-2">
|
||
<button
|
||
class="cata-button px-4 py-2"
|
||
@click="cerrar"
|
||
>
|
||
Cancelar
|
||
</button>
|
||
|
||
<button
|
||
v-if="paso === 1"
|
||
class="cata-button px-4 py-2"
|
||
:disabled="!puntajeValido"
|
||
:class="{ 'opacity-50 cursor-not-allowed': !puntajeValido }"
|
||
@click="siguientePaso"
|
||
>
|
||
Continuar
|
||
</button>
|
||
|
||
<button
|
||
v-if="paso === 2"
|
||
class="cata-button px-4 py-2"
|
||
:disabled="categoriasSeleccionadas.length !== diferencia"
|
||
:class="{ 'opacity-50 cursor-not-allowed': categoriasSeleccionadas.length !== diferencia }"
|
||
@click="aplicarAsignacion"
|
||
>
|
||
Aplicar
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</UModal>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import type { Muestra } from '~/types/catacion'
|
||
import { scaaASumatoriaAfectiva, sumatoriaAfectivaASCAA, redondearA025 } from '~/types/catacion'
|
||
|
||
interface Props {
|
||
modelValue: boolean
|
||
muestra: Muestra
|
||
}
|
||
|
||
const props = defineProps<Props>()
|
||
|
||
const emit = defineEmits<{
|
||
'update:modelValue': [value: boolean]
|
||
'aplicar': [puntajes: Record<string, number>]
|
||
}>()
|
||
|
||
// Categorías disponibles
|
||
const categoriasDisponibles = [
|
||
{ key: 'fragancia', label: 'Fragancia', icon: 'i-lucide-flower-2', color: '#8B7AB8' },
|
||
{ key: 'aroma', label: 'Aroma', icon: 'i-lucide-wind', color: '#26A69A' },
|
||
{ key: 'sabor', label: 'Sabor', icon: 'i-lucide-candy', color: '#E53935' },
|
||
{ key: 'saborResidual', label: 'Sabor Residual', icon: 'i-lucide-timer', color: '#F57C00' },
|
||
{ key: 'acidez', label: 'Acidez', icon: 'i-lucide-citrus', color: '#FDD835' },
|
||
{ key: 'dulzor', label: 'Dulzor', icon: 'i-lucide-cookie', color: '#EC407A' },
|
||
{ key: 'sensacionBoca', label: 'Sensación en Boca', icon: 'i-lucide-droplets', color: '#1E88E5' },
|
||
{ key: 'impresionGlobal', label: 'Impresión Global', icon: 'i-lucide-star', color: '#00ACC1' },
|
||
]
|
||
|
||
// Estado del modal
|
||
const isOpen = computed({
|
||
get: () => props.modelValue,
|
||
set: (value) => emit('update:modelValue', value),
|
||
})
|
||
|
||
// Estado del formulario
|
||
const paso = ref(1)
|
||
const puntajeDeseado = ref<number>(40)
|
||
const scaaDeseado = ref<number>(sumatoriaAfectivaASCAA(40))
|
||
const categoriasSeleccionadas = ref<string[]>([])
|
||
|
||
// Flag para evitar loops infinitos en la sincronización
|
||
const actualizandoInput = ref(false)
|
||
|
||
// Límites de SCAA Score (basados en sumatoria afectiva 8-72)
|
||
// 8 categorías × 1 punto mínimo = 8
|
||
// 8 categorías × 9 puntos máximo = 72
|
||
const scaaMin = sumatoriaAfectivaASCAA(8)
|
||
const scaaMax = sumatoriaAfectivaASCAA(72)
|
||
|
||
// Resetear estado cuando el modal se abre
|
||
watch(isOpen, (newValue) => {
|
||
if (newValue) {
|
||
paso.value = 1
|
||
puntajeDeseado.value = 40
|
||
scaaDeseado.value = sumatoriaAfectivaASCAA(40)
|
||
categoriasSeleccionadas.value = []
|
||
}
|
||
})
|
||
|
||
// Handler para cuando se modifica Sumatoria Afectiva usando botones +/-
|
||
const onSumatoriaChangeFromButtons = (newValue: number | null) => {
|
||
if (actualizandoInput.value) return
|
||
if (newValue === null || newValue === undefined) return
|
||
|
||
actualizandoInput.value = true
|
||
|
||
// El valor ya viene validado por UInputNumber (min/max/step)
|
||
puntajeDeseado.value = newValue
|
||
|
||
// Actualizar SCAA Score automáticamente
|
||
scaaDeseado.value = sumatoriaAfectivaASCAA(puntajeDeseado.value)
|
||
|
||
actualizandoInput.value = false
|
||
}
|
||
|
||
// Handler para cuando se pierde el foco en Sumatoria Afectiva (escritura manual)
|
||
const onSumatoriaBlur = () => {
|
||
if (actualizandoInput.value) return
|
||
|
||
actualizandoInput.value = true
|
||
|
||
// Si el valor es inválido (NaN, null, undefined), resetear a default
|
||
if (!puntajeDeseado.value || isNaN(puntajeDeseado.value)) {
|
||
puntajeDeseado.value = 40
|
||
}
|
||
|
||
// Redondear a entero
|
||
puntajeDeseado.value = Math.round(puntajeDeseado.value)
|
||
|
||
// Validar rango (solo después de redondear)
|
||
if (puntajeDeseado.value < 8) puntajeDeseado.value = 8
|
||
if (puntajeDeseado.value > 72) puntajeDeseado.value = 72
|
||
|
||
// Actualizar SCAA Score
|
||
scaaDeseado.value = sumatoriaAfectivaASCAA(puntajeDeseado.value)
|
||
|
||
actualizandoInput.value = false
|
||
}
|
||
|
||
// Handler para cuando se modifica SCAA Score usando botones +/-
|
||
const onScaaChangeFromButtons = (newValue: number | null) => {
|
||
if (actualizandoInput.value) return
|
||
if (newValue === null || newValue === undefined) return
|
||
|
||
actualizandoInput.value = true
|
||
|
||
// El valor ya viene validado por UInputNumber (min/max/step)
|
||
scaaDeseado.value = newValue
|
||
|
||
// Convertir a Sumatoria Afectiva
|
||
const sumatoriaCalculada = scaaASumatoriaAfectiva(scaaDeseado.value)
|
||
|
||
// Redondear al entero más cercano
|
||
const sumatoriaRedondeada = Math.round(sumatoriaCalculada)
|
||
|
||
// Asegurar rango válido
|
||
puntajeDeseado.value = Math.max(8, Math.min(72, sumatoriaRedondeada))
|
||
|
||
// Recalcular SCAA Score con el valor redondeado
|
||
scaaDeseado.value = sumatoriaAfectivaASCAA(puntajeDeseado.value)
|
||
|
||
actualizandoInput.value = false
|
||
}
|
||
|
||
// Handler para cuando se pierde el foco en SCAA Score (escritura manual)
|
||
const onScaaBlur = () => {
|
||
if (actualizandoInput.value) return
|
||
|
||
actualizandoInput.value = true
|
||
|
||
// Si el valor es inválido (NaN, null, undefined), resetear a default
|
||
if (!scaaDeseado.value || isNaN(scaaDeseado.value)) {
|
||
scaaDeseado.value = sumatoriaAfectivaASCAA(40)
|
||
}
|
||
|
||
// Redondear al múltiplo de 0.25 más cercano
|
||
scaaDeseado.value = redondearA025(scaaDeseado.value)
|
||
|
||
// Validar rango (solo después de redondear)
|
||
if (scaaDeseado.value < scaaMin) scaaDeseado.value = scaaMin
|
||
if (scaaDeseado.value > scaaMax) scaaDeseado.value = scaaMax
|
||
|
||
// Convertir a Sumatoria Afectiva
|
||
const sumatoriaCalculada = scaaASumatoriaAfectiva(scaaDeseado.value)
|
||
|
||
// Redondear al entero más cercano (sumatoria afectiva debe ser entero)
|
||
const sumatoriaRedondeada = Math.round(sumatoriaCalculada)
|
||
|
||
// Asegurar rango válido
|
||
puntajeDeseado.value = Math.max(8, Math.min(72, sumatoriaRedondeada))
|
||
|
||
// Recalcular SCAA Score con el valor redondeado (ya aplicará redondeo a 0.25)
|
||
scaaDeseado.value = sumatoriaAfectivaASCAA(puntajeDeseado.value)
|
||
|
||
actualizandoInput.value = false
|
||
}
|
||
|
||
// Cálculos
|
||
const puntajeValido = computed(() => {
|
||
return puntajeDeseado.value >= 8 && puntajeDeseado.value <= 72
|
||
})
|
||
|
||
const multiploMasCercano = computed(() => {
|
||
if (!puntajeValido.value) return 0
|
||
|
||
const valorActual = puntajeDeseado.value
|
||
const multiploInferior = Math.floor(valorActual / 8) * 8
|
||
const multiploSuperior = Math.ceil(valorActual / 8) * 8
|
||
|
||
const distInferior = Math.abs(valorActual - multiploInferior)
|
||
const distSuperior = Math.abs(valorActual - multiploSuperior)
|
||
|
||
// Si las distancias son iguales, elegir el mayor
|
||
if (distInferior === distSuperior) {
|
||
return multiploSuperior
|
||
}
|
||
|
||
return distInferior < distSuperior ? multiploInferior : multiploSuperior
|
||
})
|
||
|
||
const puntajeBase = computed(() => {
|
||
return multiploMasCercano.value / 8
|
||
})
|
||
|
||
const diferencia = computed(() => {
|
||
return Math.abs(puntajeDeseado.value - multiploMasCercano.value)
|
||
})
|
||
|
||
// Métodos
|
||
const siguientePaso = () => {
|
||
if (!puntajeValido.value) return
|
||
|
||
// Si el múltiplo es exacto (diferencia = 0), aplicar directamente
|
||
if (diferencia.value === 0) {
|
||
aplicarAsignacionDirecta()
|
||
} else {
|
||
paso.value = 2
|
||
}
|
||
}
|
||
|
||
const toggleCategoria = (key: string) => {
|
||
const index = categoriasSeleccionadas.value.indexOf(key)
|
||
if (index >= 0) {
|
||
categoriasSeleccionadas.value.splice(index, 1)
|
||
} else {
|
||
if (categoriasSeleccionadas.value.length < diferencia.value) {
|
||
categoriasSeleccionadas.value.push(key)
|
||
}
|
||
}
|
||
}
|
||
|
||
const aplicarAsignacionDirecta = () => {
|
||
const puntajes: Record<string, number> = {}
|
||
|
||
categoriasDisponibles.forEach(cat => {
|
||
puntajes[cat.key] = puntajeBase.value
|
||
})
|
||
|
||
emit('aplicar', puntajes)
|
||
cerrar()
|
||
}
|
||
|
||
const aplicarAsignacion = () => {
|
||
if (categoriasSeleccionadas.value.length !== diferencia.value) return
|
||
|
||
const puntajes: Record<string, number> = {}
|
||
const ajuste = multiploMasCercano.value < puntajeDeseado.value ? 1 : -1
|
||
|
||
categoriasDisponibles.forEach(cat => {
|
||
const esSeleccionada = categoriasSeleccionadas.value.includes(cat.key)
|
||
puntajes[cat.key] = puntajeBase.value + (esSeleccionada ? ajuste : 0)
|
||
})
|
||
|
||
emit('aplicar', puntajes)
|
||
cerrar()
|
||
}
|
||
|
||
const cerrar = () => {
|
||
isOpen.value = false
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.modal-overlay {
|
||
background-color: var(--cata-bg) !important;
|
||
opacity: 0.90 !important;
|
||
backdrop-filter: blur(10px);
|
||
-webkit-backdrop-filter: blur(10px);
|
||
}
|
||
</style>
|
||
|
||
<style scoped>
|
||
.selected-category {
|
||
background-color: color-mix(in srgb, var(--cata-primary) 10%, transparent);
|
||
border-color: var(--cata-primary);
|
||
}
|
||
|
||
.dark .selected-category {
|
||
background-color: color-mix(in srgb, var(--cata-primary) 15%, transparent);
|
||
box-shadow: 0 0 8px color-mix(in srgb, var(--cata-primary) 20%, transparent);
|
||
}
|
||
</style>
|