feat: agregar gestión de proveedores en la creación y edición de facturas
This commit is contained in:
parent
5dbb52a9e9
commit
53a5208cdf
@ -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',
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user