Files
cataRio/nuxt4/app/components/cata/ModalAsignacionRapida.vue
josedario87 6994114d91
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m8s
Feat: Aumentar blur del overlay y aplicar estilos cata a UInputNumber
CAMBIOS EN OVERLAY:

1. Blur más agresivo:
   - Antes: blur(10px)
   - Ahora: blur(16px)
   - Efecto mucho más pronunciado

2. Opacidad aumentada:
   - Antes: 0.90 (90%)
   - Ahora: 0.95 (95%)
   - Fondo casi completamente opaco

ESTILOS PERSONALIZADOS PARA UInputNumber:

1. Input principal:
   - background-color: var(--cata-bg)
   - color: var(--cata-text)
   - border-color: var(--cata-primary) con transparencia

2. Estado focus:
   - border-color: var(--cata-primary) sólido
   - box-shadow con color primario
   - En dark mode: glow effect adicional

3. Placeholder:
   - color: var(--cata-text) al 50% de opacidad
   - Respeta el color del texto del usuario

4. Botones +/-:
   - color: var(--cata-primary)
   - Hover: opacity 0.8
   - Active: scale 0.95 (feedback táctil)

5. Modo oscuro:
   - Border más visible (40% de primary)
   - Box-shadow con glow effect en focus

RESULTADO:
- UInputNumber ahora respeta completamente los colores del usuario
- Overlay con blur muy pronunciado para mejor enfoque
- Consistencia visual con el resto de la aplicación
2025-10-19 03:02:32 -06:00

460 lines
14 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 cata-input-number"
variant="outline"
:ui="{
base: 'cata-input text-center text-lg',
}"
@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 cata-input-number"
variant="outline"
:ui="{
base: 'cata-input text-center text-lg',
}"
: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.95 !important;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
}
/* Estilos para UInputNumber para que respete los colores del usuario */
:deep(.cata-input-number input) {
background-color: var(--cata-bg) !important;
color: var(--cata-text) !important;
border-color: color-mix(in srgb, var(--cata-primary) 30%, transparent) !important;
}
:deep(.cata-input-number input:focus) {
border-color: var(--cata-primary) !important;
outline: none !important;
box-shadow: 0 0 0 1px var(--cata-primary) !important;
}
:deep(.cata-input-number input::placeholder) {
color: color-mix(in srgb, var(--cata-text) 50%, transparent) !important;
}
/* Botones de incremento/decremento */
:deep(.cata-input-number button) {
color: var(--cata-primary) !important;
transition: all 0.2s ease;
}
:deep(.cata-input-number button:hover) {
color: var(--cata-primary) !important;
opacity: 0.8;
}
:deep(.cata-input-number button:active) {
transform: scale(0.95);
}
/* Modo oscuro */
.dark :deep(.cata-input-number input) {
background-color: var(--cata-bg) !important;
border-color: color-mix(in srgb, var(--cata-primary) 40%, transparent) !important;
}
.dark :deep(.cata-input-number input:focus) {
box-shadow: 0 0 0 1px var(--cata-primary), 0 0 8px color-mix(in srgb, var(--cata-primary) 30%, transparent) !important;
}
</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>