mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 12:43:18 +00:00
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## Resumen Implementa las observaciones de facturas, nómina e IMSS sobre el catálogo de proveedores y la nómina semanal que ya existían. ## Qué queda disponible - Proveedores: régimen fiscal, código postal, estatus activo/inactivo y filtro por nombre o RFC. - Facturas recibidas y emitidas: Excel, UUID, revisión si el RFC no está en el catálogo, clasificación a obra/concepto y gasto `source = invoice` solo para la recibida clasificada. - Nómina: bono, descuento con observación, horas y día festivo dentro del armado semanal. La semana pagada no acepta cambios. - Préstamos: edición (libre sin pagos; con pagos solo nota y cuota) y estado de cuenta con saldo = entregado + comisión − pagos. - Finiquitos y liquidaciones: conceptos, neto descontando descuentos vigentes y saldo de préstamo, y gasto al pagar. - Dispersión: corte semanal, corrección antes de definitiva y comprobante. - Historial de alta, baja y reingreso en la ficha, más movimientos IMSS. - Reporte por periodo en Excel y PDF. - Jornal IMSS distinto del jornal real y consulta de cuotas exportable a Excel. ## Verificación `deno check` de la API pasó. En este entorno no hay Postgres, así que `api/pendientes_test.ts` queda listo para correr cuando la base tenga las migraciones 042–050 y el permiso IAM 007. <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-5e23dc3b-921e-4091-9650-7091d3a52bbf?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-5e23dc3b-921e-4091-9650-7091d3a52bbf&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div>
138 lines
5.6 KiB
Vue
138 lines
5.6 KiB
Vue
<template>
|
|
<div>
|
|
<DeskTableBar v-model="tableQ" placeholder="Proveedor, RFC, UUID o folio">
|
|
<SelectButton v-model="direction" :options="dirOpts" option-label="label" option-value="value" :allow-empty="false" @change="load" />
|
|
<InputNumber v-model="amountMin" placeholder="Monto mín" />
|
|
<InputNumber v-model="amountMax" placeholder="Monto máx" />
|
|
<Button label="Importar Excel" icon="pi pi-upload" @click="showImport = true" />
|
|
</DeskTableBar>
|
|
<DataTable :value="listed" data-key="id" paginator :rows="20" @row-dblclick="(e: { data: Invoice }) => open(e.data)">
|
|
<Column field="invoice_date" header="Fecha" />
|
|
<Column field="supplier_name" header="Proveedor" />
|
|
<Column field="supplier_rfc" header="RFC" />
|
|
<Column field="uuid" header="UUID" />
|
|
<Column field="folio" header="Folio" />
|
|
<Column field="total" header="Total">
|
|
<template #body="{ data }">{{ money(data.total) }}</template>
|
|
</Column>
|
|
<Column field="status" header="Estatus" />
|
|
</DataTable>
|
|
|
|
<Dialog v-model:visible="showImport" header="Importar facturas" modal :style="{ width: '28rem' }">
|
|
<Select v-model="companyId" :options="companies" option-label="name" option-value="id" placeholder="Empresa" class="w-full" />
|
|
<input class="mt-3" type="file" accept=".xlsx,.xls" @change="onFile" />
|
|
<template #footer>
|
|
<Button label="Cargar" icon="pi pi-check" :disabled="!file || !companyId" @click="upload" />
|
|
</template>
|
|
</Dialog>
|
|
|
|
<Dialog v-model:visible="showDetail" header="Clasificar factura" modal :style="{ width: '36rem' }">
|
|
<p v-if="detail">{{ detail.supplier_name }} · {{ detail.uuid || detail.folio }} · {{ detail.review_note }}</p>
|
|
<div v-for="line in lines" :key="line.id" class="mt-2">
|
|
<div>{{ line.description }} · {{ money(line.amount) }}</div>
|
|
<Select v-model="line.project_id" :options="projects" option-label="name" option-value="id" placeholder="Obra" class="w-full mt-1" />
|
|
<BudgetItemPicker v-if="line.project_id" v-model="line.budget_item_id" :project-id="line.project_id" class="mt-1" />
|
|
</div>
|
|
<template #footer>
|
|
<Button label="Clasificar" icon="pi pi-check" @click="classify" />
|
|
</template>
|
|
</Dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
type Invoice = {
|
|
id: number;
|
|
invoice_date: string;
|
|
supplier_name: string;
|
|
supplier_rfc: string;
|
|
uuid: string;
|
|
folio: string;
|
|
total: number;
|
|
status: string;
|
|
review_note?: string;
|
|
};
|
|
type Line = { id: number; description: string; amount: number; project_id: number | null; budget_item_id: number | null };
|
|
|
|
const { api } = useApi();
|
|
const toast = useToast();
|
|
const tableQ = ref("");
|
|
const direction = ref("recibida");
|
|
const dirOpts = [{ label: "Recibidas", value: "recibida" }, { label: "Emitidas", value: "emitida" }];
|
|
const amountMin = ref<number | null>(null);
|
|
const amountMax = ref<number | null>(null);
|
|
const invoices = ref<Invoice[]>([]);
|
|
const companies = ref<{ id: number; name: string }[]>([]);
|
|
const projects = ref<{ id: number; name: string }[]>([]);
|
|
const showImport = ref(false);
|
|
const showDetail = ref(false);
|
|
const companyId = ref<number | null>(null);
|
|
const file = ref<File | null>(null);
|
|
const detail = ref<Invoice | null>(null);
|
|
const lines = ref<Line[]>([]);
|
|
|
|
const listed = computed(() => {
|
|
const q = tableQ.value.trim().toLowerCase();
|
|
return invoices.value.filter((i) => {
|
|
if (amountMin.value != null && Number(i.total) < amountMin.value) return false;
|
|
if (amountMax.value != null && Number(i.total) > amountMax.value) return false;
|
|
if (!q) return true;
|
|
return [i.supplier_name, i.supplier_rfc, i.uuid, i.folio].join(" ").toLowerCase().includes(q);
|
|
});
|
|
});
|
|
|
|
function money(n: number) {
|
|
return Number(n || 0).toLocaleString("es-MX", { style: "currency", currency: "MXN" });
|
|
}
|
|
|
|
async function load() {
|
|
const q = new URLSearchParams({ direction: direction.value });
|
|
const r = await api<{ invoices?: Invoice[]; data?: { invoices?: Invoice[] } }>(`/v1/invoices?${q}`);
|
|
invoices.value = r.invoices ?? r.data?.invoices ?? [];
|
|
}
|
|
|
|
function onFile(ev: Event) {
|
|
file.value = (ev.target as HTMLInputElement).files?.[0] ?? null;
|
|
}
|
|
|
|
async function upload() {
|
|
if (!file.value || !companyId.value) return;
|
|
const body = new FormData();
|
|
body.set("company_id", String(companyId.value));
|
|
body.set("file", file.value);
|
|
const r = await api<{ created?: number; review?: number; data?: { created?: number; review?: number } }>("/v1/invoices/import", { method: "POST", body });
|
|
const created = r.created ?? r.data?.created ?? 0;
|
|
const review = r.review ?? r.data?.review ?? 0;
|
|
toast.add({ severity: "success", summary: `${created} facturas, ${review} en revisión`, life: 2500 });
|
|
showImport.value = false;
|
|
await load();
|
|
}
|
|
|
|
async function open(row: Invoice) {
|
|
const r = await api<{ invoice?: Invoice; lines?: Line[]; data?: { invoice?: Invoice; lines?: Line[] } }>(`/v1/invoices/${row.id}`);
|
|
detail.value = r.invoice ?? r.data?.invoice ?? row;
|
|
lines.value = r.lines ?? r.data?.lines ?? [];
|
|
showDetail.value = true;
|
|
}
|
|
|
|
async function classify() {
|
|
if (!detail.value) return;
|
|
const line = lines.value[0];
|
|
await api(`/v1/invoices/${detail.value.id}/classify`, {
|
|
method: "POST",
|
|
json: { line_id: line?.id, project_id: line?.project_id, budget_item_id: line?.budget_item_id },
|
|
});
|
|
showDetail.value = false;
|
|
await load();
|
|
}
|
|
|
|
onMounted(async () => {
|
|
const [c, p] = await Promise.all([
|
|
api<{ companies?: { id: number; name: string }[] }>("/v1/companies"),
|
|
api<{ projects?: { id: number; name: string }[] }>("/v1/projects"),
|
|
]);
|
|
companies.value = c.companies ?? [];
|
|
projects.value = p.projects ?? [];
|
|
await load();
|
|
});
|
|
</script>
|