Feat: Reorganizar tabs y permitir selección múltiple de categorías en notas
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m1s

- **Nuevas tabs reorganizadas:**
  - Organoléptica: Selectores de familia de fragancia-aroma y sabor
  - Descriptiva/Afectiva: Todos los sliders de intensidad (incluye impresión global)
  - Defectos: Tazas no uniformes, defectuosas y tipo de defecto
  - Impresión Global: Vista completa con todos los componentes

- **Selector de categorías mejorado:**
  - Permitir selección múltiple de categorías padre
  - Las subcategorías son la unión de las subcategorías de los padres seleccionados
  - Permitir selección múltiple de subcategorías
  - Actualizar resumen visual de selección

- **Tipos actualizados:**
  - NotaSeleccionada ahora usa arrays para categorias y subcategorias
  - TabCatacion actualizado con las nuevas tabs
  - Funciones de actualización modificadas para trabajar con arrays

- **Correcciones TypeScript:**
  - Usar JSON.parse(JSON.stringify()) para crear copias mutables de arrays readonly
  - Resolver incompatibilidades de tipos entre readonly y mutable arrays
This commit is contained in:
2025-10-18 02:57:14 -06:00
parent 1c4f09d9bd
commit 48e0d2f7dc
5 changed files with 481 additions and 181 deletions

View File

@@ -6,9 +6,9 @@
<span v-if="required" class="text-error">*</span>
</label>
<!-- Nivel 1: Categorías principales -->
<!-- Nivel 1: Categorías principales (selección múltiple) -->
<div class="nivel-container">
<h4 class="nivel-title cata-text">Categoría</h4>
<h4 class="nivel-title cata-text">Categorías (selección múltiple)</h4>
<div class="categorias-grid">
<button
v-for="categoria in categoriasDisponibles"
@@ -18,16 +18,16 @@
'categoria-item',
'cata-checkbox',
{
'cata-checkbox-checked': modelValue.categoria === categoria,
'cata-checkbox-checked': modelValue.categorias.includes(categoria),
'disabled': disabled,
},
]"
:disabled="disabled"
@click="seleccionarCategoria(categoria)"
@click="toggleCategoria(categoria)"
>
<span class="categoria-text cata-text">{{ categoria }}</span>
<UIcon
v-if="modelValue.categoria === categoria"
v-if="modelValue.categorias.includes(categoria)"
name="i-lucide-check-circle"
class="categoria-check"
/>
@@ -35,9 +35,9 @@
</div>
</div>
<!-- Nivel 2: Subcategorías (si aplica) -->
<!-- Nivel 2: Subcategorías (unión de todas las categorías seleccionadas, selección múltiple) -->
<div v-if="subcategoriasDisponibles.length > 0" class="nivel-container mt-4">
<h4 class="nivel-title cata-text">Subcategoría</h4>
<h4 class="nivel-title cata-text">Subcategorías (selección múltiple)</h4>
<div class="subcategorias-grid">
<button
v-for="subcategoria in subcategoriasDisponibles"
@@ -47,16 +47,16 @@
'subcategoria-item',
'cata-checkbox',
{
'cata-checkbox-checked': modelValue.subcategoria === subcategoria,
'cata-checkbox-checked': modelValue.subcategorias.includes(subcategoria),
'disabled': disabled,
},
]"
:disabled="disabled"
@click="seleccionarSubcategoria(subcategoria)"
@click="toggleSubcategoria(subcategoria)"
>
<span class="subcategoria-text cata-text">{{ subcategoria }}</span>
<UIcon
v-if="modelValue.subcategoria === subcategoria"
v-if="modelValue.subcategorias.includes(subcategoria)"
name="i-lucide-check"
class="subcategoria-check"
/>
@@ -65,7 +65,7 @@
</div>
<!-- Nivel 3: Nota específica (input libre) -->
<div v-if="modelValue.categoria" class="nivel-container mt-4">
<div v-if="modelValue.categorias.length > 0" class="nivel-container mt-4">
<h4 class="nivel-title cata-text">Nota Específica</h4>
<input
v-model="notaEspecificaLocal"
@@ -81,13 +81,13 @@
<div v-if="seleccionCompleta" class="mt-4 p-3 cata-outline-box rounded-md">
<p class="text-xs font-semibold cata-text mb-1">Selección actual:</p>
<p class="text-sm cata-text">
{{ modelValue.categoria }}
<template v-if="modelValue.subcategoria">
{{ modelValue.subcategoria }}
</template>
<template v-if="modelValue.notaEspecifica">
<span class="font-semibold">{{ modelValue.notaEspecifica }}</span>
</template>
<span class="font-semibold">Categorías:</span> {{ modelValue.categorias.join(', ') }}
</p>
<p v-if="modelValue.subcategorias.length > 0" class="text-sm cata-text mt-1">
<span class="font-semibold">Subcategorías:</span> {{ modelValue.subcategorias.join(', ') }}
</p>
<p v-if="modelValue.notaEspecifica" class="text-sm cata-text mt-1">
<span class="font-semibold">Nota:</span> {{ modelValue.notaEspecifica }}
</p>
</div>
</div>
@@ -134,62 +134,94 @@ const categoriasDisponibles = computed<CategoriaNotaPrincipal[]>(() => {
return Object.keys(FAMILIAS_NOTAS_ESTRUCTURA) as CategoriaNotaPrincipal[]
})
// Subcategorías disponibles según la categoría seleccionada
// Subcategorías disponibles (unión de todas las categorías seleccionadas)
const subcategoriasDisponibles = computed<string[]>(() => {
if (!props.modelValue.categoria) return []
if (props.modelValue.categorias.length === 0) return []
const familia = FAMILIAS_NOTAS_ESTRUCTURA[props.modelValue.categoria as CategoriaNotaPrincipal]
if (!familia || typeof familia !== 'object') return []
const subcategoriasSet = new Set<string>()
return Object.keys(familia)
// Iterar sobre cada categoría seleccionada y agregar sus subcategorías
props.modelValue.categorias.forEach((categoria) => {
const familia = FAMILIAS_NOTAS_ESTRUCTURA[categoria as CategoriaNotaPrincipal]
if (familia && typeof familia === 'object') {
Object.keys(familia).forEach((subcategoria) => {
subcategoriasSet.add(subcategoria)
})
}
})
return Array.from(subcategoriasSet).sort()
})
// Verifica si la selección está completa
const seleccionCompleta = computed(() => {
return props.modelValue.categoria !== null
return props.modelValue.categorias.length > 0
})
// Seleccionar categoría
const seleccionarCategoria = (categoria: CategoriaNotaPrincipal) => {
// Toggle categoría (agregar o quitar)
const toggleCategoria = (categoria: CategoriaNotaPrincipal) => {
if (props.disabled) return
// Si se selecciona la misma categoría, deseleccionar todo
if (props.modelValue.categoria === categoria) {
emit('update:modelValue', {
categoria: null,
subcategoria: null,
notaEspecifica: null,
})
notaEspecificaLocal.value = ''
return
const categorias = [...props.modelValue.categorias]
const index = categorias.indexOf(categoria)
if (index > -1) {
// Quitar categoría
categorias.splice(index, 1)
// Si ya no hay categorías, limpiar subcategorías también
if (categorias.length === 0) {
emit('update:modelValue', {
categorias: [],
subcategorias: [],
notaEspecifica: null,
})
notaEspecificaLocal.value = ''
return
}
} else {
// Agregar categoría
categorias.push(categoria)
}
// Seleccionar nueva categoría y resetear subcategoría y nota
emit('update:modelValue', {
categoria,
subcategoria: null,
notaEspecifica: null,
// Filtrar subcategorías: mantener solo las que siguen siendo válidas
const subcategoriasValidas = new Set<string>()
categorias.forEach((cat) => {
const familia = FAMILIAS_NOTAS_ESTRUCTURA[cat as CategoriaNotaPrincipal]
if (familia && typeof familia === 'object') {
Object.keys(familia).forEach((sub) => subcategoriasValidas.add(sub))
}
})
notaEspecificaLocal.value = ''
}
// Seleccionar subcategoría
const seleccionarSubcategoria = (subcategoria: string) => {
if (props.disabled) return
const subcategoriasFiltradas = props.modelValue.subcategorias.filter((sub) =>
subcategoriasValidas.has(sub)
)
// Si se selecciona la misma subcategoría, deseleccionar
if (props.modelValue.subcategoria === subcategoria) {
emit('update:modelValue', {
...props.modelValue,
subcategoria: null,
})
return
}
// Seleccionar nueva subcategoría
emit('update:modelValue', {
...props.modelValue,
subcategoria,
categorias,
subcategorias: subcategoriasFiltradas,
})
}
// Toggle subcategoría (agregar o quitar)
const toggleSubcategoria = (subcategoria: string) => {
if (props.disabled) return
const subcategorias = [...props.modelValue.subcategorias]
const index = subcategorias.indexOf(subcategoria)
if (index > -1) {
// Quitar subcategoría
subcategorias.splice(index, 1)
} else {
// Agregar subcategoría
subcategorias.push(subcategoria)
}
emit('update:modelValue', {
...props.modelValue,
subcategorias,
})
}