Feat: Agregar calculadora SCAA en página principal
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m6s

Implementado flujo de asignación rápida reutilizable en modo calculadora
para la página principal, permitiendo calcular SCAA Score sin sesión.

Cambios:
- ModalAsignacionRapida: Nuevo modo 'calculadora' con paso 3
- Paso 3 incluye ResumenMuestra y penalizaciones configurables
- Función generadora de muestra genérica para vista previa
- Botón "Calculadora SCAA" en página principal
- Navegación mejorada entre pasos (Volver/Continuar)
This commit is contained in:
2025-10-19 03:35:20 -06:00
parent 8b03a59525
commit 4985fc8b88
2 changed files with 209 additions and 10 deletions

View File

@@ -15,7 +15,7 @@
<!-- Título personalizado -->
<template #title>
<span class="cata-text">Asignación Rápida de Puntajes</span>
<span class="cata-text">{{ tituloModal }}</span>
</template>
<!-- Contenido del modal -->
@@ -106,6 +106,72 @@
</button>
</div>
</div>
<!-- Paso 3: Vista previa con ResumenMuestra (solo en modo calculadora) -->
<div v-if="paso === 3">
<div class="space-y-4">
<!-- Inputs de penalizaciones -->
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs cata-text opacity-75 mb-1">
Tazas No Uniformes
</label>
<UInputNumber
v-model="tazasNoUniformes"
:min="0"
:max="5"
:step="1"
placeholder="0-5"
class="w-full cata-input-number"
variant="outline"
:ui="{
base: 'cata-input text-center',
}"
/>
<p class="text-xs cata-text opacity-60 mt-1 text-center">
-2 puntos cada una
</p>
</div>
<div>
<label class="block text-xs cata-text opacity-75 mb-1">
Tazas Defectuosas
</label>
<UInputNumber
v-model="tazasDefectuosas"
:min="0"
:max="5"
:step="1"
placeholder="0-5"
class="w-full cata-input-number"
variant="outline"
:ui="{
base: 'cata-input text-center',
}"
/>
<p class="text-xs cata-text opacity-60 mt-1 text-center">
-4 puntos cada una
</p>
</div>
</div>
<!-- Resumen de la muestra -->
<div class="mt-6">
<h4 class="text-sm font-semibold cata-text mb-2 opacity-75">
Vista Previa
</h4>
<div class="cata-outline-box p-4 rounded-lg">
<ResumenMuestra :muestra="muestraGenerica" tab-activa="impresion-global" />
</div>
</div>
<!-- Información adicional -->
<div class="text-xs cata-text opacity-60 text-center">
<p>Esta es una muestra de ejemplo para calcular el SCAA Score</p>
<p>Los valores descriptivos y organolépticos están ocultos</p>
</div>
</div>
</div>
</div>
</template>
@@ -113,21 +179,33 @@
<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">
<div v-if="paso === 2 || paso === 3" 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>
<span>Σ {{ puntajeDeseado }}</span>
</div>
<div v-else></div>
<!-- Botones a la derecha -->
<div class="flex items-center gap-2">
<!-- Botón Volver (pasos 2 y 3) -->
<button
v-if="paso === 2 || paso === 3"
class="cata-button px-4 py-2"
@click="pasoAnterior"
>
Volver
</button>
<!-- Botón Cancelar (solo paso 1) -->
<button
v-if="paso === 1"
class="cata-button px-4 py-2"
@click="cerrar"
>
Cancelar
</button>
<!-- Botón Continuar (paso 1) -->
<button
v-if="paso === 1"
class="cata-button px-4 py-2"
@@ -138,14 +216,24 @@
Continuar
</button>
<!-- Botón Aplicar/Continuar (paso 2) -->
<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"
@click="siguientePasoOAplicar"
>
Aplicar
{{ modo === 'calculadora' ? 'Continuar' : 'Aplicar' }}
</button>
<!-- Botón Cerrar (paso 3 - solo modo calculadora) -->
<button
v-if="paso === 3"
class="cata-button px-4 py-2"
@click="cerrar"
>
Cerrar
</button>
</div>
</div>
@@ -154,15 +242,18 @@
</template>
<script setup lang="ts">
import type { Muestra } from '~/types/catacion'
import type { Muestra, IntensidadValor } from '~/types/catacion'
import { scaaASumatoriaAfectiva, sumatoriaAfectivaASCAA, redondearA025 } from '~/types/catacion'
interface Props {
modelValue: boolean
muestra: Muestra
muestra?: Muestra
modo?: 'calculadora' | 'asignacion'
}
const props = defineProps<Props>()
const props = withDefaults(defineProps<Props>(), {
modo: 'asignacion',
})
const emit = defineEmits<{
'update:modelValue': [value: boolean]
@@ -193,9 +284,18 @@ const puntajeDeseado = ref<number>(40)
const scaaDeseado = ref<number>(sumatoriaAfectivaASCAA(40))
const categoriasSeleccionadas = ref<string[]>([])
// Estado de penalizaciones (paso 3)
const tazasNoUniformes = ref<number>(0)
const tazasDefectuosas = ref<number>(0)
// Flag para evitar loops infinitos en la sincronización
const actualizandoInput = ref(false)
// Título del modal según el modo
const tituloModal = computed(() => {
return props.modo === 'calculadora' ? 'Calculadora SCAA Score' : 'Asignación Rápida de Puntajes'
})
// 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
@@ -209,6 +309,60 @@ watch(isOpen, (newValue) => {
puntajeDeseado.value = 40
scaaDeseado.value = sumatoriaAfectivaASCAA(40)
categoriasSeleccionadas.value = []
tazasNoUniformes.value = 0
tazasDefectuosas.value = 0
}
})
// Crear muestra genérica para vista previa (modo calculadora)
const muestraGenerica = computed<Muestra>(() => {
// Primero calculamos los puntajes afectivos
const puntajes: Record<string, number> = {}
const puntajeBase = multiploMasCercano.value / 8
const ajuste = multiploMasCercano.value < puntajeDeseado.value ? 1 : -1
categoriasDisponibles.forEach(cat => {
const esSeleccionada = categoriasSeleccionadas.value.includes(cat.key)
puntajes[cat.key] = puntajeBase + (esSeleccionada ? ajuste : 0)
})
// Crear objeto de intensidades
const intensidades: Muestra['intensidades'] = {
fragancia: { descriptiva: null, afectiva: puntajes.fragancia || puntajeBase },
aroma: { descriptiva: null, afectiva: puntajes.aroma || puntajeBase },
sabor: { descriptiva: null, afectiva: puntajes.sabor || puntajeBase },
saborResidual: { descriptiva: null, afectiva: puntajes.saborResidual || puntajeBase },
acidez: { descriptiva: null, afectiva: puntajes.acidez || puntajeBase },
dulzor: { descriptiva: null, afectiva: puntajes.dulzor || puntajeBase },
sensacionBoca: { descriptiva: null, afectiva: puntajes.sensacionBoca || puntajeBase },
impresionGlobal: { descriptiva: null, afectiva: puntajes.impresionGlobal || puntajeBase },
}
// Crear arrays de tazas con defectos según las cantidades
const tazasNoUniformesArray: number[] = []
const tazasDefectuosasArray: number[] = []
for (let i = 1; i <= tazasNoUniformes.value; i++) {
tazasNoUniformesArray.push(i)
}
for (let i = 1; i <= tazasDefectuosas.value; i++) {
tazasDefectuosasArray.push(i)
}
return {
muestraId: 1,
nombre: 'Muestra de Ejemplo',
intensidades,
fraganciaAromaNotas: { categorias: [], subcategorias: [], notaEspecifica: null },
saborNotas: { categorias: [], subcategorias: [], notaEspecifica: null },
tazasNoUniformes: tazasNoUniformesArray,
tazasDefectuosas: tazasDefectuosasArray,
defecto: null,
sensacionEnBoca: null,
gustosPredominantes: [],
otrasNotas: '',
puntajeFinal: 0,
}
})
@@ -345,14 +499,39 @@ const diferencia = computed(() => {
const siguientePaso = () => {
if (!puntajeValido.value) return
// Si el múltiplo es exacto (diferencia = 0), aplicar directamente
// Si el múltiplo es exacto (diferencia = 0)
if (diferencia.value === 0) {
aplicarAsignacionDirecta()
if (props.modo === 'calculadora') {
// En modo calculadora, ir directo al paso 3
paso.value = 3
} else {
// En modo asignación, aplicar directamente
aplicarAsignacionDirecta()
}
} else {
// Hay diferencia, ir al paso 2
paso.value = 2
}
}
const pasoAnterior = () => {
if (paso.value > 1) {
paso.value--
}
}
const siguientePasoOAplicar = () => {
if (categoriasSeleccionadas.value.length !== diferencia.value) return
if (props.modo === 'calculadora') {
// En modo calculadora, ir al paso 3
paso.value = 3
} else {
// En modo asignación, aplicar
aplicarAsignacion()
}
}
const toggleCategoria = (key: string) => {
const index = categoriasSeleccionadas.value.indexOf(key)
if (index >= 0) {

View File

@@ -22,6 +22,20 @@
Sistema de Catación de Café
</p>
<hr class="cata-divider my-6 max-w-xs mx-auto">
<!-- Botón Calculadora SCAA -->
<div class="mt-6">
<button
class="cata-button px-6 py-3"
@click="mostrarCalculadora = true"
>
<UIcon name="i-lucide-calculator" class="w-5 h-5 inline mr-2" />
Calculadora SCAA
</button>
<p class="text-xs cata-text opacity-60 mt-2">
Calcula el SCAA Score sin iniciar sesión
</p>
</div>
</div>
<!-- Loading State -->
@@ -194,6 +208,11 @@
</div>
</div>
<!-- Modal Calculadora SCAA -->
<CataModalAsignacionRapida
v-model="mostrarCalculadora"
modo="calculadora"
/>
</div>
</div>
</template>
@@ -215,6 +234,7 @@ const { inicializar, tieneSecion } = useIndexedDB()
const mostrarFormulario = ref(false)
const mostrarDialogoNueva = ref(false)
const creandoSesion = ref(false)
const mostrarCalculadora = ref(false)
const formData = reactive({
catador: '',
cantidadMuestras: 5,