fix: concepto de presupuesto con Select filtrable de PrimeVue

El buscador pasa a Select con filtro, el mismo patrón que proveedor y unidad, para que overlay y tema coincidan con el resto del formulario.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
This commit is contained in:
Cursor Agent 2026-09-08 20:13:06 +00:00
parent 7a31453dfd
commit 4b5d9572ee
No known key found for this signature in database
2 changed files with 42 additions and 64 deletions

View file

@ -2166,7 +2166,8 @@ html.dark .p-select-label.p-placeholder {
html.dark .p-select-overlay,
html.dark .p-datepicker-panel,
html.dark .p-multiselect-overlay {
html.dark .p-multiselect-overlay,
html.dark .p-autocomplete-overlay {
background: #111b2e !important;
border-color: #243044 !important;
color: #e2e8f0 !important;
@ -2238,11 +2239,13 @@ html.dark .p-dialog .p-dialog-header-icon {
.cost-critical { background: #ffebee !important; color: #b42318 !important; }
.cost-critical-text { color: #b42318; font-weight: 600; }
.budget-item-picker-overlay.p-select-overlay,
.budget-item-picker-overlay.p-autocomplete-overlay,
.budget-item-picker-overlay {
overflow-x: hidden;
box-sizing: border-box;
}
.budget-item-picker-overlay .p-select-list,
.budget-item-picker-overlay .p-autocomplete-list,
.budget-item-picker-overlay ul {
width: 100%;
@ -2250,12 +2253,26 @@ html.dark .p-dialog .p-dialog-header-icon {
padding: 4px 0;
list-style: none;
}
.budget-item-picker-overlay .p-select-option,
.budget-item-picker-overlay .p-autocomplete-option,
.budget-item-picker-overlay li {
white-space: normal;
overflow-wrap: anywhere;
align-items: flex-start;
}
.budget-item-picker .budget-item-value {
display: flex;
align-items: baseline;
gap: 8px;
min-width: 0;
max-width: 100%;
}
.budget-item-picker .budget-item-value-desc {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #5b6573;
}
.budget-item-option {
display: flex;
flex-direction: column;

View file

@ -1,32 +1,35 @@
<template>
<AutoComplete
v-model="selected"
:suggestions="filtered"
<Select
:model-value="modelValue ?? null"
:options="items"
option-label="label"
option-value="id"
placeholder="Buscar concepto del presupuesto..."
class="budget-item-picker w-full"
input-class="w-full"
filter
filter-placeholder="Clave o descripción"
show-clear
fluid
class="w-full budget-item-picker"
overlay-class="budget-item-picker-overlay"
panel-class="budget-item-picker-overlay"
dropdown
fluid
force-selection
complete-on-focus
scroll-height="16rem"
empty-search-message="Sin conceptos que coincidan"
@complete="onSearch"
@item-select="onSelect"
@option-select="onSelect"
@clear="onClear"
@show="alignOverlay"
empty-filter-message="Sin conceptos que coincidan"
empty-message="Esta obra no tiene partidas cargadas"
@update:model-value="onUpdate"
>
<template #value="{ value, placeholder }">
<span v-if="selected" class="budget-item-value">
<span class="identifier">{{ selected.code || "—" }}</span>
<span class="budget-item-value-desc">{{ selected.description }}</span>
</span>
<span v-else class="p-select-placeholder">{{ placeholder }}</span>
</template>
<template #option="{ option }">
<div class="budget-item-option">
<span class="identifier">{{ option.code || "—" }}</span>
<span class="budget-item-option-desc">{{ option.description }}</span>
</div>
</template>
</AutoComplete>
</Select>
</template>
<script setup lang="ts">
@ -47,8 +50,8 @@ type BudgetOption = {
const { api } = useApi();
const items = ref<BudgetOption[]>([]);
const selected = ref<BudgetOption | null>(null);
const filtered = ref<BudgetOption[]>([]);
const selected = computed(() => items.value.find((i) => i.id === props.modelValue) ?? null);
async function load() {
if (!props.projectId) {
@ -67,55 +70,13 @@ async function load() {
amount: Number(row.amount ?? 0),
label: [row.code, row.description].filter(Boolean).join(" — "),
}));
filtered.value = items.value.slice(0, 30);
if (props.modelValue) {
selected.value = items.value.find((i) => i.id === props.modelValue) ?? null;
}
}
function onSearch(event: { query: string }) {
const q = (event.query || "").toLowerCase().trim();
filtered.value = items.value
.filter((i) => !q || i.label.toLowerCase().includes(q) || i.code.toLowerCase().includes(q))
.slice(0, 30);
}
function onSelect(event: { value?: BudgetOption; option?: BudgetOption }) {
const option = event.value ?? event.option ?? null;
function onUpdate(id: number | null) {
const option = id == null ? null : items.value.find((i) => i.id === id) ?? null;
emit("update:modelValue", option?.id ?? null);
emit("select", option);
}
function onClear() {
emit("update:modelValue", null);
emit("select", null);
}
function alignOverlay() {
nextTick(() => {
const input = document.querySelector(".budget-item-picker input") as HTMLElement | null;
const overlay = document.querySelector(".budget-item-picker-overlay") as HTMLElement | null;
if (!input || !overlay) return;
const width = Math.round(input.getBoundingClientRect().width);
overlay.style.width = `${width}px`;
overlay.style.minWidth = `${width}px`;
overlay.style.maxWidth = `${width}px`;
});
}
watch(() => props.projectId, load, { immediate: true });
watch(
() => props.modelValue,
(id) => {
if (!id) selected.value = null;
else selected.value = items.value.find((i) => i.id === id) ?? null;
},
);
</script>
<style scoped>
.budget-item-picker :deep(.p-autocomplete),
.budget-item-picker :deep(.p-autocomplete-input) {
width: 100%;
}
</style>