Se agregó la funcionalidad de búsqueda de clientes para recuperar sus detalles y descuentos. Se calcula el total estimado aplicando el descuento del cliente. Se actualizó la visualización del total para mostrar los montos con descuento cuando corresponda. Se mejoró la validación del pago para considerar los descuentos en los pagos en efectivo. Se emiten los datos del cliente durante la confirmación del pago para su procesamiento en el backend. feat: Crear componente ToggleButton para la activación de niveles (tiers) Se implementó el componente ToggleButton para activar/desactivar los niveles de los clientes. Se integró la funcionalidad de alternancia (toggle) con la API para actualizar el estado del nivel. refactor: Actualizar páginas de Clientes y Niveles para la nueva estructura de niveles Se ajustaron las páginas de Clientes y Niveles para reflejar las nuevas propiedades y estructura de los niveles. Se actualizaron los elementos de la interfaz (UI) para mostrar información del nivel, incluyendo descuentos y límites de compra. Se mejoró el modal de confirmación de eliminación para reflejar el contexto de la eliminación del nivel. fix: Formatear y mostrar correctamente la información del nivel en las vistas Stats e Index Se corrigió la visualización de los nombres de los niveles y los porcentajes de descuento en varios componentes. Se aseguró el manejo adecuado de los datos de los niveles en formularios y modales. chore: Actualizar ticketService para incluir información de descuentos en los recibos Se agregó lógica para mostrar los detalles del descuento en los tickets impresos si corresponde.
590 lines
28 KiB
Vue
590 lines
28 KiB
Vue
<script setup>
|
||
import { ref, computed, watch } from 'vue';
|
||
import { useApi, apiURL } from '@Services/Api';
|
||
import { formatCurrency } from '@/utils/formatters';
|
||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||
import Modal from '@Holos/Modal.vue';
|
||
|
||
/** Props */
|
||
const props = defineProps({
|
||
show: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
cart: {
|
||
type: Object,
|
||
required: true
|
||
},
|
||
processing: {
|
||
type: Boolean,
|
||
default: false
|
||
}
|
||
});
|
||
|
||
/** Emits */
|
||
const emit = defineEmits(['close', 'confirm']);
|
||
|
||
/** Estado */
|
||
const selectedMethod = ref('cash');
|
||
const cashReceived = ref(0);
|
||
const clientNumber = ref('');
|
||
const selectedClient = ref(null);
|
||
const searchingClient = ref(false);
|
||
const clientNotFound = ref(false);
|
||
|
||
/** Computados */
|
||
const formattedSubtotal = computed(() => {
|
||
return new Intl.NumberFormat('es-MX', {
|
||
style: 'currency',
|
||
currency: 'MXN'
|
||
}).format(props.cart.subtotal);
|
||
});
|
||
|
||
const formattedTax = computed(() => {
|
||
return new Intl.NumberFormat('es-MX', {
|
||
style: 'currency',
|
||
currency: 'MXN'
|
||
}).format(props.cart.tax);
|
||
});
|
||
|
||
// Descuento del cliente
|
||
const clientDiscount = computed(() => {
|
||
if (selectedClient.value?.tier?.discount_percentage) {
|
||
return parseFloat(selectedClient.value.tier.discount_percentage);
|
||
}
|
||
return 0;
|
||
});
|
||
|
||
const estimatedDiscountAmount = computed(() => {
|
||
return (props.cart.subtotal * clientDiscount.value) / 100;
|
||
});
|
||
|
||
// Total sin descuento
|
||
const formattedTotal = computed(() => {
|
||
return new Intl.NumberFormat('es-MX', {
|
||
style: 'currency',
|
||
currency: 'MXN'
|
||
}).format(props.cart.total);
|
||
});
|
||
|
||
// Cálculo de cambio (usando total sin descuento)
|
||
const change = computed(() => {
|
||
if (selectedMethod.value === 'cash' && cashReceived.value > 0) {
|
||
return cashReceived.value - estimatedTotalWithDiscount.value;
|
||
}
|
||
return 0;
|
||
});
|
||
|
||
// Validar que el efectivo sea suficiente
|
||
const isValidCash = computed(() => {
|
||
if (selectedMethod.value === 'cash') {
|
||
return cashReceived.value >= estimatedTotalWithDiscount.value;
|
||
}
|
||
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 = [
|
||
{
|
||
value: 'cash',
|
||
label: 'Efectivo',
|
||
subtitle: 'Pago en efectivo',
|
||
icon: 'payments',
|
||
color: 'bg-green-500'
|
||
},
|
||
{
|
||
value: 'credit_card',
|
||
label: 'Tarjeta de Crédito',
|
||
subtitle: 'Pago con tarjeta de crédito',
|
||
icon: 'credit_card',
|
||
color: 'bg-blue-500'
|
||
},
|
||
{
|
||
value: 'debit_card',
|
||
label: 'Tarjeta de Débito',
|
||
subtitle: 'Pago con tarjeta de débito',
|
||
icon: 'credit_card',
|
||
color: 'bg-purple-500'
|
||
}
|
||
];
|
||
|
||
/** Métodos de búsqueda de cliente */
|
||
const searchClient = () => {
|
||
if (!clientNumber.value || clientNumber.value.trim() === '') {
|
||
selectedClient.value = null;
|
||
clientNotFound.value = false;
|
||
return;
|
||
}
|
||
|
||
searchingClient.value = true;
|
||
clientNotFound.value = false;
|
||
|
||
const api = useApi();
|
||
let urlParams = `client_number=${clientNumber.value.trim()}&with=tier`;
|
||
api.get(apiURL(`clients?${urlParams}`), {
|
||
onSuccess: (data) => {
|
||
if (data.clients && data.clients.data.length > 0) {
|
||
const client = data.clients.data[0];
|
||
selectedClient.value = client;
|
||
clientNotFound.value = false;
|
||
window.Notify.success(`Cliente ${client.name} encontrado`);
|
||
} else {
|
||
selectedClient.value = null;
|
||
clientNotFound.value = true;
|
||
}
|
||
},
|
||
onFail: (data) => {
|
||
selectedClient.value = null;
|
||
clientNotFound.value = true;
|
||
window.Notify.error(data.message || 'Error al buscar cliente');
|
||
},
|
||
onError: () => {
|
||
selectedClient.value = null;
|
||
clientNotFound.value = true;
|
||
},
|
||
onFinish: () => {
|
||
searchingClient.value = false;
|
||
}
|
||
});
|
||
};
|
||
|
||
const clearClient = () => {
|
||
clientNumber.value = '';
|
||
selectedClient.value = null;
|
||
clientNotFound.value = false;
|
||
};
|
||
|
||
|
||
|
||
/** Watchers */
|
||
// Limpiar cashReceived cuando cambia el método de pago
|
||
watch(selectedMethod, (newMethod) => {
|
||
if (newMethod !== 'cash') {
|
||
cashReceived.value = 0;
|
||
}
|
||
});
|
||
|
||
// Resetear cuando se abre el modal
|
||
watch(() => props.show, (isShown) => {
|
||
if (isShown) {
|
||
cashReceived.value = 0;
|
||
clientNumber.value = '';
|
||
selectedClient.value = null;
|
||
clientNotFound.value = false;
|
||
}
|
||
});
|
||
|
||
/** Métodos */
|
||
const handleConfirm = () => {
|
||
// Validar método de pago seleccionado
|
||
if (!selectedMethod.value) {
|
||
window.Notify.error('Selecciona un método de pago');
|
||
return;
|
||
}
|
||
|
||
// Validaciones específicas para efectivo (usando total sin descuento)
|
||
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 client_number para que el backend aplique el descuento automáticamente
|
||
emit('confirm', {
|
||
paymentMethod: selectedMethod.value,
|
||
cashReceived: selectedMethod.value === 'cash' ? parseFloat(cashReceived.value) : null,
|
||
clientNumber: selectedClient.value ? selectedClient.value.client_number : null,
|
||
clientData: selectedClient.value // Enviar datos completos del cliente para referencia
|
||
});
|
||
};
|
||
|
||
const handleClose = () => {
|
||
if (!props.processing) {
|
||
selectedMethod.value = 'cash';
|
||
cashReceived.value = 0;
|
||
clearClient();
|
||
emit('close');
|
||
}
|
||
};
|
||
|
||
// Total estimado con descuento (para mostrar al operador)
|
||
const estimatedTotalWithDiscount = computed(() => {
|
||
if (clientDiscount.value > 0) {
|
||
return props.cart.total - estimatedDiscountAmount.value;
|
||
}
|
||
return props.cart.total;
|
||
});
|
||
|
||
const formattedEstimatedTotal = computed(() => {
|
||
return new Intl.NumberFormat('es-MX', {
|
||
style: 'currency',
|
||
currency: 'MXN'
|
||
}).format(estimatedTotalWithDiscount.value);
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<Modal :show="show" max-width="xl" @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-14 h-14 rounded-full bg-indigo-100 dark:bg-indigo-900/30">
|
||
<GoogleIcon name="point_of_sale" class="text-3xl text-indigo-600 dark:text-indigo-400" />
|
||
</div>
|
||
<div>
|
||
<h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">
|
||
Cobrar
|
||
</h3>
|
||
<p class="text-sm text-gray-500 dark:text-gray-400">Resumen de compra y método de pago</p>
|
||
</div>
|
||
</div>
|
||
<button
|
||
@click="handleClose"
|
||
:disabled="processing"
|
||
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors disabled:opacity-50"
|
||
>
|
||
<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-6">
|
||
<!-- Resumen de compra -->
|
||
<div>
|
||
<h4 class="text-sm font-bold text-gray-700 dark:text-gray-300 mb-3">
|
||
Resumen de compra
|
||
</h4>
|
||
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-4 space-y-3">
|
||
<!-- Items -->
|
||
<div class="space-y-2 max-h-56 overflow-y-auto">
|
||
<div
|
||
v-for="item in cart.items"
|
||
:key="item.inventory_id"
|
||
class="flex items-start justify-between py-2"
|
||
>
|
||
<div class="flex-1 min-w-0 pr-4">
|
||
<p class="text-sm font-semibold text-gray-800 dark:text-gray-200 truncate">
|
||
{{ item.product_name }}
|
||
</p>
|
||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||
{{ item.quantity }} × ${{ item.unit_price.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
|
||
</p>
|
||
</div>
|
||
<span class="text-base font-bold text-gray-900 dark:text-gray-100 shrink-0">
|
||
${{ (item.quantity * item.unit_price).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Totales -->
|
||
<div class="pt-3 border-t border-gray-300 dark:border-gray-600 space-y-2">
|
||
<div class="flex items-center justify-between text-sm">
|
||
<span class="text-gray-600 dark:text-gray-400">Subtotal</span>
|
||
<span class="font-semibold text-gray-800 dark:text-gray-200">{{ formattedSubtotal }}</span>
|
||
</div>
|
||
<div class="flex items-center justify-between text-sm">
|
||
<span class="text-gray-600 dark:text-gray-400">IVA (16%)</span>
|
||
<span class="font-semibold text-gray-800 dark:text-gray-200">{{ formattedTax }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Descuento del cliente (informativo) -->
|
||
<div v-if="selectedClient && clientDiscount > 0" class="pt-3 border-t border-gray-300 dark:border-gray-600">
|
||
<div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-700 rounded-lg p-3">
|
||
<div class="flex items-center gap-2 mb-1">
|
||
<GoogleIcon name="info" class="text-green-600 dark:text-green-400 text-sm" />
|
||
<span class="text-xs text-green-700 dark:text-green-300 font-semibold">Descuento a aplicar</span>
|
||
</div>
|
||
<div class="flex items-center justify-between text-sm">
|
||
<span class="text-green-600 dark:text-green-400 font-semibold">{{ clientDiscount.toFixed(2) }}% ({{ selectedClient.tier?.tier_name }})</span>
|
||
<span class="font-semibold text-green-600 dark:text-green-400">≈ -${{ estimatedDiscountAmount.toFixed(2) }}</span>
|
||
</div>
|
||
<p class="text-xs text-green-700 dark:text-green-400 mt-1">
|
||
El descuento se aplicará automáticamente al procesar la venta
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Total a pagar -->
|
||
<div class="pt-3 border-t-2 border-gray-300 dark:border-gray-600 space-y-2">
|
||
<!-- Total original (tachado si hay descuento) -->
|
||
<div v-if="selectedClient && clientDiscount > 0" class="flex items-center justify-between">
|
||
<span class="text-sm text-gray-500 dark:text-gray-400">Total sin descuento</span>
|
||
<span class="text-lg text-gray-400 line-through">{{ formattedTotal }}</span>
|
||
</div>
|
||
|
||
<!-- Total final (con o sin descuento) -->
|
||
<div class="flex items-center justify-between">
|
||
<span class="text-base font-bold text-gray-800 dark:text-gray-200">
|
||
{{ selectedClient && clientDiscount > 0 ? 'Total con descuento' : 'Total a pagar' }}
|
||
</span>
|
||
<span
|
||
class="text-3xl font-bold"
|
||
:class="selectedClient && clientDiscount > 0
|
||
? 'text-green-600 dark:text-green-400'
|
||
: 'text-indigo-600 dark:text-indigo-400'"
|
||
>
|
||
{{ selectedClient && clientDiscount > 0 ? formattedEstimatedTotal : formattedTotal }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sección de Cliente -->
|
||
<div>
|
||
<h4 class="text-sm font-bold text-gray-700 dark:text-gray-300 mb-3">
|
||
Cliente (Opcional)
|
||
</h4>
|
||
|
||
<!-- Input de búsqueda de cliente -->
|
||
<div v-if="!selectedClient" class="space-y-2">
|
||
<div class="relative">
|
||
<input
|
||
v-model="clientNumber"
|
||
@keyup.enter="searchClient"
|
||
type="text"
|
||
placeholder="Ingresa el código de cliente (ej: CLI-0001)"
|
||
class="w-full px-4 py-3 pr-24 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': clientNotFound
|
||
}"
|
||
:disabled="searchingClient"
|
||
/>
|
||
<button
|
||
@click="searchClient"
|
||
:disabled="searchingClient || !clientNumber"
|
||
class="absolute right-2 top-1/2 -translate-y-1/2 px-4 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
{{ searchingClient ? 'Buscando...' : 'Buscar' }}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Error de cliente no encontrado -->
|
||
<div v-if="clientNotFound" 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-3">
|
||
<GoogleIcon name="error" class="text-red-600 dark:text-red-400 text-lg shrink-0 mt-0.5" />
|
||
<p class="text-sm text-red-800 dark:text-red-300">
|
||
Cliente no encontrado. Verifica el código e intenta nuevamente.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Cliente seleccionado -->
|
||
<div v-else class="bg-linear-to-br from-indigo-50 to-indigo-100 dark:from-indigo-900/20 dark:to-indigo-800/20 border border-indigo-200 dark:border-indigo-800 rounded-xl p-4 space-y-3">
|
||
<div class="flex items-start justify-between">
|
||
<div class="flex items-center gap-3">
|
||
<div class="w-12 h-12 rounded-full bg-indigo-600 flex items-center justify-center text-white font-bold text-lg">
|
||
{{ selectedClient.name.charAt(0).toUpperCase() }}
|
||
</div>
|
||
<div>
|
||
<p class="text-base font-bold text-gray-900 dark:text-gray-100">
|
||
{{ selectedClient.name }}
|
||
</p>
|
||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||
{{ selectedClient.client_number }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<button
|
||
@click="clearClient"
|
||
class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
|
||
title="Quitar cliente"
|
||
>
|
||
<GoogleIcon name="close" class="text-xl" />
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Tier y estadísticas -->
|
||
<div class="grid grid-cols-2 gap-2">
|
||
<div class="bg-white/50 dark:bg-gray-900/20 rounded-lg p-2">
|
||
<p class="text-xs text-gray-600 dark:text-gray-400">Tier actual</p>
|
||
<p class="text-sm font-bold text-gray-900 dark:text-gray-100">
|
||
{{ selectedClient.tier?.tier_name || 'Sin tier' }}
|
||
</p>
|
||
</div>
|
||
<div class="bg-white/50 dark:bg-gray-900/20 rounded-lg p-2">
|
||
<p class="text-xs text-gray-600 dark:text-gray-400">Descuento</p>
|
||
<p class="text-sm font-bold text-green-600 dark:text-green-400">
|
||
{{ clientDiscount.toFixed(2) }}%
|
||
</p>
|
||
</div>
|
||
<div v-if="selectedClient.total_purchases !== undefined" class="bg-white/50 dark:bg-gray-900/20 rounded-lg p-2">
|
||
<p class="text-xs text-gray-600 dark:text-gray-400">Compras acumuladas</p>
|
||
<p class="text-sm font-bold text-gray-900 dark:text-gray-100">
|
||
{{ formatCurrency(selectedClient.total_purchases) }}
|
||
</p>
|
||
</div>
|
||
<div v-if="selectedClient.total_transactions !== undefined" class="bg-white/50 dark:bg-gray-900/20 rounded-lg p-2">
|
||
<p class="text-xs text-gray-600 dark:text-gray-400">Transacciones</p>
|
||
<p class="text-sm font-bold text-gray-900 dark:text-gray-100">
|
||
{{ selectedClient.total_transactions || 0 }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Método de pago -->
|
||
<div>
|
||
<h4 class="text-sm font-bold text-gray-700 dark:text-gray-300 mb-3">
|
||
Selecciona método de pago
|
||
</h4>
|
||
<div class="grid grid-cols-1 gap-3">
|
||
<button
|
||
v-for="method in paymentMethods"
|
||
:key="method.value"
|
||
type="button"
|
||
:disabled="processing"
|
||
class="relative flex items-center gap-4 p-4 rounded-xl border-2 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||
:class="{
|
||
'border-indigo-500 bg-indigo-50 dark:bg-indigo-900/20 ring-2 ring-indigo-500 ring-offset-2 dark:ring-offset-gray-900': selectedMethod === method.value,
|
||
'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800': selectedMethod !== method.value
|
||
}"
|
||
@click="selectedMethod = method.value"
|
||
>
|
||
<div
|
||
class="w-14 h-14 rounded-xl flex items-center justify-center text-white shadow-lg transition-transform"
|
||
:class="[
|
||
method.color,
|
||
selectedMethod === method.value ? 'scale-110' : ''
|
||
]"
|
||
>
|
||
<GoogleIcon :name="method.icon" class="text-2xl" />
|
||
</div>
|
||
<div class="flex-1 text-left">
|
||
<p class="text-base font-bold text-gray-800 dark:text-gray-200">
|
||
{{ method.label }}
|
||
</p>
|
||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||
{{ method.subtitle }}
|
||
</p>
|
||
</div>
|
||
<div v-if="selectedMethod === method.value" class="absolute top-3 right-3">
|
||
<div class="w-6 h-6 rounded-full bg-indigo-600 flex items-center justify-center">
|
||
<GoogleIcon name="check" class="text-sm text-white" />
|
||
</div>
|
||
</div>
|
||
</button>
|
||
</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 {{ formattedEstimatedTotal }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</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"
|
||
:disabled="processing"
|
||
class="px-6 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 disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
Cancelar
|
||
</button>
|
||
<button
|
||
type="button"
|
||
@click="handleConfirm"
|
||
: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"
|
||
>
|
||
<GoogleIcon
|
||
:name="processing ? 'hourglass_empty' : 'check'"
|
||
class="text-xl"
|
||
:class="{ 'animate-spin': processing }"
|
||
/>
|
||
<span v-if="processing">Procesando...</span>
|
||
<span v-else>Confirmar Cobro</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
</template>
|