285 lines
12 KiB
Vue
285 lines
12 KiB
Vue
<script setup>
|
|
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.');
|
|
} 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');
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="show" class="fixed inset-0 z-50 overflow-y-auto">
|
|
<!-- Overlay -->
|
|
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity" @click="closeModal"></div>
|
|
|
|
<!-- Modal -->
|
|
<div class="flex min-h-screen items-center justify-center p-4">
|
|
<div class="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
|
|
<h3 class="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
|
Importar Productos desde Excel
|
|
</h3>
|
|
<button
|
|
@click="closeModal"
|
|
class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300 transition-colors"
|
|
>
|
|
<GoogleIcon name="close" class="text-2xl" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Body -->
|
|
<div class="p-6">
|
|
<!-- Instrucciones -->
|
|
<div class="mb-6 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
|
<div class="flex items-start gap-3">
|
|
<GoogleIcon name="info" class="text-blue-600 dark:text-blue-400 text-xl mt-0.5" />
|
|
<div class="text-sm text-blue-700 dark:text-blue-300">
|
|
<p class="font-semibold mb-2">Instrucciones:</p>
|
|
<ol class="list-decimal ml-4 space-y-1">
|
|
<li>Descarga la plantilla de Excel haciendo clic en el botón de abajo</li>
|
|
<li>Completa la plantilla con los datos de tus productos</li>
|
|
<li>Guarda el archivo y súbelo usando el botón "Seleccionar archivo"</li>
|
|
<li>Haz clic en "Importar" para procesar el archivo</li>
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Botón Descargar Plantilla -->
|
|
<div class="mb-6">
|
|
<button
|
|
@click="downloadTemplate"
|
|
class="w-full flex items-center justify-center gap-2 px-4 py-3 bg-green-600 hover:bg-green-700 text-white font-semibold rounded-lg transition-colors"
|
|
>
|
|
<GoogleIcon name="download" class="text-xl" />
|
|
Descargar Plantilla Excel
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Selector de Archivo -->
|
|
<div class="mb-6">
|
|
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
|
Archivo Excel (.xlsx, .xls)
|
|
</label>
|
|
<div class="flex items-center gap-3">
|
|
<label class="flex-1 cursor-pointer">
|
|
<div class="flex items-center justify-center gap-2 px-4 py-3 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg hover:border-indigo-500 dark:hover:border-indigo-400 transition-colors">
|
|
<GoogleIcon name="upload_file" class="text-2xl text-gray-400" />
|
|
<span class="text-sm text-gray-600 dark:text-gray-400">
|
|
{{ selectedFile ? selectedFile.name : 'Seleccionar archivo Excel...' }}
|
|
</span>
|
|
</div>
|
|
<input
|
|
ref="fileInput"
|
|
type="file"
|
|
accept=".xlsx,.xls"
|
|
@change="handleFileSelect"
|
|
class="hidden"
|
|
/>
|
|
</label>
|
|
<button
|
|
v-if="selectedFile"
|
|
@click="selectedFile = null; fileInput.value = ''"
|
|
class="p-3 text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 transition-colors"
|
|
title="Eliminar archivo"
|
|
>
|
|
<GoogleIcon name="delete" class="text-xl" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Resultados de Importación -->
|
|
<div v-if="importResults" class="mb-6 p-4 bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg">
|
|
<h4 class="font-semibold text-gray-900 dark:text-gray-100 mb-3">Resultados de la Importación</h4>
|
|
<div class="space-y-2 text-sm">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-gray-600 dark:text-gray-400">Productos importados:</span>
|
|
<span class="font-semibold text-green-600 dark:text-green-400">{{ importResults.imported }}</span>
|
|
</div>
|
|
<div v-if="importResults.skipped > 0" class="flex items-center justify-between">
|
|
<span class="text-gray-600 dark:text-gray-400">Productos omitidos:</span>
|
|
<span class="font-semibold text-yellow-600 dark:text-yellow-400">{{ importResults.skipped }}</span>
|
|
</div>
|
|
<div v-if="importResults.errors && importResults.errors.length > 0">
|
|
<p class="text-red-600 dark:text-red-400 font-semibold mb-1">Errores:</p>
|
|
<ul class="list-disc ml-5 text-red-600 dark:text-red-400">
|
|
<li v-for="(error, index) in importResults.errors" :key="index">{{ error }}</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="flex items-center justify-end gap-3 p-6 border-t border-gray-200 dark:border-gray-700">
|
|
<button
|
|
@click="closeModal"
|
|
class="px-4 py-2 text-sm font-semibold text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
@click="importProducts"
|
|
:disabled="!selectedFile || uploading"
|
|
class="flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<GoogleIcon
|
|
:name="uploading ? 'hourglass_empty' : 'upload'"
|
|
:class="{ 'animate-spin': uploading }"
|
|
class="text-xl"
|
|
/>
|
|
{{ uploading ? 'Importando...' : 'Importar Productos' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|