Feat: Implementar asignación rápida de puntajes con botón de nube cáustica
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m8s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m8s
- Crear ModalAsignacionRapida.vue con lógica de distribución por múltiplos de 8 - Input numérico (8-80) para puntaje total deseado - Cálculo automático del múltiplo de 8 más cercano - Si múltiplo < valor: permite seleccionar categorías que sobresalen (+1) - Si múltiplo > valor: permite seleccionar categorías que palidecen (-1) - Si múltiplo = valor: asignación directa sin ajustes - Crear BotonNubeCaustica.vue con diseño especial - Forma de nube usando SVG path - Animación de patrones de luz cáustica con gradientes animados - Efectos de brillo y ondas al hacer hover - Icono de rayo mágico con animación sparkle - Integrar funcionalidad en FormularioMuestra.vue - Ubicar botón en esquina superior izquierda de sección Descriptiva/Afectiva - Aplicar puntajes calculados a todas las categorías descriptivas - Actualizar puntaje final automáticamente - Corregir comentario: "Suma de valores descriptivos" (era "afectivos")
This commit is contained in:
260
nuxt4/app/components/cata/BotonNubeCaustica.vue
Normal file
260
nuxt4/app/components/cata/BotonNubeCaustica.vue
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="boton-nube-caustica"
|
||||||
|
@click="$emit('click')"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 100 60"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="nube-svg"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<!-- Gradiente animado para efecto cáustica -->
|
||||||
|
<linearGradient id="caustic-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" class="caustic-stop-1">
|
||||||
|
<animate
|
||||||
|
attributeName="stop-color"
|
||||||
|
values="#60A5FA;#3B82F6;#60A5FA"
|
||||||
|
dur="3s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</stop>
|
||||||
|
<stop offset="50%" class="caustic-stop-2">
|
||||||
|
<animate
|
||||||
|
attributeName="stop-color"
|
||||||
|
values="#3B82F6;#60A5FA;#93C5FD;#3B82F6"
|
||||||
|
dur="2.5s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</stop>
|
||||||
|
<stop offset="100%" class="caustic-stop-3">
|
||||||
|
<animate
|
||||||
|
attributeName="stop-color"
|
||||||
|
values="#93C5FD;#60A5FA;#93C5FD"
|
||||||
|
dur="3.5s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</stop>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- Filtro para efecto de brillo suave -->
|
||||||
|
<filter id="glow">
|
||||||
|
<feGaussianBlur stdDeviation="2" result="coloredBlur"/>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="coloredBlur"/>
|
||||||
|
<feMergeNode in="SourceGraphic"/>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
|
||||||
|
<!-- Patrón de ondas cáusticas -->
|
||||||
|
<pattern id="caustic-pattern" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
|
||||||
|
<circle cx="10" cy="10" r="8" fill="rgba(255,255,255,0.15)">
|
||||||
|
<animate
|
||||||
|
attributeName="r"
|
||||||
|
values="6;10;6"
|
||||||
|
dur="2s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0.1;0.3;0.1"
|
||||||
|
dur="2s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</circle>
|
||||||
|
<circle cx="30" cy="25" r="6" fill="rgba(255,255,255,0.1)">
|
||||||
|
<animate
|
||||||
|
attributeName="r"
|
||||||
|
values="4;8;4"
|
||||||
|
dur="2.5s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0.05;0.25;0.05"
|
||||||
|
dur="2.5s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</circle>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Forma de nube -->
|
||||||
|
<path
|
||||||
|
d="M 25,40
|
||||||
|
C 25,35 20,30 25,25
|
||||||
|
C 25,20 30,15 35,15
|
||||||
|
C 40,15 45,17 47,20
|
||||||
|
C 50,17 55,15 60,15
|
||||||
|
C 70,15 75,20 75,28
|
||||||
|
C 80,28 85,32 85,38
|
||||||
|
C 85,44 80,48 75,48
|
||||||
|
L 30,48
|
||||||
|
C 27,48 25,45 25,40 Z"
|
||||||
|
class="nube-base"
|
||||||
|
fill="url(#caustic-gradient)"
|
||||||
|
filter="url(#glow)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Capa de patrones cáusticos -->
|
||||||
|
<path
|
||||||
|
d="M 25,40
|
||||||
|
C 25,35 20,30 25,25
|
||||||
|
C 25,20 30,15 35,15
|
||||||
|
C 40,15 45,17 47,20
|
||||||
|
C 50,17 55,15 60,15
|
||||||
|
C 70,15 75,20 75,28
|
||||||
|
C 80,28 85,32 85,38
|
||||||
|
C 85,44 80,48 75,48
|
||||||
|
L 30,48
|
||||||
|
C 27,48 25,45 25,40 Z"
|
||||||
|
fill="url(#caustic-pattern)"
|
||||||
|
class="caustic-layer"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Icono central (rayo mágico) -->
|
||||||
|
<g transform="translate(50, 30)">
|
||||||
|
<path
|
||||||
|
d="M 0,-8 L 2,-2 L 8,-3 L 3,1 L 4,7 L 0,3 L -4,7 L -3,1 L -8,-3 L -2,-2 Z"
|
||||||
|
fill="white"
|
||||||
|
opacity="0.9"
|
||||||
|
class="icono-rayo"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Texto del botón -->
|
||||||
|
<span class="boton-texto">Asignación Rápida</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineEmits<{
|
||||||
|
click: []
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.boton-nube-caustica {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boton-nube-caustica:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.boton-nube-caustica:active {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nube-svg {
|
||||||
|
width: 80px;
|
||||||
|
height: 48px;
|
||||||
|
filter: drop-shadow(0 4px 8px rgba(59, 130, 246, 0.3));
|
||||||
|
transition: filter 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boton-nube-caustica:hover .nube-svg {
|
||||||
|
filter: drop-shadow(0 6px 12px rgba(59, 130, 246, 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animación de la capa cáustica */
|
||||||
|
.caustic-layer {
|
||||||
|
animation: caustic-flow 4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes caustic-flow {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: translateX(0) translateY(0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: translateX(2px) translateY(-1px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: translateX(-1px) translateY(1px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: translateX(1px) translateY(-2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animación del icono */
|
||||||
|
.icono-rayo {
|
||||||
|
animation: sparkle 2s ease-in-out infinite;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sparkle {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: scale(1) rotate(0deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.1) rotate(5deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.boton-texto {
|
||||||
|
font-size: 0.625rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgb(59 130 246);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .boton-texto {
|
||||||
|
color: rgb(147 197 253);
|
||||||
|
}
|
||||||
|
|
||||||
|
.boton-nube-caustica:hover .boton-texto {
|
||||||
|
color: rgb(37 99 235);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .boton-nube-caustica:hover .boton-texto {
|
||||||
|
color: rgb(191 219 254);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Efecto de ondas al hacer hover */
|
||||||
|
.boton-nube-caustica:hover .nube-base {
|
||||||
|
animation: wave-pulse 1s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wave-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modo oscuro - ajustar colores del gradiente */
|
||||||
|
.dark .nube-svg .caustic-stop-1 {
|
||||||
|
stop-color: #3B82F6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .nube-svg .caustic-stop-2 {
|
||||||
|
stop-color: #60A5FA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .nube-svg .caustic-stop-3 {
|
||||||
|
stop-color: #93C5FD;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -70,6 +70,11 @@
|
|||||||
|
|
||||||
<!-- Tab 2: Descriptiva/Afectiva (todos los sliders incluyendo impresión global) -->
|
<!-- Tab 2: Descriptiva/Afectiva (todos los sliders incluyendo impresión global) -->
|
||||||
<div v-if="tabActiva === 'descriptiva-afectiva'" class="tab-content cata-fade-in">
|
<div v-if="tabActiva === 'descriptiva-afectiva'" class="tab-content cata-fade-in">
|
||||||
|
<!-- Botón de asignación rápida -->
|
||||||
|
<div class="flex justify-start mb-4">
|
||||||
|
<CataBotonNubeCaustica @click="modalAsignacionRapida = true" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Sliders de Fragancia -->
|
<!-- Sliders de Fragancia -->
|
||||||
<div v-if="mostrarFraganciaSlider" class="form-section">
|
<div v-if="mostrarFraganciaSlider" class="form-section">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
@@ -796,11 +801,18 @@
|
|||||||
<span class="text-3xl font-bold cata-text">{{ muestra.puntajeFinal }}</span>
|
<span class="text-3xl font-bold cata-text">{{ muestra.puntajeFinal }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs cata-text opacity-60 mt-1">
|
<p class="text-xs cata-text opacity-60 mt-1">
|
||||||
Suma de valores afectivos
|
Suma de valores descriptivos
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de Asignación Rápida -->
|
||||||
|
<CataModalAsignacionRapida
|
||||||
|
v-model="modalAsignacionRapida"
|
||||||
|
:muestra="muestra"
|
||||||
|
@aplicar="aplicarAsignacionRapida"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -937,6 +949,17 @@ const mostrarImpresionGlobalAfectiva = computed(() => deberMostrarSlider('afecti
|
|||||||
// Estado local para otras notas
|
// Estado local para otras notas
|
||||||
const otrasNotasLocal = ref(props.muestra.otrasNotas)
|
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
|
// Actualizar intensidad
|
||||||
const actualizarIntensidad = async (
|
const actualizarIntensidad = async (
|
||||||
parametro: keyof Muestra['intensidades'],
|
parametro: keyof Muestra['intensidades'],
|
||||||
|
|||||||
247
nuxt4/app/components/cata/ModalAsignacionRapida.vue
Normal file
247
nuxt4/app/components/cata/ModalAsignacionRapida.vue
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
<template>
|
||||||
|
<UModal v-model="isOpen" :ui="{ width: 'sm:max-w-md' }">
|
||||||
|
<UCard>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-lg font-semibold">Asignación Rápida de Puntajes</h3>
|
||||||
|
<UButton
|
||||||
|
color="gray"
|
||||||
|
variant="ghost"
|
||||||
|
icon="i-heroicons-x-mark"
|
||||||
|
@click="cerrar"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- Paso 1: Ingresar puntaje total deseado -->
|
||||||
|
<div v-if="paso === 1">
|
||||||
|
<label class="block text-sm font-medium mb-2">
|
||||||
|
Puntaje Total Deseado (8-80)
|
||||||
|
</label>
|
||||||
|
<UInput
|
||||||
|
v-model.number="puntajeDeseado"
|
||||||
|
type="number"
|
||||||
|
:min="8"
|
||||||
|
:max="80"
|
||||||
|
placeholder="Ej: 45"
|
||||||
|
:ui="{ base: 'text-center text-lg' }"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-gray-500 mt-2">
|
||||||
|
Este puntaje se distribuirá entre las 8 categorías descriptivas
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Paso 2: Seleccionar categorías que sobresalen o palidecen -->
|
||||||
|
<div v-if="paso === 2">
|
||||||
|
<p class="text-sm mb-3">
|
||||||
|
<span v-if="multiploMasCercano < puntajeDeseado">
|
||||||
|
Selecciona {{ diferencia }} categoría(s) que <strong>sobresalen</strong> en esta muestra:
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
Selecciona {{ diferencia }} categoría(s) que <strong>palidecen</strong> en esta muestra:
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<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 border-2 transition-all text-left flex items-center gap-2',
|
||||||
|
categoriasSeleccionadas.includes(cat.key)
|
||||||
|
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
|
||||||
|
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300'
|
||||||
|
]"
|
||||||
|
: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>{{ cat.label }}</span>
|
||||||
|
<UIcon
|
||||||
|
v-if="categoriasSeleccionadas.includes(cat.key)"
|
||||||
|
name="i-heroicons-check-circle-solid"
|
||||||
|
class="w-5 h-5 ml-auto text-primary-500"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-xs text-gray-500 mt-3">
|
||||||
|
Seleccionadas: {{ categoriasSeleccionadas.length }} / {{ diferencia }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Resumen -->
|
||||||
|
<div v-if="paso === 2" class="bg-gray-50 dark:bg-gray-800 rounded-md p-3 text-sm">
|
||||||
|
<p class="font-medium mb-1">Distribución:</p>
|
||||||
|
<p>• Puntaje base: {{ puntajeBase }} para todas las categorías</p>
|
||||||
|
<p v-if="multiploMasCercano < puntajeDeseado">
|
||||||
|
• +1 punto a las {{ diferencia }} categoría(s) que sobresalen
|
||||||
|
</p>
|
||||||
|
<p v-else>
|
||||||
|
• -1 punto a las {{ diferencia }} categoría(s) que palidecen
|
||||||
|
</p>
|
||||||
|
<p class="font-semibold mt-2">Total: {{ puntajeDeseado }} puntos</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<UButton
|
||||||
|
color="gray"
|
||||||
|
variant="ghost"
|
||||||
|
@click="cerrar"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</UButton>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
v-if="paso === 1"
|
||||||
|
:disabled="!puntajeValido"
|
||||||
|
@click="siguientePaso"
|
||||||
|
>
|
||||||
|
Continuar
|
||||||
|
</UButton>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
v-if="paso === 2"
|
||||||
|
:disabled="categoriasSeleccionadas.length !== diferencia"
|
||||||
|
@click="aplicarAsignacion"
|
||||||
|
>
|
||||||
|
Aplicar
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</UModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Muestra } 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-wind', color: '#8B7AB8' },
|
||||||
|
{ key: 'aroma', label: 'Aroma', icon: 'i-lucide-nose', color: '#26A69A' },
|
||||||
|
{ key: 'sabor', label: 'Sabor', icon: 'i-lucide-ice-cream-cone', color: '#E53935' },
|
||||||
|
{ key: 'saborResidual', label: 'Sabor Residual', icon: 'i-lucide-timer', color: '#F57C00' },
|
||||||
|
{ key: 'acidez', label: 'Acidez', icon: 'i-lucide-zap', color: '#FDD835' },
|
||||||
|
{ key: 'dulzor', label: 'Dulzor', icon: 'i-lucide-candy', color: '#EC407A' },
|
||||||
|
{ key: 'sensacionBoca', label: 'Sensación en Boca', icon: 'i-lucide-smile', 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 categoriasSeleccionadas = ref<string[]>([])
|
||||||
|
|
||||||
|
// Cálculos
|
||||||
|
const puntajeValido = computed(() => {
|
||||||
|
return puntajeDeseado.value >= 8 && puntajeDeseado.value <= 80
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
// Resetear estado
|
||||||
|
setTimeout(() => {
|
||||||
|
paso.value = 1
|
||||||
|
puntajeDeseado.value = 40
|
||||||
|
categoriasSeleccionadas.value = []
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user