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>
147 lines
4.3 KiB
Vue
147 lines
4.3 KiB
Vue
<template>
|
|
<Dialog
|
|
v-model:visible="visible"
|
|
:header="supplierId ? 'Editar proveedor' : 'Nuevo proveedor'"
|
|
modal
|
|
:style="{ width: '640px' }"
|
|
>
|
|
<div class="form-grid form-grid-compact">
|
|
<FormField label="Nombre comercial" required>
|
|
<InputText v-model="form.name" class="w-full" placeholder="Home Depot" />
|
|
</FormField>
|
|
<FormField label="Razón social">
|
|
<InputText v-model="form.legal_name" class="w-full" />
|
|
</FormField>
|
|
<FormField label="RFC">
|
|
<InputText v-model="form.rfc" class="w-full identifier-input" maxlength="13" placeholder="XAXX010101000" />
|
|
</FormField>
|
|
<FormField label="Régimen fiscal">
|
|
<InputText v-model="form.regimen_fiscal" class="w-full identifier-input" maxlength="3" placeholder="601" />
|
|
</FormField>
|
|
<FormField label="C.P. fiscal">
|
|
<InputText v-model="form.postal_code" class="w-full identifier-input" maxlength="5" />
|
|
</FormField>
|
|
<FormField label="Estatus">
|
|
<Select v-model="form.status" :options="statusOpts" option-label="label" option-value="value" class="w-full" />
|
|
</FormField>
|
|
<FormField label="Teléfono">
|
|
<InputText v-model="form.phone" class="w-full" />
|
|
</FormField>
|
|
<FormField label="Correo">
|
|
<InputText v-model="form.email" class="w-full no-caps" />
|
|
</FormField>
|
|
<FormField label="Ciudad">
|
|
<InputText v-model="form.city" class="w-full" />
|
|
</FormField>
|
|
<FormField label="Domicilio" class="span-2">
|
|
<InputText v-model="form.address" class="w-full" />
|
|
</FormField>
|
|
<FormField label="Notas" class="span-2">
|
|
<Textarea v-model="form.notes" class="w-full" rows="2" auto-resize />
|
|
</FormField>
|
|
</div>
|
|
<Message v-if="error" severity="error" class="mt-3" :closable="false">{{ error }}</Message>
|
|
<template #footer>
|
|
<Button label="Cancelar" text @click="visible = false" />
|
|
<Button label="Guardar" icon="pi pi-check" :loading="saving" @click="save" />
|
|
</template>
|
|
</Dialog>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import FormField from "~/components/padron/FormField.vue";
|
|
import { displayErrorMessage } from "~/composables/api-response";
|
|
|
|
export type Supplier = {
|
|
id: number;
|
|
name: string;
|
|
legal_name?: string;
|
|
rfc?: string;
|
|
email?: string;
|
|
phone?: string;
|
|
address?: string;
|
|
city?: string;
|
|
state?: string;
|
|
postal_code?: string;
|
|
regimen_fiscal?: string;
|
|
notes?: string;
|
|
status?: string;
|
|
};
|
|
|
|
const props = defineProps<{
|
|
modelValue: boolean;
|
|
supplier?: Supplier | null;
|
|
}>();
|
|
const emit = defineEmits<{
|
|
"update:modelValue": [boolean];
|
|
saved: [Supplier];
|
|
}>();
|
|
|
|
const { api } = useApi();
|
|
const visible = computed({
|
|
get: () => props.modelValue,
|
|
set: (v: boolean) => emit("update:modelValue", v),
|
|
});
|
|
|
|
const empty = () => ({
|
|
name: "",
|
|
legal_name: "",
|
|
rfc: "",
|
|
email: "",
|
|
phone: "",
|
|
address: "",
|
|
city: "",
|
|
postal_code: "",
|
|
regimen_fiscal: "",
|
|
notes: "",
|
|
status: "activo",
|
|
});
|
|
|
|
const statusOpts = [
|
|
{ label: "Activo", value: "activo" },
|
|
{ label: "Inactivo", value: "inactivo" },
|
|
];
|
|
|
|
const form = reactive(empty());
|
|
const error = ref("");
|
|
const saving = ref(false);
|
|
const supplierId = computed(() => props.supplier?.id ?? null);
|
|
|
|
watch(
|
|
() => [props.modelValue, props.supplier] as const,
|
|
([open, supplier]) => {
|
|
if (!open) return;
|
|
error.value = "";
|
|
Object.assign(form, empty(), supplier || {});
|
|
},
|
|
);
|
|
|
|
async function save() {
|
|
error.value = "";
|
|
if (!form.name.trim()) {
|
|
error.value = "El nombre del proveedor es obligatorio";
|
|
return;
|
|
}
|
|
saving.value = true;
|
|
try {
|
|
const payload = { ...form, rfc: form.rfc.trim().toUpperCase() };
|
|
const r = supplierId.value
|
|
? await api<{ supplier?: Supplier; data?: { supplier?: Supplier } }>(`/v1/suppliers/${supplierId.value}`, {
|
|
method: "PATCH",
|
|
json: payload,
|
|
})
|
|
: await api<{ supplier?: Supplier; data?: { supplier?: Supplier } }>("/v1/suppliers", {
|
|
method: "POST",
|
|
json: payload,
|
|
});
|
|
const saved = r.supplier ?? r.data?.supplier;
|
|
if (!saved) throw new Error("No se recibió el proveedor guardado");
|
|
emit("saved", saved);
|
|
visible.value = false;
|
|
} catch (e) {
|
|
error.value = displayErrorMessage((e as { data?: unknown }).data ?? e);
|
|
} finally {
|
|
saving.value = false;
|
|
}
|
|
}
|
|
</script>
|