feat: añadir funcionalidad de gestión de clientes con creación, edición y eliminación
This commit is contained in:
parent
5c3df890e4
commit
d469b18bf5
158
src/pages/POS/Clients/Create.vue
Normal file
158
src/pages/POS/Clients/Create.vue
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useForm, apiURL } from '@Services/Api';
|
||||||
|
|
||||||
|
import Modal from '@Holos/Modal.vue';
|
||||||
|
import FormInput from '@Holos/Form/Input.vue';
|
||||||
|
import FormError from '@Holos/Form/Elements/Error.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits(['close', 'created']);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
show: Boolean
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Formulario */
|
||||||
|
const form = useForm({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
address: '',
|
||||||
|
rfc: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const createClient = () => {
|
||||||
|
form.post(apiURL('clients'), {
|
||||||
|
onSuccess: () => {
|
||||||
|
window.Notify.success('Cliente creado exitosamente');
|
||||||
|
emit('created');
|
||||||
|
closeModal();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
window.Notify.error('Error al crear el cliente');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
form.reset();
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :show="show" max-width="md" @close="closeModal">
|
||||||
|
<div class="p-6">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||||
|
Crear Cliente
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
@click="closeModal"
|
||||||
|
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulario -->
|
||||||
|
<form @submit.prevent="createClient" class="space-y-4">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<!-- Nombre -->
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
NOMBRE
|
||||||
|
</label>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nombre del cliente"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<FormError :message="form.errors?.name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
EMAIL
|
||||||
|
</label>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.email"
|
||||||
|
type="text"
|
||||||
|
placeholder="Email"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<FormError :message="form.errors?.email" />
|
||||||
|
</div>
|
||||||
|
<!-- Teléfono -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
TELÉFONO
|
||||||
|
</label>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.phone"
|
||||||
|
type="text"
|
||||||
|
placeholder="9933428818"
|
||||||
|
maxlength="10"
|
||||||
|
/>
|
||||||
|
<FormError :message="form.errors?.phone" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dirección -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
DIRECCIÓN
|
||||||
|
</label>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.address"
|
||||||
|
type="text"
|
||||||
|
placeholder="Dirección"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<FormError :message="form.errors?.address" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RFC -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
RFC
|
||||||
|
</label>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.rfc"
|
||||||
|
type="text"
|
||||||
|
placeholder="RFC"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<FormError :message="form.errors?.rfc" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Botones -->
|
||||||
|
<div class="flex items-center justify-end gap-3 mt-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="closeModal"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="form.processing"
|
||||||
|
class="px-4 py-2 text-sm font-semibold text-white bg-gray-900 rounded-lg hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
<span v-if="form.processing">Guardando...</span>
|
||||||
|
<span v-else>Guardar</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
97
src/pages/POS/Clients/Delete.vue
Normal file
97
src/pages/POS/Clients/Delete.vue
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<script setup>
|
||||||
|
import Modal from '@Holos/Modal.vue';
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
|
||||||
|
/** Props */
|
||||||
|
const props = defineProps({
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Emits */
|
||||||
|
const emit = defineEmits(['close', 'confirm']);
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const handleConfirm = () => {
|
||||||
|
emit('confirm', props.client.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :show="show" max-width="md" @close="handleClose">
|
||||||
|
<div class="p-6">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex items-center justify-center w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/30">
|
||||||
|
<GoogleIcon name="delete_forever" class="text-2xl text-red-600 dark:text-red-400" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||||
|
Eliminar Cliente
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="handleClose"
|
||||||
|
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="space-y-5">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 text-base">
|
||||||
|
¿Estás seguro de que deseas eliminar este cliente?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div v-if="client" class="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl p-5 space-y-3">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-base font-bold text-gray-900 dark:text-gray-100 mb-1">
|
||||||
|
{{ client.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start gap-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
||||||
|
<GoogleIcon name="info" class="text-red-600 dark:text-red-400 text-xl flex-shrink-0 mt-0.5" />
|
||||||
|
<p class="text-sm text-red-800 dark:text-red-300 font-medium">
|
||||||
|
Esta acción es permanente y no se puede deshacer.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="flex items-center justify-end gap-3 mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="handleClose"
|
||||||
|
class="px-5 py-2.5 text-sm font-semibold text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-colors"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="handleConfirm"
|
||||||
|
class="flex items-center gap-2 px-5 py-2.5 bg-red-600 hover:bg-red-700 text-white text-sm font-semibold rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-600 shadow-lg shadow-red-600/30 transition-all"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="delete" class="text-xl" />
|
||||||
|
Eliminar Cliente
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
174
src/pages/POS/Clients/Edit.vue
Normal file
174
src/pages/POS/Clients/Edit.vue
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<script setup>
|
||||||
|
import { watch } from 'vue';
|
||||||
|
import { useForm, apiURL } from '@Services/Api';
|
||||||
|
|
||||||
|
import Modal from '@Holos/Modal.vue';
|
||||||
|
import FormInput from '@Holos/Form/Input.vue';
|
||||||
|
import FormError from '@Holos/Form/Elements/Error.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits(['close', 'updated']);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
show: Boolean,
|
||||||
|
client: Object
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Formulario */
|
||||||
|
const form = useForm({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
address: '',
|
||||||
|
rfc: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const updateClient = () => {
|
||||||
|
form.put(apiURL(`clients/${props.client.id}`), {
|
||||||
|
onSuccess: () => {
|
||||||
|
window.Notify.success('Cliente actualizado exitosamente');
|
||||||
|
emit('updated');
|
||||||
|
closeModal();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
window.Notify.error('Error al actualizar el cliente');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
form.reset();
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Observadores */
|
||||||
|
watch(() => props.client, (newClient) => {
|
||||||
|
if (newClient) {
|
||||||
|
form.name = newClient.name || '';
|
||||||
|
form.email = newClient.email || '';
|
||||||
|
form.phone = newClient.phone || '';
|
||||||
|
form.address = newClient.address || '';
|
||||||
|
form.rfc = newClient.rfc || '';
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :show="show" max-width="md" @close="closeModal">
|
||||||
|
<div class="p-6">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||||
|
Editar Cliente
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
@click="closeModal"
|
||||||
|
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulario -->
|
||||||
|
<form @submit.prevent="updateClient" class="space-y-4">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<!-- Nombre -->
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
NOMBRE
|
||||||
|
</label>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nombre del cliente"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<FormError :message="form.errors?.name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- EMAIL -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
EMAIL
|
||||||
|
</label>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.email"
|
||||||
|
type="email"
|
||||||
|
placeholder="Correo electrónico"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<FormError :message="form.errors?.email" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Teléfono -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
TELÉFONO
|
||||||
|
</label>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.phone"
|
||||||
|
type="tel"
|
||||||
|
placeholder="9922334455"
|
||||||
|
maxlength="10"
|
||||||
|
/>
|
||||||
|
<FormError :message="form.errors?.phone" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dirección -->
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
DIRECCIÓN
|
||||||
|
</label>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.address"
|
||||||
|
type="text"
|
||||||
|
placeholder="Dirección del cliente"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<FormError :message="form.errors?.address" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RFC -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
RFC
|
||||||
|
</label>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.rfc"
|
||||||
|
type="text"
|
||||||
|
maxlength="13"
|
||||||
|
placeholder="RFC del cliente"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<FormError :message="form.errors?.rfc" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Botones -->
|
||||||
|
<div class="flex items-center justify-between mt-6">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="closeModal"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="form.processing"
|
||||||
|
class="px-4 py-2 text-sm font-semibold text-white bg-gray-900 rounded-lg hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
<span v-if="form.processing">Actualizando...</span>
|
||||||
|
<span v-else>Actualizar</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@ -1,13 +1,23 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { useSearcher, apiURL } from '@Services/Api';
|
import { useSearcher, apiURL } from '@Services/Api';
|
||||||
|
import { can } from './Module.js';
|
||||||
|
|
||||||
import SearcherHead from '@Holos/Searcher.vue';
|
import SearcherHead from '@Holos/Searcher.vue';
|
||||||
import Table from '@Holos/Table.vue';
|
import Table from '@Holos/Table.vue';
|
||||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
import CreateModal from './Create.vue';
|
||||||
|
import EditModal from './Edit.vue';
|
||||||
|
import DeleteModal from './Delete.vue';
|
||||||
|
|
||||||
|
|
||||||
/** Estado */
|
/** Estado */
|
||||||
const clients = ref([]);
|
const clients = ref([]);
|
||||||
|
const showCreateModal = ref(false);
|
||||||
|
const showEditModal = ref(false);
|
||||||
|
const showDeleteModal = ref(false);
|
||||||
|
const editingClient = ref(null);
|
||||||
|
const deletingClient = ref(null);
|
||||||
|
|
||||||
/** Métodos */
|
/** Métodos */
|
||||||
const searcher = useSearcher({
|
const searcher = useSearcher({
|
||||||
@ -21,11 +31,11 @@ const searcher = useSearcher({
|
|||||||
/** Métodos auxiliares */
|
/** Métodos auxiliares */
|
||||||
const copyClientInfo = (client) => {
|
const copyClientInfo = (client) => {
|
||||||
const info = `
|
const info = `
|
||||||
Nombre: ${client.name}
|
Nombre: ${client.name}
|
||||||
Email: ${client.email || 'N/A'}
|
Email: ${client.email || 'N/A'}
|
||||||
Teléfono: ${client.phone || 'N/A'}
|
Teléfono: ${client.phone || 'N/A'}
|
||||||
Dirección: ${client.address || 'N/A'}
|
Dirección: ${client.address || 'N/A'}
|
||||||
RFC: ${client.rfc || 'N/A'}
|
RFC: ${client.rfc || 'N/A'}
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
navigator.clipboard.writeText(info).then(() => {
|
navigator.clipboard.writeText(info).then(() => {
|
||||||
@ -35,6 +45,61 @@ RFC: ${client.rfc || 'N/A'}
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const confirmDelete = async (id) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(apiURL(`clients/${id}`), {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${sessionStorage.token}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
window.Notify.success('Cliente eliminado exitosamente');
|
||||||
|
closeDeleteModal();
|
||||||
|
searcher.search();
|
||||||
|
} else {
|
||||||
|
window.Notify.error('Error al eliminar el cliente');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
window.Notify.error('Error al eliminar el cliente');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreateModal = () => {
|
||||||
|
showCreateModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeCreateModal = () => {
|
||||||
|
showCreateModal.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditModal = (client) => {
|
||||||
|
editingClient.value = client;
|
||||||
|
showEditModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeEditModal = () => {
|
||||||
|
showEditModal.value = false;
|
||||||
|
editingClient.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDeleteModal = (client) => {
|
||||||
|
deletingClient.value = client;
|
||||||
|
showDeleteModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDeleteModal = () => {
|
||||||
|
showDeleteModal.value = false;
|
||||||
|
deletingClient.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClientSaved = () => {
|
||||||
|
searcher.search();
|
||||||
|
};
|
||||||
|
|
||||||
/** Ciclo de vida */
|
/** Ciclo de vida */
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
searcher.search();
|
searcher.search();
|
||||||
@ -48,8 +113,17 @@ onMounted(() => {
|
|||||||
placeholder="Buscar por nombre..."
|
placeholder="Buscar por nombre..."
|
||||||
@search="(x) => searcher.search(x)"
|
@search="(x) => searcher.search(x)"
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
v-if="can('create')"
|
||||||
|
class="flex items-center gap-2 px-3 py-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold rounded-lg transition-colors shadow-sm"
|
||||||
|
@click="openCreateModal"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="add" class="text-xl" />
|
||||||
|
Nuevo Cliente
|
||||||
|
</button>
|
||||||
</SearcherHead>
|
</SearcherHead>
|
||||||
<div class="pt-2 w-full">
|
<div class="pt-2 w-full">
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
:items="clients"
|
:items="clients"
|
||||||
@send-pagination="(page) => searcher.pagination(page)"
|
@send-pagination="(page) => searcher.pagination(page)"
|
||||||
@ -60,13 +134,13 @@ onMounted(() => {
|
|||||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">TELEFONO</th>
|
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">TELEFONO</th>
|
||||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">DIRECCIÓN</th>
|
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">DIRECCIÓN</th>
|
||||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">RFC</th>
|
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">RFC</th>
|
||||||
|
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">ACCIONES</th>
|
||||||
</template>
|
</template>
|
||||||
<template #body="{items}">
|
<template #body="{items}">
|
||||||
<tr
|
<tr
|
||||||
v-for="client in items"
|
v-for="client in items"
|
||||||
:key="client.id"
|
:key="client.id"
|
||||||
@click="copyClientInfo(client)"
|
class="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||||
class="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer"
|
|
||||||
>
|
>
|
||||||
<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">{{ client.name }}</p>
|
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ client.name }}</p>
|
||||||
@ -83,10 +157,37 @@ onMounted(() => {
|
|||||||
<td class="px-6 py-4 text-center">
|
<td class="px-6 py-4 text-center">
|
||||||
<p class="text-sm text-gray-700 dark:text-gray-300">{{ client.rfc }}</p>
|
<p class="text-sm text-gray-700 dark:text-gray-300">{{ client.rfc }}</p>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||||
|
<div class="flex items-center justify-center gap-2">
|
||||||
|
<button
|
||||||
|
@click.stop="copyClientInfo(client)"
|
||||||
|
class="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300 transition-colors"
|
||||||
|
title="Copiar información"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="content_copy" class="text-xl" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="can('edit')"
|
||||||
|
@click.stop="openEditModal(client)"
|
||||||
|
class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors"
|
||||||
|
title="Editar cliente"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="edit" class="text-xl" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="can('destroy')"
|
||||||
|
@click.stop="openDeleteModal(client)"
|
||||||
|
class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 transition-colors"
|
||||||
|
title="Eliminar cliente"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="delete" class="text-xl" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<td colspan="5" class="table-cell text-center">
|
<td colspan="6" 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
|
<GoogleIcon
|
||||||
name="person"
|
name="person"
|
||||||
@ -100,5 +201,30 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Modal de Crear Cliente -->
|
||||||
|
<CreateModal
|
||||||
|
v-if="can('create')"
|
||||||
|
:show="showCreateModal"
|
||||||
|
@close="closeCreateModal"
|
||||||
|
@created="onClientSaved"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Modal de Editar Cliente -->
|
||||||
|
<EditModal
|
||||||
|
v-if="can('edit')"
|
||||||
|
:show="showEditModal"
|
||||||
|
:client="editingClient"
|
||||||
|
@close="closeEditModal"
|
||||||
|
@updated="onClientSaved"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Modal de Eliminar Cliente -->
|
||||||
|
<DeleteModal
|
||||||
|
v-if="can('destroy')"
|
||||||
|
:show="showDeleteModal"
|
||||||
|
:client="deletingClient"
|
||||||
|
@close="closeDeleteModal"
|
||||||
|
@confirm="confirmDelete"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
16
src/pages/POS/Clients/Module.js
Normal file
16
src/pages/POS/Clients/Module.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { hasPermission } from '@Plugins/RolePermission.js';
|
||||||
|
|
||||||
|
// Ruta API
|
||||||
|
const apiTo = (name, params = {}) => route(`clients.${name}`, params)
|
||||||
|
|
||||||
|
// Ruta visual
|
||||||
|
const viewTo = ({ name = '', params = {}, query = {} }) => ({ name: `pos.clients.${name}`, params, query })
|
||||||
|
|
||||||
|
// Determina si un usuario puede hacer algo en base a los permisos
|
||||||
|
const can = (permission) => hasPermission(`clients.${permission}`)
|
||||||
|
|
||||||
|
export {
|
||||||
|
can,
|
||||||
|
viewTo,
|
||||||
|
apiTo
|
||||||
|
}
|
||||||
@ -28,9 +28,7 @@ const emit = defineEmits(['close', 'created']);
|
|||||||
const cashRegisterStore = useCashRegister();
|
const cashRegisterStore = useCashRegister();
|
||||||
|
|
||||||
/** Estado */
|
/** Estado */
|
||||||
const step = ref(1); // 1: Buscar venta, 2: Seleccionar items
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const saleId = ref('');
|
|
||||||
const saleData = ref(null);
|
const saleData = ref(null);
|
||||||
const returnableItems = ref([]);
|
const returnableItems = ref([]);
|
||||||
const selectedItems = ref([]);
|
const selectedItems = ref([]);
|
||||||
@ -40,8 +38,9 @@ const notes = ref('');
|
|||||||
|
|
||||||
/** Opciones de motivo */
|
/** Opciones de motivo */
|
||||||
const reasonOptions = [
|
const reasonOptions = [
|
||||||
{ value: 'change_of_mind', label: 'Cambio de opinión' },
|
{ value: 'defective', label: 'Producto defectuoso' },
|
||||||
{ value: 'wrong_product', label: 'Producto incorrecto' },
|
{ value: 'wrong_product', label: 'Producto incorrecto' },
|
||||||
|
{ value: 'change_of_mind', label: 'Cambio de opinión' },
|
||||||
{ value: 'damaged', label: 'Producto dañado' },
|
{ value: 'damaged', label: 'Producto dañado' },
|
||||||
{ value: 'other', label: 'Otro' }
|
{ value: 'other', label: 'Otro' }
|
||||||
];
|
];
|
||||||
@ -51,7 +50,7 @@ const hasSelectedItems = computed(() => {
|
|||||||
return selectedItems.value.some(item => item.selected && item.quantity > 0);
|
return selectedItems.value.some(item => item.selected && item.quantity > 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalReturn = computed(() => {
|
const subtotalReturn = computed(() => {
|
||||||
return selectedItems.value
|
return selectedItems.value
|
||||||
.filter(item => item.selected && item.quantity > 0)
|
.filter(item => item.selected && item.quantity > 0)
|
||||||
.reduce((total, item) => {
|
.reduce((total, item) => {
|
||||||
@ -59,6 +58,19 @@ const totalReturn = computed(() => {
|
|||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const taxReturn = computed(() => {
|
||||||
|
if (!saleData.value || !saleData.value.subtotal || !saleData.value.tax) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// Calcular impuesto proporcional basado en la tasa de la venta original
|
||||||
|
const taxRate = parseFloat(saleData.value.tax) / parseFloat(saleData.value.subtotal);
|
||||||
|
return subtotalReturn.value * taxRate;
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalReturn = computed(() => {
|
||||||
|
return subtotalReturn.value + taxReturn.value;
|
||||||
|
});
|
||||||
|
|
||||||
const canSubmit = computed(() => {
|
const canSubmit = computed(() => {
|
||||||
return hasSelectedItems.value && reason.value !== '';
|
return hasSelectedItems.value && reason.value !== '';
|
||||||
});
|
});
|
||||||
@ -75,8 +87,6 @@ watch(() => props.show, (isShown) => {
|
|||||||
|
|
||||||
/** Métodos */
|
/** Métodos */
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
step.value = props.sale ? 2 : 1;
|
|
||||||
saleId.value = '';
|
|
||||||
saleData.value = null;
|
saleData.value = null;
|
||||||
returnableItems.value = [];
|
returnableItems.value = [];
|
||||||
selectedItems.value = [];
|
selectedItems.value = [];
|
||||||
@ -87,17 +97,15 @@ const resetForm = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const searchSale = async () => {
|
const searchSale = async () => {
|
||||||
const id = props.sale?.id || saleId.value;
|
if (!props.sale?.id) {
|
||||||
|
window.Notify.error('No se proporcionó una venta válida');
|
||||||
if (!id || (!props.sale && typeof id === 'string' && !id.trim())) {
|
|
||||||
window.Notify.error('Ingresa el ID o folio de la venta');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await returnsService.getReturnableItems(id);
|
const response = await returnsService.getReturnableItems(props.sale.id);
|
||||||
|
|
||||||
// Guardar datos de la venta
|
// Guardar datos de la venta
|
||||||
saleData.value = response.sale || null;
|
saleData.value = response.sale || null;
|
||||||
@ -122,14 +130,12 @@ const searchSale = async () => {
|
|||||||
quantity_sold: item.quantity_sold,
|
quantity_sold: item.quantity_sold,
|
||||||
quantity_already_returned: item.quantity_already_returned,
|
quantity_already_returned: item.quantity_already_returned,
|
||||||
quantity_returnable: item.quantity_returnable,
|
quantity_returnable: item.quantity_returnable,
|
||||||
available_serials: item.available_serials || [],
|
available_serials: item.serials || [],
|
||||||
// Estado de selección
|
// Estado de selección
|
||||||
selected: false,
|
selected: false,
|
||||||
quantity: 0,
|
quantity: 0,
|
||||||
selectedSerials: []
|
selectedSerials: []
|
||||||
}));
|
}));
|
||||||
|
|
||||||
step.value = 2;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error al buscar venta:', error);
|
console.error('Error al buscar venta:', error);
|
||||||
window.Notify.error('No se encontró la venta o no está disponible para devolución');
|
window.Notify.error('No se encontró la venta o no está disponible para devolución');
|
||||||
@ -188,17 +194,6 @@ const hasSerials = (item) => {
|
|||||||
return item.available_serials && item.available_serials.length > 0;
|
return item.available_serials && item.available_serials.length > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const goBack = () => {
|
|
||||||
if (props.sale) {
|
|
||||||
emit('close');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
step.value = 1;
|
|
||||||
selectedItems.value = [];
|
|
||||||
returnableItems.value = [];
|
|
||||||
saleData.value = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
resetForm();
|
resetForm();
|
||||||
emit('close');
|
emit('close');
|
||||||
@ -210,6 +205,12 @@ const submitReturn = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validar que haya una caja abierta
|
||||||
|
if (!cashRegisterStore.hasOpenRegister) {
|
||||||
|
window.Notify.error('Debes tener una caja abierta para procesar devoluciones');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -222,11 +223,12 @@ const submitReturn = async () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const returnData = {
|
const returnData = {
|
||||||
sale_id: saleData.value?.id || parseInt(saleId.value),
|
sale_id: saleData.value?.id,
|
||||||
user_id: page.user.id,
|
user_id: page.user.id,
|
||||||
cash_register_id: cashRegisterStore.currentRegisterId || null,
|
cash_register_id: cashRegisterStore.currentRegisterId || null,
|
||||||
refund_method: saleData.value?.payment_method || 'cash',
|
refund_method: saleData.value?.payment_method || 'cash',
|
||||||
reason: reason.value,
|
reason: reason.value,
|
||||||
|
reason_text: reasonText.value.trim() || null,
|
||||||
notes: notes.value.trim() || null,
|
notes: notes.value.trim() || null,
|
||||||
items: items
|
items: items
|
||||||
};
|
};
|
||||||
@ -257,7 +259,7 @@ const submitReturn = async () => {
|
|||||||
Nueva Devolución
|
Nueva Devolución
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{{ step === 1 ? 'Buscar venta' : 'Seleccionar productos a devolver' }}
|
Seleccionar productos a devolver
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -269,30 +271,8 @@ const submitReturn = async () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Indicador de pasos -->
|
|
||||||
<div v-if="!props.sale" class="flex items-center gap-2 mb-6">
|
|
||||||
<div
|
|
||||||
class="flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-medium"
|
|
||||||
:class="step >= 1 ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400' : 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400'"
|
|
||||||
>
|
|
||||||
<span class="w-5 h-5 rounded-full bg-indigo-600 text-white text-xs flex items-center justify-center">1</span>
|
|
||||||
Buscar Venta
|
|
||||||
</div>
|
|
||||||
<GoogleIcon name="chevron_right" class="text-gray-400" />
|
|
||||||
<div
|
|
||||||
class="flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-medium"
|
|
||||||
:class="step >= 2 ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400' : 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400'"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="w-5 h-5 rounded-full text-xs flex items-center justify-center"
|
|
||||||
:class="step >= 2 ? 'bg-indigo-600 text-white' : 'bg-gray-300 dark:bg-gray-600 text-gray-600 dark:text-gray-300'"
|
|
||||||
>2</span>
|
|
||||||
Seleccionar Items
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Seleccionar Items -->
|
<!-- Seleccionar Items -->
|
||||||
<div v-if="step === 2" class="space-y-4">
|
<div class="space-y-4">
|
||||||
|
|
||||||
<!-- Info de la venta -->
|
<!-- Info de la venta -->
|
||||||
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 mb-4">
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 mb-4">
|
||||||
@ -300,7 +280,7 @@ const submitReturn = async () => {
|
|||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">Venta seleccionada:</p>
|
<p class="text-sm text-gray-500 dark:text-gray-400">Venta seleccionada:</p>
|
||||||
<p class="text-lg font-semibold text-indigo-600 dark:text-indigo-400 font-mono">
|
<p class="text-lg font-semibold text-indigo-600 dark:text-indigo-400 font-mono">
|
||||||
{{ saleData?.invoice_number || `#${saleId}` }}
|
{{ saleData?.invoice_number || '-' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
@ -459,13 +439,29 @@ const submitReturn = async () => {
|
|||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Total a devolver -->
|
<!-- Desglose de totales a devolver -->
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<div class="bg-indigo-50 dark:bg-indigo-900/20 border border-indigo-200 dark:border-indigo-800 rounded-lg px-6 py-4">
|
<div class="bg-indigo-50 dark:bg-indigo-900/20 border border-indigo-200 dark:border-indigo-800 rounded-lg px-6 py-4 min-w-64">
|
||||||
<p class="text-xs text-indigo-600 dark:text-indigo-400 uppercase mb-1">Total a Devolver</p>
|
<div class="space-y-2">
|
||||||
<p class="text-2xl font-bold text-indigo-700 dark:text-indigo-300">
|
<div class="flex justify-between text-sm">
|
||||||
|
<span class="text-indigo-600 dark:text-indigo-400">Subtotal:</span>
|
||||||
|
<span class="text-indigo-700 dark:text-indigo-300 font-medium">
|
||||||
|
{{ formatCurrency(subtotalReturn) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<span class="text-indigo-600 dark:text-indigo-400">IVA:</span>
|
||||||
|
<span class="text-indigo-700 dark:text-indigo-300 font-medium">
|
||||||
|
{{ formatCurrency(taxReturn) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between pt-2 border-t border-indigo-200 dark:border-indigo-700">
|
||||||
|
<span class="text-xs text-indigo-600 dark:text-indigo-400 uppercase font-semibold">Total a Devolver</span>
|
||||||
|
<span class="text-2xl font-bold text-indigo-700 dark:text-indigo-300">
|
||||||
{{ formatCurrency(totalReturn) }}
|
{{ formatCurrency(totalReturn) }}
|
||||||
</p>
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -480,7 +476,6 @@ const submitReturn = async () => {
|
|||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="step === 2"
|
|
||||||
type="button"
|
type="button"
|
||||||
:disabled="!canSubmit || loading"
|
:disabled="!canSubmit || loading"
|
||||||
class="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-400 disabled:cursor-not-allowed text-white text-sm font-semibold rounded-lg transition-colors flex items-center gap-2"
|
class="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-400 disabled:cursor-not-allowed text-white text-sm font-semibold rounded-lg transition-colors flex items-center gap-2"
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import SearcherHead from '@Holos/Searcher.vue';
|
|||||||
import Table from '@Holos/Table.vue';
|
import Table from '@Holos/Table.vue';
|
||||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
import ReturnDetailModal from './ReturnDetail.vue';
|
import ReturnDetailModal from './ReturnDetail.vue';
|
||||||
import CreateReturnModal from './CreateReturn.vue';
|
|
||||||
|
|
||||||
/** Estado */
|
/** Estado */
|
||||||
const models = ref([]);
|
const models = ref([]);
|
||||||
@ -20,10 +19,9 @@ const selectedReturn = ref(null);
|
|||||||
const searcher = useSearcher({
|
const searcher = useSearcher({
|
||||||
url: apiURL('returns'),
|
url: apiURL('returns'),
|
||||||
onSuccess: (r) => {
|
onSuccess: (r) => {
|
||||||
models.value = r.returns || r.models || { data: [], total: 0 };
|
models.value = r.returns || r;
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error('❌ ERROR al cargar devoluciones:', error);
|
|
||||||
models.value = { data: [], total: 0 };
|
models.value = { data: [], total: 0 };
|
||||||
window.Notify.error('Error al cargar devoluciones');
|
window.Notify.error('Error al cargar devoluciones');
|
||||||
}
|
}
|
||||||
@ -32,8 +30,9 @@ const searcher = useSearcher({
|
|||||||
/** Métodos */
|
/** Métodos */
|
||||||
const openDetailModal = async (returnItem) => {
|
const openDetailModal = async (returnItem) => {
|
||||||
try {
|
try {
|
||||||
const details = await returnsService.getReturnDetails(returnItem.id);
|
const response = await returnsService.getReturnDetails(returnItem.id);
|
||||||
selectedReturn.value = details;
|
// Backend retorna: { model: {...} }
|
||||||
|
selectedReturn.value = response.model || response;
|
||||||
showDetailModal.value = true;
|
showDetailModal.value = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error al cargar detalles:', error);
|
console.error('Error al cargar detalles:', error);
|
||||||
@ -51,26 +50,13 @@ const onReturnCancelled = () => {
|
|||||||
searcher.search();
|
searcher.search();
|
||||||
};
|
};
|
||||||
|
|
||||||
const openCreateModal = () => {
|
|
||||||
showCreateModal.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeCreateModal = () => {
|
|
||||||
showCreateModal.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onReturnCreated = () => {
|
|
||||||
closeCreateModal();
|
|
||||||
searcher.search();
|
|
||||||
window.Notify.success('Devolución creada exitosamente');
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Helpers de método de reembolso */
|
/** Helpers de método de reembolso */
|
||||||
const getRefundMethodLabel = (method) => {
|
const getRefundMethodLabel = (method) => {
|
||||||
const labels = {
|
const labels = {
|
||||||
cash: 'Efectivo',
|
cash: 'Efectivo',
|
||||||
card: 'Tarjeta',
|
credit_card: 'Credito',
|
||||||
store_credit: 'Crédito tienda'
|
debit_card: 'Debito'
|
||||||
};
|
};
|
||||||
return labels[method] || method || '-';
|
return labels[method] || method || '-';
|
||||||
};
|
};
|
||||||
@ -97,13 +83,6 @@ onMounted(() => {
|
|||||||
placeholder="Buscar por folio de venta o usuario..."
|
placeholder="Buscar por folio de venta o usuario..."
|
||||||
@search="(x) => searcher.search(x)"
|
@search="(x) => searcher.search(x)"
|
||||||
>
|
>
|
||||||
<button
|
|
||||||
class="flex items-center gap-2 px-3 py-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold rounded-lg transition-colors shadow-sm"
|
|
||||||
@click="openCreateModal"
|
|
||||||
>
|
|
||||||
<GoogleIcon name="add" class="text-xl" />
|
|
||||||
Nueva Devolución
|
|
||||||
</button>
|
|
||||||
</SearcherHead>
|
</SearcherHead>
|
||||||
<div class="pt-2 w-full">
|
<div class="pt-2 w-full">
|
||||||
<Table
|
<Table
|
||||||
@ -201,11 +180,4 @@ onMounted(() => {
|
|||||||
@close="closeDetailModal"
|
@close="closeDetailModal"
|
||||||
@cancelled="onReturnCancelled"
|
@cancelled="onReturnCancelled"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Modal de Crear Devolución -->
|
|
||||||
<CreateReturnModal
|
|
||||||
:show="showCreateModal"
|
|
||||||
@close="closeCreateModal"
|
|
||||||
@created="onReturnCreated"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
@ -77,8 +77,9 @@ const getRefundMethodIcon = (method) => {
|
|||||||
const getReasonLabel = (reason) => {
|
const getReasonLabel = (reason) => {
|
||||||
const labels = {
|
const labels = {
|
||||||
defective: 'Producto defectuoso',
|
defective: 'Producto defectuoso',
|
||||||
wrong_item: 'Producto incorrecto',
|
wrong_product: 'Producto incorrecto',
|
||||||
not_needed: 'Ya no lo necesita',
|
change_of_mind: 'Cambio de opinión',
|
||||||
|
damaged: 'Producto dañado',
|
||||||
other: 'Otro'
|
other: 'Otro'
|
||||||
};
|
};
|
||||||
return labels[reason] || reason || '-';
|
return labels[reason] || reason || '-';
|
||||||
|
|||||||
@ -14,7 +14,7 @@ const returnsService = {
|
|||||||
api.get(apiURL('returns'), {
|
api.get(apiURL('returns'), {
|
||||||
params: filters,
|
params: filters,
|
||||||
onSuccess: (response) => {
|
onSuccess: (response) => {
|
||||||
resolve(response.returns || response.models || response);
|
resolve(response);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
reject(error);
|
reject(error);
|
||||||
@ -32,7 +32,7 @@ const returnsService = {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
api.get(apiURL(`returns/${returnId}`), {
|
api.get(apiURL(`returns/${returnId}`), {
|
||||||
onSuccess: (response) => {
|
onSuccess: (response) => {
|
||||||
resolve(response.return || response.model || response);
|
resolve(response);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
reject(error);
|
reject(error);
|
||||||
@ -69,7 +69,7 @@ const returnsService = {
|
|||||||
api.post(apiURL('returns'), {
|
api.post(apiURL('returns'), {
|
||||||
data: returnData,
|
data: returnData,
|
||||||
onSuccess: (response) => {
|
onSuccess: (response) => {
|
||||||
resolve(response.return || response.model || response);
|
resolve(response);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
reject(error);
|
reject(error);
|
||||||
@ -87,7 +87,7 @@ const returnsService = {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
api.put(apiURL(`returns/${returnId}/cancel`), {
|
api.put(apiURL(`returns/${returnId}/cancel`), {
|
||||||
onSuccess: (response) => {
|
onSuccess: (response) => {
|
||||||
resolve(response.return || response.model || response);
|
resolve(response);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
reject(error);
|
reject(error);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user