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', title: 'Editar Factura',
description: 'Actualiza los datos de la factura. Si subes un nuevo archivo, el anterior será reemplazado.', description: 'Actualiza los datos de la factura. Si subes un nuevo archivo, el anterior será reemplazado.',
}, },
supplier: 'Proveedor',
paid: 'Pagada', paid: 'Pagada',
pending: 'Pendiente', pending: 'Pendiente',
export: 'Exportar pendientes', export: 'Exportar pendientes',

View File

@ -1,11 +1,15 @@
<script setup> <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 { transl } from './Module';
import Modal from '@Holos/Modal.vue'; import Modal from '@Holos/Modal.vue';
import FormInput from '@Holos/Form/Input.vue'; import FormInput from '@Holos/Form/Input.vue';
import FormError from '@Holos/Form/Elements/Error.vue'; import FormError from '@Holos/Form/Elements/Error.vue';
import SingleFile from '@Holos/Form/SingleFile.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 */ /** Eventos */
const emit = defineEmits(['close', 'created']); const emit = defineEmits(['close', 'created']);
@ -15,17 +19,25 @@ defineProps({
show: Boolean show: Boolean
}); });
/** Estado */
const api = useApi();
const suppliers = ref([]);
const selectedSupplier = ref(null);
const showSupplierModal = ref(false);
/** Formulario */ /** Formulario */
const form = useForm({ const form = useForm({
name: '', name: '',
cost: '', cost: '',
deadline: '', deadline: '',
supplier_id: null,
paid: false, paid: false,
file: null, file: null,
}); });
/** Métodos */ /** Métodos */
const create = () => { const create = () => {
form.supplier_id = selectedSupplier.value?.id ?? null;
form.post(apiURL('bills'), { form.post(apiURL('bills'), {
onSuccess: (data) => { onSuccess: (data) => {
window.Notify.success(Lang('register.create.onSuccess')); window.Notify.success(Lang('register.create.onSuccess'));
@ -37,8 +49,18 @@ const create = () => {
const closeModal = () => { const closeModal = () => {
form.reset(); form.reset();
selectedSupplier.value = null;
emit('close'); emit('close');
}; };
const loadSuppliers = () => {
api.get(apiURL('proveedores'), {
onSuccess: (data) => suppliers.value = data.suppliers?.data ?? [],
});
};
/** Ciclo de vida */
onMounted(loadSuppliers);
</script> </script>
<template> <template>
@ -62,6 +84,31 @@ const closeModal = () => {
<!-- Formulario --> <!-- Formulario -->
<form @submit.prevent="create" class="space-y-4"> <form @submit.prevent="create" class="space-y-4">
<div class="grid grid-cols-2 gap-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 --> <!-- Nombre -->
<div> <div>
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5"> <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> </form>
</div> </div>
</Modal> </Modal>
<SupplierCreate
:show="showSupplierModal"
@close="showSupplierModal = false"
@created="() => { showSupplierModal = false; loadSuppliers(); }"
/>
</template> </template>

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { watch } from 'vue'; import { onMounted, ref, watch } from 'vue';
import { useForm, apiURL } from '@Services/Api'; import { useForm, useApi, apiURL } from '@Services/Api';
import { transl } from './Module'; import { transl } from './Module';
import Modal from '@Holos/Modal.vue'; 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 FormError from '@Holos/Form/Elements/Error.vue';
import SingleFile from '@Holos/Form/SingleFile.vue'; import SingleFile from '@Holos/Form/SingleFile.vue';
import GoogleIcon from '@Shared/GoogleIcon.vue'; import GoogleIcon from '@Shared/GoogleIcon.vue';
import Selectable from '@Holos/Form/Selectable.vue';
import SupplierCreate from '@Pages/POS/Suppliers/Create.vue';
/** Eventos */ /** Eventos */
const emit = defineEmits(['close', 'updated']); const emit = defineEmits(['close', 'updated']);
@ -18,17 +20,31 @@ const props = defineProps({
bill: Object 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 */ /** Formulario */
const form = useForm({ const form = useForm({
name: '', name: '',
cost: '', cost: '',
deadline: '', deadline: '',
supplier_id: null,
paid: false, paid: false,
file: null, file: null,
}); });
/** Métodos */ /** Métodos */
const update = () => { const update = () => {
form.supplier_id = selectedSupplier.value?.id ?? null;
form.post(apiURL(`bills/${props.bill.id}`), { form.post(apiURL(`bills/${props.bill.id}`), {
onSuccess: () => { onSuccess: () => {
window.Notify.success(Lang('register.edit.onSuccess')); window.Notify.success(Lang('register.edit.onSuccess'));
@ -40,9 +56,13 @@ const update = () => {
const closeModal = () => { const closeModal = () => {
form.reset(); form.reset();
selectedSupplier.value = null;
emit('close'); emit('close');
}; };
/** Ciclo de vida */
onMounted(loadSuppliers);
/** Observadores */ /** Observadores */
watch(() => props.bill, (bill) => { watch(() => props.bill, (bill) => {
if (bill) { if (bill) {
@ -51,6 +71,7 @@ watch(() => props.bill, (bill) => {
form.deadline = bill.deadline || ''; form.deadline = bill.deadline || '';
form.paid = !!bill.paid; form.paid = !!bill.paid;
form.file = null; form.file = null;
selectedSupplier.value = bill.supplier ?? null;
} }
}, { immediate: true }); }, { immediate: true });
</script> </script>
@ -76,6 +97,31 @@ watch(() => props.bill, (bill) => {
<!-- Formulario --> <!-- Formulario -->
<form @submit.prevent="update" class="space-y-4"> <form @submit.prevent="update" class="space-y-4">
<div class="grid grid-cols-2 gap-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 --> <!-- Nombre -->
<div> <div>
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5"> <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> </form>
</div> </div>
</Modal> </Modal>
<SupplierCreate
:show="showSupplierModal"
@close="showSupplierModal = false"
@created="() => { showSupplierModal = false; loadSuppliers(); }"
/>
</template> </template>

View File

@ -81,7 +81,11 @@ const togglePaid = (bill) => {
}; };
const exportPending = () => { 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 */ /** 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"> <th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{{ transl('name') }} {{ transl('name') }}
</th> </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"> <th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{{ transl('cost') }} {{ transl('cost') }}
</th> </th>
@ -148,6 +155,12 @@ onMounted(() => searcher.search());
<td class="px-6 py-4 text-center"> <td class="px-6 py-4 text-center">
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ bill.name }}</p> <p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ bill.name }}</p>
</td> </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"> <td class="px-6 py-4 text-center">
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100"> <p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ formatCurrency(bill.cost) }} {{ formatCurrency(bill.cost) }}
@ -208,7 +221,7 @@ onMounted(() => searcher.search());
</template> </template>
<template #empty> <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"> <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" /> <GoogleIcon name="receipt_long" class="text-6xl mb-2 opacity-50" />
<p class="font-semibold">{{ $t('registers.empty') }}</p> <p class="font-semibold">{{ $t('registers.empty') }}</p>