pdv.frontend/src/pages/POS/Sales/DetailModal.vue
2026-01-01 21:59:45 -06:00

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>