Files
cataRio/nuxt4/app/components/cata/ModalAsignacionRapida.vue
josedario87 1ef86d4281
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m7s
Feat: Aumentar blur del overlay en modal de asignación rápida
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.
2025-10-19 02:57:04 -06:00

410 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>