add: cambio al pagar con efectivo
This commit is contained in:
parent
cabba3621e
commit
708cc31496
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
import Modal from '@Holos/Modal.vue';
|
import Modal from '@Holos/Modal.vue';
|
||||||
|
|
||||||
@ -24,6 +24,7 @@ const emit = defineEmits(['close', 'confirm']);
|
|||||||
|
|
||||||
/** Estado */
|
/** Estado */
|
||||||
const selectedMethod = ref('cash');
|
const selectedMethod = ref('cash');
|
||||||
|
const cashReceived = ref(0);
|
||||||
|
|
||||||
/** Computados */
|
/** Computados */
|
||||||
const formattedSubtotal = computed(() => {
|
const formattedSubtotal = computed(() => {
|
||||||
@ -47,6 +48,30 @@ const formattedTotal = computed(() => {
|
|||||||
}).format(props.cart.total);
|
}).format(props.cart.total);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Cálculo de cambio
|
||||||
|
const change = computed(() => {
|
||||||
|
if (selectedMethod.value === 'cash' && cashReceived.value > 0) {
|
||||||
|
return cashReceived.value - props.cart.total;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validar que el efectivo sea suficiente
|
||||||
|
const isValidCash = computed(() => {
|
||||||
|
if (selectedMethod.value === 'cash') {
|
||||||
|
return cashReceived.value >= props.cart.total;
|
||||||
|
}
|
||||||
|
return true; // Tarjetas siempre válidas
|
||||||
|
});
|
||||||
|
|
||||||
|
// Puede confirmar si no es efectivo o si el efectivo es válido
|
||||||
|
const canConfirm = computed(() => {
|
||||||
|
if (selectedMethod.value === 'cash') {
|
||||||
|
return cashReceived.value > 0 && isValidCash.value;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
const paymentMethods = [
|
const paymentMethods = [
|
||||||
{
|
{
|
||||||
value: 'cash',
|
value: 'cash',
|
||||||
@ -71,19 +96,53 @@ const paymentMethods = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** Watchers */
|
||||||
|
// Limpiar cashReceived cuando cambia el método de pago
|
||||||
|
watch(selectedMethod, (newMethod) => {
|
||||||
|
if (newMethod !== 'cash') {
|
||||||
|
cashReceived.value = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Resetear cashReceived cuando se abre el modal
|
||||||
|
watch(() => props.show, (isShown) => {
|
||||||
|
if (isShown) {
|
||||||
|
cashReceived.value = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/** Métodos */
|
/** Métodos */
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
|
// Validar método de pago seleccionado
|
||||||
if (!selectedMethod.value) {
|
if (!selectedMethod.value) {
|
||||||
window.Notify.error('Selecciona un método de pago');
|
window.Notify.error('Selecciona un método de pago');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit('confirm', selectedMethod.value);
|
// Validaciones específicas para efectivo
|
||||||
|
if (selectedMethod.value === 'cash') {
|
||||||
|
if (!cashReceived.value || cashReceived.value <= 0) {
|
||||||
|
window.Notify.error('Ingresa el efectivo recibido');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cashReceived.value < props.cart.total) {
|
||||||
|
window.Notify.error(`El efectivo recibido es insuficiente. Faltan $${(props.cart.total - cashReceived.value).toFixed(2)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emitir evento con datos completos
|
||||||
|
emit('confirm', {
|
||||||
|
paymentMethod: selectedMethod.value,
|
||||||
|
cashReceived: selectedMethod.value === 'cash' ? parseFloat(cashReceived.value) : null
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (!props.processing) {
|
if (!props.processing) {
|
||||||
selectedMethod.value = 'cash';
|
selectedMethod.value = 'cash';
|
||||||
|
cashReceived.value = 0;
|
||||||
emit('close');
|
emit('close');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -210,6 +269,80 @@ const handleClose = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Sección de efectivo -->
|
||||||
|
<div v-if="selectedMethod === 'cash'" class="space-y-4">
|
||||||
|
<!-- Input de efectivo recibido -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-bold text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Efectivo Recibido *
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<span class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400 text-xl font-semibold">$</span>
|
||||||
|
<input
|
||||||
|
v-model.number="cashReceived"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
placeholder="0.00"
|
||||||
|
class="w-full pl-10 pr-4 py-3 text-xl font-bold border-2 border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-400"
|
||||||
|
:class="{
|
||||||
|
'border-red-500 focus:ring-red-500 focus:border-red-500': cashReceived > 0 && !isValidCash,
|
||||||
|
'border-green-500 focus:ring-green-500 focus:border-green-500': cashReceived > 0 && isValidCash
|
||||||
|
}"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Ingresa el monto en efectivo que recibiste del cliente
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mostrar cambio -->
|
||||||
|
<div v-if="cashReceived > 0" 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">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
|
||||||
|
{{ change >= 0 ? 'Cambio a devolver' : 'Falta' }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="text-4xl font-bold"
|
||||||
|
:class="{
|
||||||
|
'text-green-600 dark:text-green-400': change >= 0,
|
||||||
|
'text-red-600 dark:text-red-400': change < 0
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
${{ Math.abs(change).toFixed(2) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<GoogleIcon
|
||||||
|
:name="change >= 0 ? 'check_circle' : 'error'"
|
||||||
|
class="text-5xl"
|
||||||
|
:class="{
|
||||||
|
'text-green-500': change >= 0,
|
||||||
|
'text-red-500': change < 0
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Advertencia de dinero insuficiente -->
|
||||||
|
<div v-if="cashReceived > 0 && !isValidCash" class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<GoogleIcon name="warning" class="text-red-600 dark:text-red-400 text-xl shrink-0" />
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-semibold text-red-800 dark:text-red-300">
|
||||||
|
Efectivo insuficiente
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-red-700 dark:text-red-400 mt-1">
|
||||||
|
El dinero recibido debe ser mayor o igual al total de ${{ cart.total.toFixed(2) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
@ -225,7 +358,7 @@ const handleClose = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="handleConfirm"
|
@click="handleConfirm"
|
||||||
:disabled="processing"
|
:disabled="processing || !canConfirm"
|
||||||
class="flex items-center gap-2 px-6 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-600 shadow-lg shadow-indigo-600/30 transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none"
|
class="flex items-center gap-2 px-6 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-600 shadow-lg shadow-indigo-600/30 transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none"
|
||||||
>
|
>
|
||||||
<GoogleIcon
|
<GoogleIcon
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import Modal from '@Holos/Modal.vue';
|
import Modal from '@Holos/Modal.vue';
|
||||||
import FormInput from '@Holos/Form/Input.vue';
|
|
||||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
|
||||||
/** Props */
|
/** Props */
|
||||||
@ -35,6 +34,14 @@ const expectedCash = computed(() => initialCash.value + cashSales.value);
|
|||||||
const difference = computed(() => finalCash.value - expectedCash.value);
|
const difference = computed(() => finalCash.value - expectedCash.value);
|
||||||
const hasDifference = computed(() => Math.abs(difference.value) > 0.01);
|
const hasDifference = computed(() => Math.abs(difference.value) > 0.01);
|
||||||
|
|
||||||
|
const totalCashReceived = computed(() =>
|
||||||
|
parseFloat(props.cashRegister?.total_cash_received || 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalChangeGiven = computed(() =>
|
||||||
|
parseFloat(props.cashRegister?.total_change_given || 0)
|
||||||
|
);
|
||||||
|
|
||||||
/** Métodos */
|
/** Métodos */
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
emit('confirm', {
|
emit('confirm', {
|
||||||
@ -122,29 +129,48 @@ const handleClose = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cálculo de Efectivo -->
|
<!-- Cálculo de Efectivo -->
|
||||||
<div class="bg-gray-50 dark:bg-gray-800 rounded-xl p-5 space-y-4 border border-gray-200 dark:border-gray-700">
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-xl p-5 space-y-4">
|
||||||
<h4 class="text-sm font-bold text-gray-900 dark:text-gray-100 uppercase tracking-wide">
|
<h4 class="text-sm font-bold text-gray-900 dark:text-gray-100">
|
||||||
Resumen de Efectivo
|
Resumen de Efectivo
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="space-y-3">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-sm text-gray-600 dark:text-gray-400">Efectivo Inicial:</span>
|
<span class="text-sm text-gray-600 dark:text-gray-400">Efectivo Inicial:</span>
|
||||||
<span class="text-base font-semibold text-gray-900 dark:text-gray-100">
|
<span class="text-base font-semibold text-gray-900 dark:text-gray-100">
|
||||||
${{ (initialCash || 0).toLocaleString('es-MX', { minimumFractionDigits: 2 }) }}
|
${{ (initialCash || 0).toLocaleString('es-MX', { minimumFractionDigits: 2 }) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<span class="text-sm text-gray-600 dark:text-gray-400">Ventas en Efectivo:</span>
|
<!-- Desglose de flujo de efectivo -->
|
||||||
<span class="text-base font-semibold text-green-600 dark:text-green-400">
|
<div class="pl-4 border-l-2 border-green-500 space-y-2">
|
||||||
+ ${{ (cashSales || 0).toLocaleString('es-MX', { minimumFractionDigits: 2 }) }}
|
<div class="flex justify-between items-center text-sm">
|
||||||
|
<span class="text-gray-600 dark:text-gray-400">Efectivo Recibido:</span>
|
||||||
|
<span class="text-green-600 dark:text-green-400 font-semibold">
|
||||||
|
+ ${{ (totalCashReceived || 0).toLocaleString('es-MX', { minimumFractionDigits: 2 }) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center text-sm">
|
||||||
|
<span class="text-gray-600 dark:text-gray-400">Cambio Devuelto:</span>
|
||||||
|
<span class="text-red-600 dark:text-red-400 font-semibold">
|
||||||
|
- ${{ (totalChangeGiven || 0).toLocaleString('es-MX', { minimumFractionDigits: 2 }) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border-t border-gray-300 dark:border-gray-600 pt-3">
|
<div class="flex justify-between items-center pt-2 border-t border-gray-300">
|
||||||
|
<span class="text-sm text-gray-600 dark:text-gray-400">Ventas en Efectivo (Neto):</span>
|
||||||
|
<span class="text-base font-semibold text-green-600 dark:text-green-400">
|
||||||
|
= ${{ (cashSales || 0).toLocaleString('es-MX', { minimumFractionDigits: 2 }) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t-2 border-gray-300 dark:border-gray-600 pt-3">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-base font-bold text-gray-900 dark:text-gray-100">Efectivo Esperado:</span>
|
<span class="text-base font-bold text-gray-900 dark:text-gray-100">
|
||||||
|
Efectivo Esperado:
|
||||||
|
</span>
|
||||||
<span class="text-xl font-bold text-blue-600 dark:text-blue-400">
|
<span class="text-xl font-bold text-blue-600 dark:text-blue-400">
|
||||||
${{ (expectedCash || 0).toLocaleString('es-MX', { minimumFractionDigits: 2 }) }}
|
${{ (expectedCash || 0).toLocaleString('es-MX', { minimumFractionDigits: 2 }) }}
|
||||||
</span>
|
</span>
|
||||||
@ -158,16 +184,16 @@ const handleClose = () => {
|
|||||||
Efectivo Real en Caja *
|
Efectivo Real en Caja *
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||||
<span class="text-gray-500 dark:text-gray-400 text-lg font-semibold">$</span>
|
<span class="text-gray-500 dark:text-gray-400 text-lg font-semibold">$</span>
|
||||||
</div>
|
</div>
|
||||||
<FormInput
|
<input
|
||||||
v-model.number="finalCash"
|
v-model.number="finalCash"
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
placeholder="0.00"
|
placeholder="0.00"
|
||||||
class="pl-8 text-lg font-semibold"
|
class="w-full pl-7 pr-4 py-2.5 text-lg font-semibold border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-400"
|
||||||
required
|
required
|
||||||
autofocus
|
autofocus
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -79,12 +79,12 @@ const closeCheckout = () => {
|
|||||||
showCheckoutModal.value = false;
|
showCheckoutModal.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirmSale = async (paymentMethod) => {
|
const handleConfirmSale = async (paymentData) => {
|
||||||
processingPayment.value = true;
|
processingPayment.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Establecer método de pago
|
// Establecer método de pago
|
||||||
cart.setPaymentMethod(paymentMethod);
|
cart.setPaymentMethod(paymentData.paymentMethod);
|
||||||
|
|
||||||
// Preparar datos de la venta
|
// Preparar datos de la venta
|
||||||
const saleData = {
|
const saleData = {
|
||||||
@ -92,7 +92,7 @@ const handleConfirmSale = async (paymentMethod) => {
|
|||||||
subtotal: parseFloat(cart.subtotal.toFixed(2)),
|
subtotal: parseFloat(cart.subtotal.toFixed(2)),
|
||||||
tax: parseFloat(cart.tax.toFixed(2)),
|
tax: parseFloat(cart.tax.toFixed(2)),
|
||||||
total: parseFloat(cart.total.toFixed(2)),
|
total: parseFloat(cart.total.toFixed(2)),
|
||||||
payment_method: paymentMethod,
|
payment_method: paymentData.paymentMethod,
|
||||||
items: cart.items.map(item => ({
|
items: cart.items.map(item => ({
|
||||||
inventory_id: item.inventory_id,
|
inventory_id: item.inventory_id,
|
||||||
product_name: item.product_name,
|
product_name: item.product_name,
|
||||||
@ -102,12 +102,25 @@ const handleConfirmSale = async (paymentMethod) => {
|
|||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Agregar cash_received si es pago en efectivo
|
||||||
|
if (paymentData.paymentMethod === 'cash' && paymentData.cashReceived) {
|
||||||
|
saleData.cash_received = parseFloat(paymentData.cashReceived.toFixed(2));
|
||||||
|
}
|
||||||
|
|
||||||
// Crear venta
|
// Crear venta
|
||||||
window.Notify.info('Procesando venta...');
|
window.Notify.info('Procesando venta...');
|
||||||
const response = await salesService.createSale(saleData);
|
const response = await salesService.createSale(saleData);
|
||||||
|
|
||||||
// Éxito
|
// Mostrar mensaje de éxito con cambio si es efectivo
|
||||||
|
if (paymentData.paymentMethod === 'cash' && response.change !== undefined) {
|
||||||
|
window.Notify.success(
|
||||||
|
`¡Venta realizada! Cambio: $${parseFloat(response.change).toFixed(2)}`,
|
||||||
|
'',
|
||||||
|
8
|
||||||
|
);
|
||||||
|
} else {
|
||||||
window.Notify.success('¡Venta realizada exitosamente!');
|
window.Notify.success('¡Venta realizada exitosamente!');
|
||||||
|
}
|
||||||
|
|
||||||
if (response && response.id) {
|
if (response && response.id) {
|
||||||
try {
|
try {
|
||||||
@ -139,7 +152,14 @@ const handleConfirmSale = async (paymentMethod) => {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error en venta:', error);
|
console.error('Error en venta:', error);
|
||||||
|
|
||||||
|
// Manejar errores de validación del backend (422)
|
||||||
|
if (error.status === 422 && error.errors) {
|
||||||
|
const errorMessages = Object.values(error.errors).flat();
|
||||||
|
errorMessages.forEach(msg => window.Notify.error(msg));
|
||||||
|
} else {
|
||||||
window.Notify.error(error.message || t('cart.error'));
|
window.Notify.error(error.message || t('cart.error'));
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
processingPayment.value = false;
|
processingPayment.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,9 @@ const salesService = {
|
|||||||
api.post(apiURL('sales'), {
|
api.post(apiURL('sales'), {
|
||||||
data: saleData,
|
data: saleData,
|
||||||
onSuccess: (response) => {
|
onSuccess: (response) => {
|
||||||
resolve(response.model);
|
// El backend puede devolver: sale, model, o el objeto directo
|
||||||
|
// También incluye change, cash_received para pagos en efectivo
|
||||||
|
resolve(response.sale || response.model || response);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
reject(error);
|
reject(error);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user