mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 11:23:18 +00:00
Implementa el módulo integral de costos según el plan acordado: - Esquema Liquibase: expense_entries, almacén (central + obra), IVA, tenant_cost_settings, permisos IAM y RLS - Funciones RPC core: gastos, almacén, control presupuestal, sync nómina, cierre de almacén al concluir proyecto - API HTTP: expenses_http, warehouse_http, cost_control_http, iam_http - Middleware requirePermission con matriz IAM - UI web-panel: /gastos, /almacen, /control-presupuesto, /usuarios - Componentes BudgetItemPicker, CostSemaphore y semáforos CSS - Smoke tests extendidos para gastos/almacén/control Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
73 lines
1.9 KiB
Vue
73 lines
1.9 KiB
Vue
<template>
|
|
<AutoComplete
|
|
v-model="selected"
|
|
:suggestions="filtered"
|
|
option-label="label"
|
|
placeholder="Buscar concepto del presupuesto..."
|
|
class="w-full"
|
|
force-selection
|
|
@complete="onSearch"
|
|
@item-select="onSelect"
|
|
/>
|
|
</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);
|
|
}
|
|
|
|
function onSearch(event: { query: string }) {
|
|
const q = event.query.toLowerCase();
|
|
filtered.value = items.value.filter((i) => i.label.toLowerCase().includes(q)).slice(0, 30);
|
|
}
|
|
|
|
function onSelect(event: { value: BudgetOption }) {
|
|
emit("update:modelValue", event.value?.id ?? null);
|
|
emit("select", event.value ?? null);
|
|
}
|
|
|
|
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>
|