Feat: Reemplazar UCheckboxGroup de ubicaciones por InputMenu estilo clientes
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 49s
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:
103
nuxt4-app/app/components/UbicacionMultiSelector.vue
Normal file
103
nuxt4-app/app/components/UbicacionMultiSelector.vue
Normal 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>
|
||||
@@ -70,23 +70,17 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- Ubicaciones -->
|
||||
<div>
|
||||
<UCheckboxGroup
|
||||
v-model="selectedUbicaciones"
|
||||
legend="Ubicaciones"
|
||||
:items="opcionesFiltros.ubicaciones"
|
||||
color="primary"
|
||||
variant="list"
|
||||
orientation="vertical"
|
||||
size="sm"
|
||||
:ui="{
|
||||
fieldset: 'max-h-40 overflow-y-auto',
|
||||
container: 'hover:bg-[#c08040]/5 transition-colors duration-200 rounded-md px-2 -mx-2'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<!-- Selector de Ubicaciones -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2 text-[var(--brand-text)]">Ubicaciones</label>
|
||||
<UbicacionMultiSelector
|
||||
:selected-ubicaciones="selectedUbicaciones"
|
||||
:ubicaciones="opcionesFiltros.ubicaciones"
|
||||
@update:selected-ubicaciones="selectedUbicaciones = $event"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
<!-- Tipos de Café -->
|
||||
<div>
|
||||
@@ -253,23 +247,17 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- Ubicaciones -->
|
||||
<div>
|
||||
<UCheckboxGroup
|
||||
v-model="selectedUbicaciones"
|
||||
legend="Ubicaciones"
|
||||
:items="opcionesFiltros.ubicaciones"
|
||||
color="primary"
|
||||
variant="list"
|
||||
orientation="vertical"
|
||||
size="sm"
|
||||
:ui="{
|
||||
fieldset: 'max-h-40 overflow-y-auto',
|
||||
container: 'hover:bg-[#c08040]/5 transition-colors duration-200 rounded-md px-2 -mx-2'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<!-- Selector de Ubicaciones -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2 text-[var(--brand-text)]">Ubicaciones</label>
|
||||
<UbicacionMultiSelector
|
||||
:selected-ubicaciones="selectedUbicaciones"
|
||||
:ubicaciones="opcionesFiltros.ubicaciones"
|
||||
@update:selected-ubicaciones="selectedUbicaciones = $event"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
<!-- Tipos de Café -->
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user