panels-origin/web-panel/components/BudgetItemPicker.vue
Cursor Agent 7a31453dfd
fix: overlay del buscador de concepto acotado al campo
El AutoComplete se estiraba con las descripciones largas y tapaba el diálogo. La lista queda al ancho del input, con clave + texto envuelto.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 20:11:56 +00:00

121 lines
3.4 KiB
Vue

<template>
<AutoComplete
v-model="selected"
:suggestions="filtered"
option-label="label"
placeholder="Buscar concepto del presupuesto..."
class="budget-item-picker w-full"
input-class="w-full"
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"
>
<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>
</template>
<script setup lang="ts">
const props = defineProps<{
projectId: number;
modelValue?: number | null;
}>();
const emit = defineEmits<{ "update:modelValue": [number | null]; select: [BudgetOption | null] }>();
type BudgetOption = {
id: number;
code: string;
description: string;
unit: string;
amount: number;
label: string;
};
const { api } = useApi();
const items = ref<BudgetOption[]>([]);
const selected = ref<BudgetOption | null>(null);
const filtered = ref<BudgetOption[]>([]);
async function load() {
if (!props.projectId) {
items.value = [];
return;
}
const r = await api<{ items?: Array<Record<string, unknown>> }>(
`/v1/projects/${props.projectId}/budget`,
);
const raw = r.items ?? [];
items.value = raw.map((row) => ({
id: Number(row.id),
code: String(row.code ?? ""),
description: String(row.description ?? ""),
unit: String(row.unit ?? ""),
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;
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>