feat: agregar gestión de proveedores en la creación y edición de facturas

This commit is contained in:
Juan Felipe Zapata Moreno 2026-03-21 12:38:57 -06:00
parent 5dbb52a9e9
commit 53a5208cdf
4 changed files with 128 additions and 9 deletions

View File

@ -18,6 +18,7 @@ export default {
title: 'Editar Factura',
description: 'Actualiza los datos de la factura. Si subes un nuevo archivo, el anterior será reemplazado.',
},
supplier: 'Proveedor',
paid: 'Pagada',
pending: 'Pendiente',
export: 'Exportar pendientes',

View File

@ -1,11 +1,15 @@
<script setup>
import { useForm, apiURL } from '@Services/Api';
import { onMounted, ref } from 'vue';
import { useForm, useApi, apiURL } from '@Services/Api';
import { transl } from './Module';
import Modal from '@Holos/Modal.vue';
import FormInput from '@Holos/Form/Input.vue';
import FormError from '@Holos/Form/Elements/Error.vue';
import SingleFile from '@Holos/Form/SingleFile.vue';
import Selectable from '@Holos/Form/Selectable.vue';
import GoogleIcon from '@Shared/GoogleIcon.vue';
import SupplierCreate from '@Pages/POS/Suppliers/Create.vue';
/** Eventos */
const emit = defineEmits(['close', 'created']);
@ -15,17 +19,25 @@ defineProps({
show: Boolean
});
/** Estado */
const api = useApi();
const suppliers = ref([]);
const selectedSupplier = ref(null);
const showSupplierModal = ref(false);
/** Formulario */
const form = useForm({
name: '',
cost: '',
deadline: '',
supplier_id: null,
paid: false,
file: null,
});
/** Métodos */
const create = () => {
form.supplier_id = selectedSupplier.value?.id ?? null;
form.post(apiURL('bills'), {
onSuccess: (data) => {
window.Notify.success(Lang('register.create.onSuccess'));
@ -37,8 +49,18 @@ const create = () => {
const closeModal = () => {
form.reset();
selectedSupplier.value = null;
emit('close');
};
const loadSuppliers = () => {
api.get(apiURL('proveedores'), {
onSuccess: (data) => suppliers.value = data.suppliers?.data ?? [],
});
};
/** Ciclo de vida */
onMounted(loadSuppliers);
</script>
<template>
@ -62,6 +84,31 @@ const closeModal = () => {
<!-- Formulario -->
<form @submit.prevent="create" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<!-- Proveedor -->
<div class="col-span-2">
<div class="flex items-center justify-between mb-1.5">
<label class="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase">
{{ transl('supplier') }}
</label>
<button
type="button"
class="flex items-center justify-center w-6 h-6 rounded-full bg-indigo-600 hover:bg-indigo-700 text-white transition-colors"
:title="$t('crud.create')"
@click="showSupplierModal = true"
>
<GoogleIcon name="add" class="text-sm" />
</button>
</div>
<Selectable
v-model="selectedSupplier"
:options="suppliers"
label="business_name"
track-by="id"
:placeholder="transl('supplier')"
/>
<FormError :message="form.errors?.supplier_id" />
</div>
<!-- Nombre -->
<div>
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
@ -150,4 +197,10 @@ const closeModal = () => {
</form>
</div>
</Modal>
<SupplierCreate
:show="showSupplierModal"
@close="showSupplierModal = false"
@created="() => { showSupplierModal = false; loadSuppliers(); }"
/>
</template>

View File

@ -1,6 +1,6 @@
<script setup>
import { watch } from 'vue';
import { useForm, apiURL } from '@Services/Api';
import { onMounted, ref, watch } from 'vue';
import { useForm, useApi, apiURL } from '@Services/Api';
import { transl } from './Module';
import Modal from '@Holos/Modal.vue';
@ -8,6 +8,8 @@ import FormInput from '@Holos/Form/Input.vue';
import FormError from '@Holos/Form/Elements/Error.vue';
import SingleFile from '@Holos/Form/SingleFile.vue';
import GoogleIcon from '@Shared/GoogleIcon.vue';
import Selectable from '@Holos/Form/Selectable.vue';
import SupplierCreate from '@Pages/POS/Suppliers/Create.vue';
/** Eventos */
const emit = defineEmits(['close', 'updated']);
@ -18,17 +20,31 @@ const props = defineProps({
bill: Object
});
/** Estado */
const api = useApi();
const suppliers = ref([]);
const selectedSupplier = ref(null);
const showSupplierModal = ref(false);
const loadSuppliers = () => {
api.get(apiURL('proveedores'), {
onSuccess: (data) => suppliers.value = data.suppliers?.data ?? [],
});
};
/** Formulario */
const form = useForm({
name: '',
cost: '',
deadline: '',
supplier_id: null,
paid: false,
file: null,
});
/** Métodos */
const update = () => {
form.supplier_id = selectedSupplier.value?.id ?? null;
form.post(apiURL(`bills/${props.bill.id}`), {
onSuccess: () => {
window.Notify.success(Lang('register.edit.onSuccess'));
@ -40,9 +56,13 @@ const update = () => {
const closeModal = () => {
form.reset();
selectedSupplier.value = null;
emit('close');
};
/** Ciclo de vida */
onMounted(loadSuppliers);
/** Observadores */
watch(() => props.bill, (bill) => {
if (bill) {
@ -51,6 +71,7 @@ watch(() => props.bill, (bill) => {
form.deadline = bill.deadline || '';
form.paid = !!bill.paid;
form.file = null;
selectedSupplier.value = bill.supplier ?? null;
}
}, { immediate: true });
</script>
@ -76,6 +97,31 @@ watch(() => props.bill, (bill) => {
<!-- Formulario -->
<form @submit.prevent="update" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<!-- Proveedor -->
<div class="col-span-2">
<div class="flex items-center justify-between mb-1.5">
<label class="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase">
{{ transl('supplier') }}
</label>
<button
type="button"
class="flex items-center justify-center w-6 h-6 rounded-full bg-indigo-600 hover:bg-indigo-700 text-white transition-colors"
:title="$t('crud.create')"
@click="showSupplierModal = true"
>
<GoogleIcon name="add" class="text-sm" />
</button>
</div>
<Selectable
v-model="selectedSupplier"
:options="suppliers"
label="business_name"
track-by="id"
:placeholder="transl('supplier')"
/>
<FormError :message="form.errors?.supplier_id" />
</div>
<!-- Nombre -->
<div>
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
@ -178,4 +224,10 @@ watch(() => props.bill, (bill) => {
</form>
</div>
</Modal>
<SupplierCreate
:show="showSupplierModal"
@close="showSupplierModal = false"
@created="() => { showSupplierModal = false; loadSuppliers(); }"
/>
</template>

View File

@ -81,7 +81,11 @@ const togglePaid = (bill) => {
};
const exportPending = () => {
api.download(apiURL('bills/pending/excel'), 'Facturas_Pendientes.xlsx');
const now = new Date();
const pad = (n) => String(n).padStart(2, '0');
const date = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
const time = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
api.download(apiURL('bills/pending/excel'), `Facturas_Pendientes_${date}_${time}.xlsx`);
};
/** Ciclo de vida */
@ -122,6 +126,9 @@ onMounted(() => searcher.search());
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{{ transl('name') }}
</th>
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{{ transl('supplier') }}
</th>
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{{ transl('cost') }}
</th>
@ -148,6 +155,12 @@ onMounted(() => searcher.search());
<td class="px-6 py-4 text-center">
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ bill.name }}</p>
</td>
<td class="px-6 py-4 text-center">
<p v-if="bill.supplier" class="text-sm text-gray-700 dark:text-gray-300">
{{ bill.supplier.business_name }}
</p>
<span v-else class="text-sm text-gray-400"></span>
</td>
<td class="px-6 py-4 text-center">
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ formatCurrency(bill.cost) }}
@ -208,7 +221,7 @@ onMounted(() => searcher.search());
</template>
<template #empty>
<td colspan="6" class="table-cell text-center">
<td colspan="7" class="table-cell text-center">
<div class="flex flex-col items-center justify-center py-8 text-gray-500">
<GoogleIcon name="receipt_long" class="text-6xl mb-2 opacity-50" />
<p class="font-semibold">{{ $t('registers.empty') }}</p>