Files
cataRio/nuxt4/app/components/cata/ModalAsignacionRapida.vue
josedario87 e36d7dac6b
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m5s
Fix: Redondear SCAA Score a múltiplos de 0.25
PROBLEMA:
El SCAA Score debe moverse en steps de 0.25 (no aceptar
cualquier valor decimal racional).

SOLUCIÓN:

1. Nueva función en catacion.ts:
   - redondearA025(): Redondea al múltiplo de 0.25 más cercano
   - Math.round(valor / 0.25) * 0.25

2. Aplicado en todas las funciones de SCAA:
   - calcularSCAA(): redondea el resultado final
   - sumatoriaAfectivaASCAA(): redondea la conversión

3. ModalAsignacionRapida.vue:
   - Input step cambiado de 0.01 a 0.25
   - onScaaChange(): redondea el valor ingresado
   - Placeholder actualizado: 58.75-112.00

VALORES VÁLIDOS:
Ahora el SCAA Score solo puede tener valores como:
- 85.00, 85.25, 85.50, 85.75, 86.00, etc.
- Nunca valores como 85.17 o 85.33

La visualización con .toFixed(2) sigue mostrando 2 decimales
correctamente.
2025-10-19 02:37:35 -06:00

355 lines
11 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>
<input
v-model.number="puntajeDeseado"
type="number"
:min="9"
:max="90"
step="1"
placeholder="9-90"
class="cata-input w-full px-3 py-2 rounded-md text-center text-lg"
@input="onSumatoriaChange"
/>
</div>
<!-- Input de SCAA Score -->
<div>
<label class="block text-xs cata-text opacity-75 mb-1 text-center">
SCAA Score
</label>
<input
v-model.number="scaaDeseado"
type="number"
:min="scaaMin"
:max="scaaMax"
step="0.25"
placeholder="58.75-112.00"
class="cata-input w-full px-3 py-2 rounded-md text-center text-lg"
@input="onScaaChange"
/>
</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>(45)
const scaaDeseado = ref<number>(sumatoriaAfectivaASCAA(45))
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 9-90)
const scaaMin = sumatoriaAfectivaASCAA(9)
const scaaMax = sumatoriaAfectivaASCAA(90)
// Resetear estado cuando el modal se abre
watch(isOpen, (newValue) => {
if (newValue) {
paso.value = 1
puntajeDeseado.value = 45
scaaDeseado.value = sumatoriaAfectivaASCAA(45)
categoriasSeleccionadas.value = []
}
})
// Handler para cuando se modifica Sumatoria Afectiva
const onSumatoriaChange = () => {
if (actualizandoInput.value) return
actualizandoInput.value = true
// Validar rango
if (puntajeDeseado.value < 9) puntajeDeseado.value = 9
if (puntajeDeseado.value > 90) puntajeDeseado.value = 90
// Redondear a entero
puntajeDeseado.value = Math.round(puntajeDeseado.value)
// Actualizar SCAA Score
scaaDeseado.value = sumatoriaAfectivaASCAA(puntajeDeseado.value)
actualizandoInput.value = false
}
// Handler para cuando se modifica SCAA Score
const onScaaChange = () => {
if (actualizandoInput.value) return
actualizandoInput.value = true
// Redondear al múltiplo de 0.25 más cercano
scaaDeseado.value = redondearA025(scaaDeseado.value)
// Validar rango
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(9, Math.min(90, 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 >= 9 && puntajeDeseado.value <= 90
})
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.85 !important;
backdrop-filter: blur(4px);
}
</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>