Feat: Reemplazar UCheckboxGroup de ubicaciones por InputMenu estilo clientes
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 49s

Nuevo componente creado:
- UbicacionMultiSelector.vue: InputMenu con tema personalizado
- Mismo estilo y funcionalidad que ClienteMultiSelector
- Búsqueda en tiempo real de ubicaciones
- Tags visuales para selección múltiple
- Contador y botón "Limpiar todo"

Cambios en informe-ingresos.vue:
- Reemplazar UCheckboxGroup de ubicaciones por UbicacionMultiSelector
- Mover selector de ubicaciones a su propia sección (fuera del grid)
- Grid ahora tiene 2 columnas (Tipos y Estados) en lugar de 3
- Mantener layout consistente con selector de clientes

Estilos aplicados (igual que ClienteMultiSelector):
- Fondo: --brand-surface
- Bordes: --brand-border con focus dorado
- Item highlighted: tono dorado suave
- Tags: color --brand-primary

Ahora ubicaciones tiene la misma UX moderna que clientes.
This commit is contained in:
2025-10-30 14:07:04 -06:00
parent a7e585a3de
commit a58f0b26ed
2 changed files with 125 additions and 34 deletions

View File

@@ -0,0 +1,103 @@
<template>
<div class="space-y-3">
<!-- InputMenu for ubicacion search and selection -->
<UInputMenu
:model-value="selectedUbicacionesObjects"
:items="filteredItems"
multiple
label-key="label"
value-key="value"
icon="i-lucide-map-pin"
placeholder="Buscar ubicaciones..."
size="sm"
ignore-filter
:ui="{
root: 'focus-within:ring-1 focus-within:ring-[var(--brand-primary)] transition-shadow',
base: 'bg-[var(--brand-surface)] text-[var(--brand-text)] border border-[var(--brand-border)] focus:ring-1 focus:ring-[var(--brand-primary)] focus:border-[var(--brand-primary)]',
placeholder: 'placeholder-[var(--brand-text-muted)]',
leadingIcon: 'text-[var(--brand-text-muted)]',
content: 'bg-[var(--brand-surface)] border border-[var(--brand-border)]',
item: 'text-[var(--brand-text)] data-highlighted:bg-[rgba(224,192,128,0.12)] data-highlighted:text-[var(--brand-text)]',
itemLeadingIcon: 'text-[var(--brand-text-muted)]',
tagsItem: 'bg-[rgba(224,192,128,0.14)] border border-[rgba(224,192,128,0.28)] text-[var(--brand-primary)]',
tagsItemText: 'text-[var(--brand-primary)]',
tagsItemDelete: 'text-[var(--brand-text-muted)] hover:text-[var(--brand-primary)] hover:bg-[rgba(224,192,128,0.2)]'
}"
@update:model-value="onSelectionChange"
@update:search-term="searchQuery = $event"
>
<template #empty>
<div class="text-center py-2">
<span class="text-[var(--brand-text-muted)] text-sm">
No se encontraron ubicaciones
</span>
</div>
</template>
</UInputMenu>
<!-- Selected count and clear all -->
<div v-if="selectedUbicaciones.length > 0" class="flex items-center justify-between text-sm">
<span class="text-[var(--brand-text-muted)]">
{{ selectedUbicaciones.length }} ubicación{{ selectedUbicaciones.length !== 1 ? 'es' : '' }} seleccionada{{ selectedUbicaciones.length !== 1 ? 's' : '' }}
</span>
<UButton
size="xs"
color="gray"
variant="link"
@click="clearAll"
>
Limpiar todo
</UButton>
</div>
</div>
</template>
<script setup lang="ts">
import type { InputMenuItem } from '@nuxt/ui'
interface UbicacionItem {
label: string
value: string
}
const props = defineProps<{
selectedUbicaciones: string[]
ubicaciones: UbicacionItem[]
}>()
const emit = defineEmits<{
'update:selectedUbicaciones': [value: string[]]
}>()
// State
const searchQuery = ref('')
// Computed - Get selected ubicaciones as objects for InputMenu
const selectedUbicacionesObjects = computed(() => {
return props.ubicaciones.filter(u => props.selectedUbicaciones.includes(u.value))
})
// Computed - Filter items based on search
const filteredItems = computed((): InputMenuItem[] => {
const query = searchQuery.value.trim().toLowerCase()
if (!query) {
return props.ubicaciones
}
return props.ubicaciones.filter(u =>
u.label.toLowerCase().includes(query)
)
})
// Methods
function onSelectionChange(value: any[]) {
// Extract values from selected items
const values = value.map(item => item.value || item)
emit('update:selectedUbicaciones', values)
}
function clearAll() {
emit('update:selectedUbicaciones', [])
}
</script>

View File

@@ -70,23 +70,17 @@
/> />
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <!-- Selector de Ubicaciones -->
<!-- Ubicaciones --> <div>
<div> <label class="block text-sm font-medium mb-2 text-[var(--brand-text)]">Ubicaciones</label>
<UCheckboxGroup <UbicacionMultiSelector
v-model="selectedUbicaciones" :selected-ubicaciones="selectedUbicaciones"
legend="Ubicaciones" :ubicaciones="opcionesFiltros.ubicaciones"
:items="opcionesFiltros.ubicaciones" @update:selected-ubicaciones="selectedUbicaciones = $event"
color="primary" />
variant="list" </div>
orientation="vertical"
size="sm" <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
:ui="{
fieldset: 'max-h-40 overflow-y-auto',
container: 'hover:bg-[#c08040]/5 transition-colors duration-200 rounded-md px-2 -mx-2'
}"
/>
</div>
<!-- Tipos de Café --> <!-- Tipos de Café -->
<div> <div>
@@ -253,23 +247,17 @@
/> />
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <!-- Selector de Ubicaciones -->
<!-- Ubicaciones --> <div>
<div> <label class="block text-sm font-medium mb-2 text-[var(--brand-text)]">Ubicaciones</label>
<UCheckboxGroup <UbicacionMultiSelector
v-model="selectedUbicaciones" :selected-ubicaciones="selectedUbicaciones"
legend="Ubicaciones" :ubicaciones="opcionesFiltros.ubicaciones"
:items="opcionesFiltros.ubicaciones" @update:selected-ubicaciones="selectedUbicaciones = $event"
color="primary" />
variant="list" </div>
orientation="vertical"
size="sm" <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
:ui="{
fieldset: 'max-h-40 overflow-y-auto',
container: 'hover:bg-[#c08040]/5 transition-colors duration-200 rounded-md px-2 -mx-2'
}"
/>
</div>
<!-- Tipos de Café --> <!-- Tipos de Café -->
<div> <div>