338 lines
16 KiB
Vue
338 lines
16 KiB
Vue
<script setup>
|
|
import { computed, watch } from 'vue';
|
|
import ticketService from '@Services/ticketService';
|
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
|
import Modal from '@Holos/Modal.vue';
|
|
|
|
/** Props */
|
|
const props = defineProps({
|
|
show: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
sale: {
|
|
type: Object,
|
|
default: null
|
|
}
|
|
});
|
|
|
|
/** Emits */
|
|
const emit = defineEmits(['close', 'cancel-sale']);
|
|
|
|
/** Computados */
|
|
const saleDetails = computed(() => {
|
|
return props.sale?.details || [];
|
|
});
|
|
|
|
const hasDetails = computed(() => {
|
|
return saleDetails.value.length > 0;
|
|
});
|
|
|
|
const formattedSubtotal = computed(() => {
|
|
return formatCurrency(props.sale?.subtotal || 0);
|
|
});
|
|
|
|
const formattedTax = computed(() => {
|
|
return formatCurrency(props.sale?.tax || 0);
|
|
});
|
|
|
|
const formattedTotal = computed(() => {
|
|
return formatCurrency(props.sale?.total || 0);
|
|
});
|
|
|
|
const formattedDate = computed(() => {
|
|
if (!props.sale?.created_at) return '-';
|
|
const date = new Date(props.sale.created_at);
|
|
return new Intl.DateTimeFormat('es-MX', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
}).format(date);
|
|
});
|
|
|
|
const paymentMethodLabel = computed(() => {
|
|
const methods = {
|
|
'cash': 'Efectivo',
|
|
'credit_card': 'Tarjeta de Crédito',
|
|
'debit_card': 'Tarjeta de Débito'
|
|
};
|
|
return methods[props.sale?.payment_method] || props.sale?.payment_method || '-';
|
|
});
|
|
|
|
const paymentMethodIcon = computed(() => {
|
|
const icons = {
|
|
'cash': 'payments',
|
|
'credit_card': 'credit_card',
|
|
'debit_card': 'credit_card'
|
|
};
|
|
return icons[props.sale?.payment_method] || 'payment';
|
|
});
|
|
|
|
const canCancel = computed(() => {
|
|
return props.sale?.status === 'completed';
|
|
});
|
|
|
|
/** Métodos */
|
|
const formatCurrency = (amount) => {
|
|
return new Intl.NumberFormat('es-MX', {
|
|
style: 'currency',
|
|
currency: 'MXN'
|
|
}).format(amount || 0);
|
|
};
|
|
|
|
const handleClose = () => {
|
|
emit('close');
|
|
};
|
|
|
|
const handleCancelSale = () => {
|
|
if (confirm('¿Estás seguro de cancelar esta venta? Se restaurará el stock.')) {
|
|
emit('cancel-sale', props.sale.id);
|
|
}
|
|
};
|
|
|
|
const handleDownloadTicket = () => {
|
|
try {
|
|
ticketService.generateSaleTicket(props.sale, {
|
|
businessName: 'HIKVISION DISTRIBUIDOR',
|
|
businessAddress: 'Ciudad de México, México',
|
|
businessPhone: 'Tel: (55) 1234-5678',
|
|
autoDownload: true
|
|
});
|
|
window.Notify.success('Ticket descargado correctamente');
|
|
} catch (error) {
|
|
console.error('Error generando ticket:', error);
|
|
window.Notify.error('Error al generar el ticket PDF');
|
|
}
|
|
};
|
|
|
|
/** Watchers */
|
|
watch(() => props.show, () => {
|
|
// Modal opened
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<Modal :show="show" max-width="4xl" @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-indigo-100 dark:bg-indigo-900/30">
|
|
<GoogleIcon name="receipt_long" class="text-2xl text-indigo-600 dark:text-indigo-400" />
|
|
</div>
|
|
<div>
|
|
<h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">
|
|
{{ $t('sales.detail') }}
|
|
</h3>
|
|
<p v-if="sale" class="text-sm text-gray-500 dark:text-gray-400">
|
|
Folio: {{ sale.invoice_number || `#${String(sale.id).padStart(6, '0')}` }}
|
|
</p>
|
|
</div>
|
|
</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 v-if="sale" class="space-y-6">
|
|
<!-- Información de la venta -->
|
|
<div class="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 rounded-xl p-5 border border-gray-200 dark:border-gray-700">
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase mb-1">
|
|
{{ $t('sales.date') }}
|
|
</p>
|
|
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
{{ formattedDate }}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase mb-1">
|
|
{{ $t('sales.cashier') }}
|
|
</p>
|
|
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
{{ sale.user?.name || '-' }}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase mb-1">
|
|
{{ $t('sales.paymentMethod') }}
|
|
</p>
|
|
<div class="flex items-center gap-2">
|
|
<GoogleIcon
|
|
:name="paymentMethodIcon"
|
|
class="text-lg text-gray-500 dark:text-gray-400"
|
|
/>
|
|
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
{{ paymentMethodLabel }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase mb-1">
|
|
{{ $t('sales.status') }}
|
|
</p>
|
|
<span
|
|
class="inline-flex px-3 py-1 text-xs font-semibold rounded-full"
|
|
:class="{
|
|
'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400': sale.status === 'completed',
|
|
'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400': sale.status === 'cancelled'
|
|
}"
|
|
>
|
|
{{ sale.status === 'completed' ? 'Completada' : 'Cancelada' }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Items de la venta -->
|
|
<div>
|
|
<h3 class="text-sm font-bold text-gray-900 dark:text-gray-100 uppercase tracking-wide mb-3">
|
|
Productos Vendidos
|
|
</h3>
|
|
|
|
<!-- Tabla con scroll -->
|
|
<div class="border border-gray-200 dark:border-gray-700 rounded-xl overflow-hidden">
|
|
<div class="max-h-80 overflow-y-auto">
|
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
<thead class="bg-gray-50 dark:bg-gray-800 sticky top-0 z-10">
|
|
<tr>
|
|
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase">
|
|
Producto
|
|
</th>
|
|
<th class="px-4 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase">
|
|
Cant.
|
|
</th>
|
|
<th class="px-4 py-3 text-right text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase">
|
|
P. Unit.
|
|
</th>
|
|
<th class="px-4 py-3 text-right text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase">
|
|
Subtotal
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody v-if="hasDetails" class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
|
<tr
|
|
v-for="(item, index) in saleDetails"
|
|
:key="index"
|
|
class="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
|
>
|
|
<td class="px-4 py-3">
|
|
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
{{ item.product_name }}
|
|
</p>
|
|
<p v-if="item.inventory?.sku" class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
|
SKU: {{ item.inventory.sku }}
|
|
</p>
|
|
</td>
|
|
<td class="px-4 py-3 text-center">
|
|
<span class="inline-flex items-center justify-center w-8 h-8 text-sm font-bold text-indigo-600 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-900/30 rounded-full">
|
|
{{ item.quantity }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-right">
|
|
<span class="text-sm text-gray-700 dark:text-gray-300">
|
|
{{ formatCurrency(item.unit_price) }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-right">
|
|
<span class="text-sm font-bold text-gray-900 dark:text-gray-100">
|
|
{{ formatCurrency(item.subtotal) }}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
<tbody v-else>
|
|
<tr>
|
|
<td colspan="4" class="px-4 py-8 text-center">
|
|
<GoogleIcon name="inventory_2" class="text-5xl text-gray-300 dark:text-gray-600 mx-auto mb-2" />
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
No hay productos en esta venta
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Totales -->
|
|
<div class="bg-gradient-to-br from-indigo-50 to-indigo-100 dark:from-indigo-900/20 dark:to-indigo-800/20 rounded-xl p-5 border border-indigo-200 dark:border-indigo-800">
|
|
<div class="space-y-3">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
{{ $t('sales.subtotal') }}
|
|
</span>
|
|
<span class="text-base font-semibold text-gray-900 dark:text-gray-100">
|
|
{{ formattedSubtotal }}
|
|
</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
{{ $t('sales.tax') }}
|
|
</span>
|
|
<span class="text-base font-semibold text-gray-900 dark:text-gray-100">
|
|
{{ formattedTax }}
|
|
</span>
|
|
</div>
|
|
<div class="pt-3 border-t border-indigo-200 dark:border-indigo-700">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-lg font-bold text-gray-900 dark:text-gray-100">
|
|
{{ $t('sales.total') }}
|
|
</span>
|
|
<span class="text-3xl font-bold text-indigo-600 dark:text-indigo-400">
|
|
{{ formattedTotal }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="flex items-center justify-between gap-3 mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
|
<!-- Botón Cancelar Venta (solo si está completada) -->
|
|
<button
|
|
v-if="canCancel"
|
|
type="button"
|
|
class="flex items-center gap-2 px-4 py-2.5 text-sm font-semibold text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors border border-red-200 dark:border-red-800"
|
|
@click="handleCancelSale"
|
|
>
|
|
<GoogleIcon name="cancel" class="text-lg" />
|
|
{{ $t('sales.cancel') }}
|
|
</button>
|
|
|
|
<div v-else></div> <!-- Spacer -->
|
|
|
|
<!-- Botones de acción -->
|
|
<div class="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
class="flex items-center gap-2 px-4 py-2.5 bg-green-600 hover:bg-green-700 text-white text-sm font-semibold rounded-lg transition-colors shadow-sm"
|
|
@click="handleDownloadTicket"
|
|
>
|
|
<GoogleIcon name="download" class="text-lg" />
|
|
Descargar Ticket
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="px-4 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 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
@click="handleClose"
|
|
>
|
|
Cerrar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
</template>
|