All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m6s
CAMBIOS CRÍTICOS EN ESCALAS: - Escala descriptiva: 1-10 → 1-15 - Escala afectiva: 1-15 → 1-9 - Puntaje final: suma de afectivos (no descriptivos) - Rango puntaje final: 9-90 (8 categorías × 1-9 afectivo) Iconos de selección en SelectorFamilia: - Fragancia/Aroma: i-heroicons-check-circle-solid - Sabor: i-heroicons-check-badge-solid - Eliminar textos de títulos para interfaz más limpia Actualizar calcularPuntajeFinal (catacion.ts): - Cambiar suma de descriptivos a afectivos - Actualizar comentarios JSDoc Actualizar SelectorIntensidad: - Máximo descriptiva: 10 → 15 - Máximo afectiva: 15 → 9 - Actualizar comentarios y tipos Actualizar ModalAsignacionRapida: - Rango de entrada: 8-80 → 9-90 - Modificar valores afectivos (no descriptivos) - Valor por defecto: 40 → 45 - Actualizar validaciones de rango Actualizar FormularioMuestra: - aplicarAsignacionRapida usa tipo 'afectiva' - Texto: "Suma de valores afectivos" IMPORTANTE: Estos cambios afectan toda la lógica de puntuación
273 lines
8.2 KiB
Vue
273 lines
8.2 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">
|
|
<input
|
|
v-model.number="puntajeDeseado"
|
|
type="number"
|
|
:min="9"
|
|
:max="90"
|
|
placeholder="Puntaje Total (9-90)"
|
|
class="cata-input w-full px-3 py-2 rounded-md text-center text-lg"
|
|
/>
|
|
</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'
|
|
|
|
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 categoriasSeleccionadas = ref<string[]>([])
|
|
|
|
// Resetear estado cuando el modal se abre
|
|
watch(isOpen, (newValue) => {
|
|
if (newValue) {
|
|
paso.value = 1
|
|
puntajeDeseado.value = 45
|
|
categoriasSeleccionadas.value = []
|
|
}
|
|
})
|
|
|
|
// 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>
|