From 1daed06f350dac74c58af97e12629023bff4df3c Mon Sep 17 00:00:00 2001 From: Juan Felipe Zapata Moreno Date: Fri, 2 Jan 2026 15:35:29 -0600 Subject: [PATCH] WIP: importar excel --- src/pages/POS/CashRegister/Detail.vue | 92 ++++---- src/pages/POS/CashRegister/History.vue | 3 - src/pages/POS/Inventory/ImportModal.vue | 284 ++++++++++++++++++++++++ src/pages/POS/Inventory/Index.vue | 40 +++- src/pages/POS/Sales/Index.vue | 34 +-- src/services/cashRegisterService.js | 91 +++++--- src/services/salesService.js | 3 +- src/services/ticketService.js | 32 +-- src/stores/cashRegister.js | 13 ++ src/utils/formatters.js | 149 +++++++++++++ 10 files changed, 611 insertions(+), 130 deletions(-) create mode 100644 src/pages/POS/Inventory/ImportModal.vue create mode 100644 src/utils/formatters.js diff --git a/src/pages/POS/CashRegister/Detail.vue b/src/pages/POS/CashRegister/Detail.vue index e85bc22..12eebf0 100644 --- a/src/pages/POS/CashRegister/Detail.vue +++ b/src/pages/POS/CashRegister/Detail.vue @@ -4,6 +4,7 @@ import { useRoute, useRouter } from 'vue-router'; import cashRegisterService from '@Services/cashRegisterService'; import salesService from '@Services/salesService'; import ticketService from '@Services/ticketService'; +import { formatCurrency, formatDate, safeParseFloat, PAYMENT_METHODS } from '@/utils/formatters'; import GoogleIcon from '@Shared/GoogleIcon.vue'; import SaleDetailModal from '@Pages/POS/Sales/DetailModal.vue'; @@ -21,8 +22,8 @@ const selectedSale = ref(null); /** Computed */ const getSalesByMethod = (method) => { return sales.value - .filter(sale => sale.payment_method === method) - .reduce((sum, sale) => sum + parseFloat(sale.total || 0), 0); + .filter(sale => sale.status === 'completed' && sale.payment_method === method) + .reduce((sum, sale) => sum + safeParseFloat(sale.total), 0); }; const totalCashSales = computed(() => getSalesByMethod('cash')); @@ -32,8 +33,13 @@ const totalCardSales = computed(() => totalCreditCard.value + totalDebitCard.val const difference = computed(() => { if (!cashRegister.value) return 0; - const finalCash = parseFloat(cashRegister.value.final_cash || 0); - const initialCash = parseFloat(cashRegister.value.initial_cash || 0); + // Si ya se calculó en loadData, usar ese valor + if (cashRegister.value.difference !== undefined) { + return safeParseFloat(cashRegister.value.difference); + } + // Fallback: calcular aquí + const finalCash = safeParseFloat(cashRegister.value.final_cash); + const initialCash = safeParseFloat(cashRegister.value.initial_cash); const expectedCash = initialCash + totalCashSales.value; return finalCash - expectedCash; }); @@ -46,11 +52,27 @@ const loadData = async () => { const registerData = await cashRegisterService.getCashRegisterDetail(route.params.id); cashRegister.value = registerData; - // Cargar ventas del período + // Cargar ventas del período (filtradas por cash_register_id en el backend) const salesData = await salesService.getSales({ cash_register_id: route.params.id }); - sales.value = salesData.data || []; + + const completedSales = salesData.data.filter(sale => sale.status === 'completed'); + + sales.value = salesData.data || []; // Muestra todas las ventas + + // Calcula solo con ventas completadas + const cashSales = completedSales + .filter(s => s.payment_method === 'cash') + .reduce((sum, s) => sum + safeParseFloat(s.total), 0); + + const expectedCash = safeParseFloat(cashRegister.value.initial_cash) + cashSales; + const calculatedDifference = safeParseFloat(cashRegister.value.final_cash) - expectedCash; + + // Actualiza los valores calculados + cashRegister.value.expected_cash = expectedCash; + cashRegister.value.difference = calculatedDifference; + } catch (error) { window.Notify.error('Error al cargar el detalle del corte'); } finally { @@ -89,24 +111,6 @@ const downloadTicket = () => { } }; -const formatCurrency = (amount) => { - return new Intl.NumberFormat('es-MX', { - style: 'currency', - currency: 'MXN' - }).format(amount || 0); -}; - -const formatDate = (dateString) => { - if (!dateString) return '-'; - return new Date(dateString).toLocaleString('es-MX', { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); -}; - const getDifferenceColor = () => { const diff = difference.value; if (Math.abs(diff) < 0.01) return 'text-gray-600 dark:text-gray-400'; @@ -120,12 +124,7 @@ const getDifferenceIcon = () => { }; const getPaymentMethodLabel = (method) => { - const methods = { - cash: 'Efectivo', - credit_card: 'Tarjeta de Crédito', - debit_card: 'Tarjeta de Débito' - }; - return methods[method] || method; + return PAYMENT_METHODS[method] || method; }; const getPaymentMethodIcon = (method) => { @@ -210,7 +209,7 @@ onMounted(() => {

Apertura

- {{ formatDate(cashRegister.opened_at) }} + {{ formatDate(cashRegister.opened_at, { includeTime: true }) }}

@@ -218,7 +217,7 @@ onMounted(() => {

Cierre

- {{ formatDate(cashRegister.closed_at) }} + {{ formatDate(cashRegister.closed_at, { includeTime: true }) }}

@@ -304,7 +303,6 @@ onMounted(() => {

- Ventas Realizadas ({{ sales.length }} transacciones) @@ -321,10 +319,11 @@ onMounted(() => { - - - - + + + + + @@ -334,12 +333,12 @@ onMounted(() => { :key="sale.id" class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors" > - - - - +
FolioHoraMétodoTotalFolioHoraMétodoEstadoTotal Acciones
+ {{ sale.invoice_number || `#${String(sale.id).padStart(6, '0')}` }} + {{ new Date(sale.created_at).toLocaleTimeString('es-MX', { hour: '2-digit', @@ -347,7 +346,7 @@ onMounted(() => { }) }} +
{
+ + + {{ sale.status === 'completed' ? 'Completada' : 'Cancelada' }} + + {{ formatCurrency(sale.total) }} diff --git a/src/pages/POS/CashRegister/History.vue b/src/pages/POS/CashRegister/History.vue index 7900ec9..0540173 100644 --- a/src/pages/POS/CashRegister/History.vue +++ b/src/pages/POS/CashRegister/History.vue @@ -178,9 +178,6 @@ onMounted(() => {

${{ parseFloat(register.total_sales || 0).toLocaleString('es-MX', { minimumFractionDigits: 2 }) }}

-

- {{ register.transaction_count || 0 }} ventas -

+import { ref } from 'vue'; +import { apiURL } from '@Services/Api'; +import GoogleIcon from '@Shared/GoogleIcon.vue'; + +/** Props */ +const props = defineProps({ + show: Boolean +}); + +/** Eventos */ +const emit = defineEmits(['close', 'imported']); + +/** Estado */ +const selectedFile = ref(null); +const uploading = ref(false); +const importResults = ref(null); +const fileInput = ref(null); + +/** Métodos */ +const handleFileSelect = (event) => { + const file = event.target.files[0]; + + if (!file) { + selectedFile.value = null; + return; + } + + // Validar extensión + const validExtensions = ['.xlsx', '.xls']; + const fileExtension = file.name.substring(file.name.lastIndexOf('.')).toLowerCase(); + + if (!validExtensions.includes(fileExtension)) { + window.Notify.error('Por favor selecciona un archivo Excel (.xlsx o .xls)'); + selectedFile.value = null; + event.target.value = ''; + return; + } + + selectedFile.value = file; + importResults.value = null; +}; + +const downloadTemplate = async () => { + try { + window.Notify.info('Descargando plantilla...'); + + const response = await fetch(apiURL('inventario/template/download'), { + method: 'GET', + headers: { + 'Authorization': `Bearer ${sessionStorage.token}`, + } + }); + + if (!response.ok) { + throw new Error('Error al descargar la plantilla'); + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = 'plantilla_productos.xlsx'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + + window.Notify.success('Plantilla descargada exitosamente'); + } catch (error) { + console.error('Error:', error); + window.Notify.error('Error al descargar la plantilla'); + } +}; + +const importProducts = async () => { + if (!selectedFile.value) { + window.Notify.warning('Por favor selecciona un archivo'); + return; + } + + uploading.value = true; + importResults.value = null; + + try { + const formData = new FormData(); + formData.append('file', selectedFile.value); + + const response = await fetch(apiURL('inventario/import'), { + method: 'POST', + headers: { + 'Authorization': `Bearer ${sessionStorage.token}`, + 'Accept': 'application/json', + }, + body: formData + }); + + const data = await response.json(); + + if (response.ok && data.status === 'success') { + importResults.value = data.data; + + const { imported, skipped, errors } = data.data; + + if (imported > 0) { + window.Notify.success(`${imported} producto(s) importado(s) exitosamente`); + } + + if (skipped > 0) { + window.Notify.warning(`${skipped} producto(s) omitido(s)`); + } + + if (errors && errors.length > 0) { + console.error('Errores de importación:', errors); + } + + // Resetear formulario + selectedFile.value = null; + if (fileInput.value) { + fileInput.value.value = ''; + } + + // Notificar al componente padre + emit('imported'); + } else { + // Manejar errores de validación + if (data.data?.errors) { + const errorMessages = data.data.errors.map(err => + `Fila ${err.row}: ${err.errors.join(', ')}` + ).join('\n'); + + console.error('Errores de validación:', errorMessages); + window.Notify.error('Error de validación en el archivo. Revisa la consola.'); + } else { + window.Notify.error(data.data?.message || 'Error al importar productos'); + } + } + } catch (error) { + console.error('Error:', error); + window.Notify.error('Error al importar productos'); + } finally { + uploading.value = false; + } +}; + +const closeModal = () => { + selectedFile.value = null; + importResults.value = null; + if (fileInput.value) { + fileInput.value.value = ''; + } + emit('close'); +}; + + + diff --git a/src/pages/POS/Inventory/Index.vue b/src/pages/POS/Inventory/Index.vue index 0f9623c..db57517 100644 --- a/src/pages/POS/Inventory/Index.vue +++ b/src/pages/POS/Inventory/Index.vue @@ -1,6 +1,7 @@