mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 16:13:17 +00:00
Compare commits
1 commit
ff04a6621a
...
72aa6ea045
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72aa6ea045 |
4 changed files with 25 additions and 243 deletions
|
|
@ -39,6 +39,5 @@
|
||||||
<include file="changesets/030-rpc-work-program.sql" relativeToChangelogFile="true"/>
|
<include file="changesets/030-rpc-work-program.sql" relativeToChangelogFile="true"/>
|
||||||
<include file="changesets/031-project-doc-types-technical.sql" relativeToChangelogFile="true"/>
|
<include file="changesets/031-project-doc-types-technical.sql" relativeToChangelogFile="true"/>
|
||||||
<include file="changesets/033-catalog-suppliers-units.sql" relativeToChangelogFile="true"/>
|
<include file="changesets/033-catalog-suppliers-units.sql" relativeToChangelogFile="true"/>
|
||||||
<include file="changesets/034-fn-expense-update-reclassify.sql" relativeToChangelogFile="true"/>
|
|
||||||
|
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
--liquibase formatted sql
|
|
||||||
-- PANELS · core · reclasificar gasto (concepto, proveedor, obra)
|
|
||||||
|
|
||||||
--changeset panel:core-034a-fn-expense-update-reclassify splitStatements:false
|
|
||||||
CREATE OR REPLACE FUNCTION core.fn_expense_update(payload jsonb)
|
|
||||||
RETURNS jsonb
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
SECURITY INVOKER
|
|
||||||
SET search_path = core
|
|
||||||
AS $$
|
|
||||||
DECLARE
|
|
||||||
v_id bigint := NULLIF(payload->>'id', '')::bigint;
|
|
||||||
v_tid integer := core._cost_tenant_id(payload);
|
|
||||||
v_cur expense_entries%ROWTYPE;
|
|
||||||
v_warning jsonb;
|
|
||||||
v_row jsonb;
|
|
||||||
v_subtotal numeric; v_iva_amount numeric; v_total numeric;
|
|
||||||
v_line numeric;
|
|
||||||
v_supplier_id bigint;
|
|
||||||
v_supplier_name text;
|
|
||||||
v_supplier_rfc text;
|
|
||||||
v_budget_item_id bigint;
|
|
||||||
v_chapter_id bigint;
|
|
||||||
BEGIN
|
|
||||||
IF v_id IS NULL THEN
|
|
||||||
RETURN core.rpc_err('VALIDATION', 'fn_expense_update: id es obligatorio',
|
|
||||||
jsonb_build_object('fn', 'fn_expense_update', 'field', 'id'));
|
|
||||||
END IF;
|
|
||||||
SELECT * INTO v_cur FROM expense_entries WHERE id = v_id;
|
|
||||||
IF NOT FOUND THEN
|
|
||||||
RETURN core.rpc_err('NOT_FOUND', format('fn_expense_update: gasto id=%s no encontrado', v_id),
|
|
||||||
jsonb_build_object('fn', 'fn_expense_update', 'id', v_id));
|
|
||||||
END IF;
|
|
||||||
IF v_cur.is_locked OR v_cur.source <> 'manual' THEN
|
|
||||||
RETURN core.rpc_err('VALIDATION', 'fn_expense_update: solo se editan gastos manuales no bloqueados',
|
|
||||||
jsonb_build_object('fn', 'fn_expense_update', 'id', v_id, 'source', v_cur.source));
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF payload ? 'supplier_id' THEN
|
|
||||||
v_supplier_id := NULLIF(payload->>'supplier_id', '')::bigint;
|
|
||||||
IF v_supplier_id IS NOT NULL THEN
|
|
||||||
SELECT name, rfc INTO v_supplier_name, v_supplier_rfc
|
|
||||||
FROM suppliers
|
|
||||||
WHERE id = v_supplier_id AND tenant_id = v_tid AND status = 'activo';
|
|
||||||
IF NOT FOUND THEN
|
|
||||||
RETURN core.rpc_err('NOT_FOUND', format('fn_expense_update: proveedor id=%s no encontrado', v_supplier_id),
|
|
||||||
jsonb_build_object('fn', 'fn_expense_update', 'supplier_id', v_supplier_id));
|
|
||||||
END IF;
|
|
||||||
ELSE
|
|
||||||
v_supplier_name := '';
|
|
||||||
v_supplier_rfc := '';
|
|
||||||
END IF;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF payload ? 'budget_item_id' THEN
|
|
||||||
v_budget_item_id := NULLIF(payload->>'budget_item_id', '')::bigint;
|
|
||||||
IF v_budget_item_id IS NOT NULL THEN
|
|
||||||
SELECT chapter_id INTO v_chapter_id FROM budget_items WHERE id = v_budget_item_id;
|
|
||||||
END IF;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
UPDATE expense_entries SET
|
|
||||||
expense_date = COALESCE(NULLIF(payload->>'expense_date', '')::date, expense_date),
|
|
||||||
description = COALESCE(nullif(btrim(payload->>'description'), ''), description),
|
|
||||||
project_id = CASE WHEN payload ? 'project_id' THEN NULLIF(payload->>'project_id', '')::bigint ELSE project_id END,
|
|
||||||
supplier_id = CASE WHEN payload ? 'supplier_id' THEN v_supplier_id ELSE supplier_id END,
|
|
||||||
supplier_name = CASE
|
|
||||||
WHEN payload ? 'supplier_id' THEN coalesce(v_supplier_name, '')
|
|
||||||
WHEN payload ? 'supplier_name' THEN coalesce(nullif(btrim(payload->>'supplier_name'), ''), '')
|
|
||||||
ELSE supplier_name END,
|
|
||||||
supplier_rfc = CASE
|
|
||||||
WHEN payload ? 'supplier_id' THEN coalesce(v_supplier_rfc, '')
|
|
||||||
WHEN payload ? 'supplier_rfc' THEN upper(coalesce(nullif(btrim(payload->>'supplier_rfc'), ''), ''))
|
|
||||||
ELSE supplier_rfc END,
|
|
||||||
budget_item_id = CASE WHEN payload ? 'budget_item_id' THEN v_budget_item_id ELSE budget_item_id END,
|
|
||||||
budget_chapter_id = CASE WHEN payload ? 'budget_item_id' THEN v_chapter_id ELSE budget_chapter_id END,
|
|
||||||
quantity = CASE WHEN payload ? 'quantity' THEN COALESCE((payload->>'quantity')::numeric, 0) ELSE quantity END,
|
|
||||||
unit = CASE WHEN payload ? 'unit' THEN coalesce(nullif(btrim(payload->>'unit'), ''), '') ELSE unit END,
|
|
||||||
unit_price = CASE WHEN payload ? 'unit_price' THEN COALESCE((payload->>'unit_price')::numeric, 0) ELSE unit_price END,
|
|
||||||
tax_included = CASE WHEN payload ? 'tax_included' THEN COALESCE((payload->>'tax_included')::boolean, false) ELSE tax_included END,
|
|
||||||
notes = CASE WHEN payload ? 'notes' THEN coalesce(nullif(btrim(payload->>'notes'), ''), '') ELSE notes END
|
|
||||||
WHERE id = v_id;
|
|
||||||
SELECT * INTO v_cur FROM expense_entries WHERE id = v_id;
|
|
||||||
v_line := CASE
|
|
||||||
WHEN v_cur.quantity > 0 AND v_cur.unit_price > 0 THEN round(v_cur.quantity * v_cur.unit_price, 2)
|
|
||||||
ELSE v_cur.subtotal
|
|
||||||
END;
|
|
||||||
SELECT c.subtotal, c.iva_amount, c.total INTO v_subtotal, v_iva_amount, v_total
|
|
||||||
FROM core._cost_calc_amounts(
|
|
||||||
v_line,
|
|
||||||
v_cur.iva_rate,
|
|
||||||
v_cur.tax_included,
|
|
||||||
CASE WHEN v_cur.tax_included THEN v_line ELSE NULL END
|
|
||||||
) c;
|
|
||||||
UPDATE expense_entries SET subtotal = v_subtotal, iva_amount = v_iva_amount, total = v_total WHERE id = v_id;
|
|
||||||
IF v_cur.counts_toward_budget AND v_cur.budget_item_id IS NOT NULL THEN
|
|
||||||
v_warning := core._cost_budget_warning(v_tid, v_cur.budget_item_id, 0, 0);
|
|
||||||
END IF;
|
|
||||||
SELECT to_jsonb(e) INTO v_row FROM expense_entries e WHERE id = v_id;
|
|
||||||
RETURN core.rpc_ok(
|
|
||||||
jsonb_build_object('expense', v_row, 'budget_warning', v_warning),
|
|
||||||
format('Gasto id=%s actualizado', v_id),
|
|
||||||
jsonb_build_object('fn', 'fn_expense_update', 'id', v_id, 'budget_item_id', v_cur.budget_item_id)
|
|
||||||
);
|
|
||||||
EXCEPTION WHEN OTHERS THEN
|
|
||||||
RETURN core.rpc_from_exception('fn_expense_update', SQLSTATE, SQLERRM);
|
|
||||||
END;
|
|
||||||
$$;
|
|
||||||
|
|
||||||
--changeset panel:core-034b-fn-expense-update-grant endDelimiter:; splitStatements:true
|
|
||||||
GRANT EXECUTE ON FUNCTION core.fn_expense_update(jsonb) TO panels_core_app;
|
|
||||||
|
|
@ -2286,8 +2286,7 @@ html.dark .p-dialog .p-dialog-header-icon {
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
html.dark .budget-item-option-desc,
|
html.dark .budget-item-option-desc {
|
||||||
html.dark .budget-item-picker .budget-item-value-desc {
|
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@
|
||||||
<DeskTableBar v-model="tableQ" placeholder="Buscar descripción, proveedor o concepto">
|
<DeskTableBar v-model="tableQ" placeholder="Buscar descripción, proveedor o concepto">
|
||||||
<Chip :label="projectLabel" icon="pi pi-building" />
|
<Chip :label="projectLabel" icon="pi pi-building" />
|
||||||
<Chip :label="`${listed.length} gastos`" icon="pi pi-list" />
|
<Chip :label="`${listed.length} gastos`" icon="pi pi-list" />
|
||||||
<Chip :label="`${unclassifiedCount} sin clasificar`" icon="pi pi-question-circle" />
|
|
||||||
<Chip :label="money(totalSubtotal)" icon="pi pi-wallet" />
|
<Chip :label="money(totalSubtotal)" icon="pi pi-wallet" />
|
||||||
<SelectButton v-model="listFilter" :options="listFilterOpts" option-label="label" option-value="value" :allow-empty="false" />
|
|
||||||
</DeskTableBar>
|
</DeskTableBar>
|
||||||
|
|
||||||
<Message v-if="budgetWarning" :severity="budgetWarning.level === 'critical' ? 'error' : 'warn'" :closable="true" class="mb-3">
|
<Message v-if="budgetWarning" :severity="budgetWarning.level === 'critical' ? 'error' : 'warn'" :closable="true" class="mb-3">
|
||||||
|
|
@ -13,28 +11,20 @@
|
||||||
</Message>
|
</Message>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
v-model:selection="selected"
|
|
||||||
:value="listed"
|
:value="listed"
|
||||||
data-key="id"
|
data-key="id"
|
||||||
paginator
|
paginator
|
||||||
:rows="25"
|
:rows="25"
|
||||||
empty-message="No hay gastos en el periodo seleccionado."
|
empty-message="No hay gastos en el periodo seleccionado."
|
||||||
selection-mode="single"
|
|
||||||
meta-key-selection="false"
|
|
||||||
scrollable
|
scrollable
|
||||||
scroll-height="flex"
|
scroll-height="flex"
|
||||||
table-style="min-width: 56rem"
|
table-style="min-width: 56rem"
|
||||||
@row-click="onRow"
|
|
||||||
@row-dblclick="(e: { data: Expense }) => openEdit(e.data)"
|
|
||||||
>
|
>
|
||||||
<Column field="expense_date" header="Fecha" sortable style="width: 7rem" />
|
<Column field="expense_date" header="Fecha" sortable style="width: 7rem" />
|
||||||
<Column field="description" header="Descripción" sortable />
|
<Column field="description" header="Descripción" sortable />
|
||||||
<Column field="supplier_name" header="Proveedor" sortable style="width: 10rem" />
|
<Column field="supplier_name" header="Proveedor" sortable style="width: 10rem" />
|
||||||
<Column field="budget_item_code" header="Concepto" style="width: 10rem">
|
<Column field="budget_item_code" header="Concepto" style="width: 8rem">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }"><span class="identifier">{{ data.budget_item_code || "—" }}</span></template>
|
||||||
<Tag v-if="!data.budget_item_id" value="Sin clasificar" severity="warn" />
|
|
||||||
<span v-else class="identifier">{{ data.budget_item_code || "—" }}</span>
|
|
||||||
</template>
|
|
||||||
</Column>
|
</Column>
|
||||||
<Column field="subtotal" header="Subtotal" sortable style="width: 9rem">
|
<Column field="subtotal" header="Subtotal" sortable style="width: 9rem">
|
||||||
<template #body="{ data }">{{ money(data.subtotal) }}</template>
|
<template #body="{ data }">{{ money(data.subtotal) }}</template>
|
||||||
|
|
@ -48,7 +38,7 @@
|
||||||
<Column field="source" header="Origen" style="width: 7rem" />
|
<Column field="source" header="Origen" style="width: 7rem" />
|
||||||
</DataTable>
|
</DataTable>
|
||||||
|
|
||||||
<Dialog v-model:visible="showForm" :header="editingId ? 'Editar gasto' : 'Nuevo gasto'" modal :style="{ width: '720px' }">
|
<Dialog v-model:visible="showForm" header="Nuevo gasto" modal :style="{ width: '720px' }">
|
||||||
<div class="form-grid form-grid-compact">
|
<div class="form-grid form-grid-compact">
|
||||||
<FormField label="Empresa" required>
|
<FormField label="Empresa" required>
|
||||||
<Select v-model="form.company_id" :options="companies" option-label="name" option-value="id" class="w-full" />
|
<Select v-model="form.company_id" :options="companies" option-label="name" option-value="id" class="w-full" />
|
||||||
|
|
@ -138,7 +128,7 @@
|
||||||
<Message v-if="formError" severity="error" class="mt-3" :closable="false">{{ formError }}</Message>
|
<Message v-if="formError" severity="error" class="mt-3" :closable="false">{{ formError }}</Message>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button label="Cancelar" text @click="showForm = false" />
|
<Button label="Cancelar" text @click="showForm = false" />
|
||||||
<Button :label="editingId ? 'Guardar cambios' : 'Guardar'" icon="pi pi-check" @click="save" />
|
<Button label="Guardar" icon="pi pi-check" @click="save" />
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
|
@ -174,15 +164,7 @@ type Expense = {
|
||||||
expense_date: string;
|
expense_date: string;
|
||||||
description: string;
|
description: string;
|
||||||
supplier_name: string;
|
supplier_name: string;
|
||||||
supplier_id?: number | null;
|
|
||||||
budget_item_id?: number | null;
|
|
||||||
budget_item_code?: string;
|
budget_item_code?: string;
|
||||||
company_id?: number | null;
|
|
||||||
quantity?: number;
|
|
||||||
unit?: string;
|
|
||||||
unit_price?: number;
|
|
||||||
tax_included?: boolean;
|
|
||||||
is_locked?: boolean;
|
|
||||||
subtotal: number;
|
subtotal: number;
|
||||||
iva_amount: number;
|
iva_amount: number;
|
||||||
total: number;
|
total: number;
|
||||||
|
|
@ -210,15 +192,7 @@ const unitError = ref("");
|
||||||
const budgetWarning = ref<{ level: string; message: string } | null>(null);
|
const budgetWarning = ref<{ level: string; message: string } | null>(null);
|
||||||
const receiptFile = ref<File | null>(null);
|
const receiptFile = ref<File | null>(null);
|
||||||
const ivaRate = ref(0.16);
|
const ivaRate = ref(0.16);
|
||||||
const editingId = ref<number | null>(null);
|
|
||||||
const selected = ref<Expense | null>(null);
|
|
||||||
const listFilter = ref<"all" | "unclassified">("all");
|
|
||||||
const listFilterOpts = [
|
|
||||||
{ label: "Todos", value: "all" },
|
|
||||||
{ label: "Sin clasificar", value: "unclassified" },
|
|
||||||
];
|
|
||||||
const canCreateExpense = computed(() => perms.can("expenses.create"));
|
const canCreateExpense = computed(() => perms.can("expenses.create"));
|
||||||
const canUpdateExpense = computed(() => perms.can("expenses.update"));
|
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
company_id: null as number | null,
|
company_id: null as number | null,
|
||||||
|
|
@ -264,16 +238,11 @@ const projectLabel = computed(() => {
|
||||||
});
|
});
|
||||||
const listed = computed(() => {
|
const listed = computed(() => {
|
||||||
const q = tableQ.value.toLowerCase();
|
const q = tableQ.value.toLowerCase();
|
||||||
return expenses.value.filter((e) => {
|
return expenses.value.filter((e) =>
|
||||||
if (listFilter.value === "unclassified" && e.budget_item_id) return false;
|
!q || [e.description, e.supplier_name, e.budget_item_code].join(" ").toLowerCase().includes(q),
|
||||||
return !q || [e.description, e.supplier_name, e.budget_item_code].join(" ").toLowerCase().includes(q);
|
);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
const unclassifiedCount = computed(() => expenses.value.filter((e) => !e.budget_item_id).length);
|
|
||||||
const totalSubtotal = computed(() => listed.value.reduce((s, e) => s + Number(e.subtotal || 0), 0));
|
const totalSubtotal = computed(() => listed.value.reduce((s, e) => s + Number(e.subtotal || 0), 0));
|
||||||
const canEditSelected = computed(() =>
|
|
||||||
Boolean(selected.value && canUpdateExpense.value && isEditable(selected.value)),
|
|
||||||
);
|
|
||||||
|
|
||||||
function money(v: number) {
|
function money(v: number) {
|
||||||
return new Intl.NumberFormat("es-MX", { style: "currency", currency: "MXN" }).format(Number(v || 0));
|
return new Intl.NumberFormat("es-MX", { style: "currency", currency: "MXN" }).format(Number(v || 0));
|
||||||
|
|
@ -309,10 +278,6 @@ function onSupplierSaved(supplier: Supplier) {
|
||||||
form.supplier_id = supplier.id;
|
form.supplier_id = supplier.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isEditable(row: Expense) {
|
|
||||||
return row.source === "manual" && !row.is_locked;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
form.company_id = companies.value[0]?.id ?? null;
|
form.company_id = companies.value[0]?.id ?? null;
|
||||||
form.project_id = projectId.value;
|
form.project_id = projectId.value;
|
||||||
|
|
@ -329,53 +294,10 @@ function resetForm() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function openNew() {
|
function openNew() {
|
||||||
editingId.value = null;
|
|
||||||
resetForm();
|
resetForm();
|
||||||
showForm.value = true;
|
showForm.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRow(e: { data: Expense }) {
|
|
||||||
selected.value = e.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openEdit(row: Expense) {
|
|
||||||
selected.value = row;
|
|
||||||
if (!canUpdateExpense.value) return;
|
|
||||||
if (!isEditable(row)) {
|
|
||||||
toast.add({
|
|
||||||
severity: "info",
|
|
||||||
summary: "Este gasto no se edita aquí",
|
|
||||||
detail: row.source === "manual" ? "Está bloqueado" : "Viene de nómina o destajo",
|
|
||||||
life: 3000,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
formError.value = "";
|
|
||||||
try {
|
|
||||||
const r = await api<{ expense?: Expense; data?: { expense?: Expense } }>(`/v1/expenses/${row.id}`);
|
|
||||||
const e = r.expense ?? r.data?.expense ?? row;
|
|
||||||
editingId.value = e.id;
|
|
||||||
form.company_id = e.company_id ?? row.company_id ?? companies.value[0]?.id ?? null;
|
|
||||||
form.project_id = e.project_id ?? null;
|
|
||||||
form.expense_date = String(e.expense_date || "").slice(0, 10);
|
|
||||||
form.budget_item_id = e.budget_item_id ?? null;
|
|
||||||
form.description = e.description || "";
|
|
||||||
form.supplier_id = e.supplier_id ?? null;
|
|
||||||
form.quantity = Number(e.quantity || 0);
|
|
||||||
form.unit = e.unit || "";
|
|
||||||
form.unit_price = Number(e.unit_price || 0);
|
|
||||||
form.tax_included = Boolean(e.tax_included);
|
|
||||||
receiptFile.value = null;
|
|
||||||
showForm.value = true;
|
|
||||||
} catch (err) {
|
|
||||||
toast.add({
|
|
||||||
severity: "error",
|
|
||||||
summary: displayErrorMessage((err as { data?: unknown }).data ?? err),
|
|
||||||
life: 4000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncChrome() {
|
function syncChrome() {
|
||||||
desk.configure({
|
desk.configure({
|
||||||
tab: "Gastos",
|
tab: "Gastos",
|
||||||
|
|
@ -399,16 +321,6 @@ function syncChrome() {
|
||||||
disabled: !canCreateExpense.value,
|
disabled: !canCreateExpense.value,
|
||||||
command: openNew,
|
command: openNew,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: "edit",
|
|
||||||
label: "Reclasificar",
|
|
||||||
icon: "pi pi-pencil",
|
|
||||||
disabled: !canEditSelected.value,
|
|
||||||
title: canEditSelected.value
|
|
||||||
? "Cambiar concepto u otros datos del gasto"
|
|
||||||
: "Seleccione un gasto manual sin clasificar o para cambiar de partida",
|
|
||||||
command: () => selected.value && openEdit(selected.value),
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -485,22 +397,8 @@ async function saveUnit() {
|
||||||
async function loadExpenses() {
|
async function loadExpenses() {
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
if (projectId.value) qs.set("project_id", String(projectId.value));
|
if (projectId.value) qs.set("project_id", String(projectId.value));
|
||||||
const r = await api<{ expenses?: Expense[]; data?: { expenses?: Expense[] }; message?: string }>(`/v1/expenses?${qs}`);
|
const r = await api<{ data?: { expenses?: Expense[] }; message?: string }>(`/v1/expenses?${qs}`);
|
||||||
const keep = selected.value?.id;
|
expenses.value = (r.data?.expenses ?? []) as Expense[];
|
||||||
expenses.value = (r.expenses ?? r.data?.expenses ?? []) as Expense[];
|
|
||||||
selected.value = keep ? expenses.value.find((e) => e.id === keep) ?? null : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function payload() {
|
|
||||||
return {
|
|
||||||
...form,
|
|
||||||
project_id: form.project_id || null,
|
|
||||||
budget_item_id: form.budget_item_id || null,
|
|
||||||
supplier_id: form.supplier_id || null,
|
|
||||||
subtotal: computedAmounts.value.subtotal,
|
|
||||||
total: computedAmounts.value.total,
|
|
||||||
iva_rate: ivaRate.value,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
|
|
@ -510,29 +408,28 @@ async function save() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const r = editingId.value
|
const r = await api<{ data?: { expense?: { id: number }; budget_warning?: { level: string; message: string } }; message?: string }>(
|
||||||
? await api<{ data?: { expense?: { id: number }; budget_warning?: { level: string; message: string } }; message?: string }>(
|
"/v1/expenses",
|
||||||
`/v1/expenses/${editingId.value}`,
|
{
|
||||||
{ method: "PATCH", json: payload() },
|
method: "POST",
|
||||||
)
|
json: {
|
||||||
: await api<{ data?: { expense?: { id: number }; budget_warning?: { level: string; message: string } }; message?: string }>(
|
...form,
|
||||||
"/v1/expenses",
|
project_id: form.project_id || null,
|
||||||
{ method: "POST", json: payload() },
|
subtotal: computedAmounts.value.subtotal,
|
||||||
);
|
total: computedAmounts.value.total,
|
||||||
const expenseId = r.data?.expense?.id ?? editingId.value;
|
iva_rate: ivaRate.value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const expenseId = r.data?.expense?.id;
|
||||||
if (expenseId && receiptFile.value) {
|
if (expenseId && receiptFile.value) {
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append("file", receiptFile.value);
|
fd.append("file", receiptFile.value);
|
||||||
await fetch(`/v1/expenses/${expenseId}/attachments`, { method: "POST", body: fd, credentials: "include" });
|
await fetch(`/v1/expenses/${expenseId}/attachments`, { method: "POST", body: fd, credentials: "include" });
|
||||||
}
|
}
|
||||||
budgetWarning.value = r.data?.budget_warning ?? null;
|
budgetWarning.value = r.data?.budget_warning ?? null;
|
||||||
toast.add({
|
toast.add({ severity: "success", summary: r.message || "Gasto registrado", life: 2500 });
|
||||||
severity: "success",
|
|
||||||
summary: r.message || (editingId.value ? "Gasto actualizado" : "Gasto registrado"),
|
|
||||||
life: 2500,
|
|
||||||
});
|
|
||||||
showForm.value = false;
|
showForm.value = false;
|
||||||
editingId.value = null;
|
|
||||||
await loadExpenses();
|
await loadExpenses();
|
||||||
if (route.query.nuevo || route.query.budget_item_id) {
|
if (route.query.nuevo || route.query.budget_item_id) {
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
|
|
@ -556,8 +453,6 @@ watch(projectId, () => {
|
||||||
syncChrome();
|
syncChrome();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(selected, syncChrome);
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => String(route.query.nuevo || ""),
|
() => String(route.query.nuevo || ""),
|
||||||
(nuevo) => {
|
(nuevo) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue