388 lines
12 KiB
Vue
388 lines
12 KiB
Vue
<script setup>
|
|
import { ref, watch, onMounted } from "vue";
|
|
import axios from "axios";
|
|
import DateRange from "@/Components/Dashboard/Form/DateRange.vue";
|
|
|
|
const props = defineProps({
|
|
show: Boolean
|
|
});
|
|
|
|
const emit = defineEmits(['close']);
|
|
|
|
const loading = ref(false);
|
|
const loadingOptions = ref(false);
|
|
const form = ref({
|
|
startDate: new Date().toISOString().slice(0, 10),
|
|
endDate: new Date().toISOString().slice(0, 10),
|
|
supportType: '', // almacen o servicio
|
|
typeId: '', // ID del tipo específico
|
|
supportId: '' // Opciones específicas seleccionadas
|
|
});
|
|
|
|
const dateRange = ref({
|
|
start: new Date().toISOString().slice(0, 10),
|
|
end: new Date().toISOString().slice(0, 10)
|
|
});
|
|
|
|
const supportTypes = [
|
|
{ id: '', name: 'Seleccionar tipo de apoyo' },
|
|
{ id: 'almacen', name: 'Almacén', api_types: [1, 2, 3, 4, 5, 6, 11, 12, 13] },
|
|
{ id: 'servicio', name: 'Servicio', api_types: [7, 8, 9, 10, 14] }
|
|
];
|
|
|
|
const typeOptions = ref([]);
|
|
const supportOptionsData = ref([]);
|
|
|
|
// Watch para actualizar fechas desde DateRange
|
|
watch(dateRange, (newRange) => {
|
|
form.value.startDate = newRange.start;
|
|
form.value.endDate = newRange.end;
|
|
}, { deep: true });
|
|
|
|
// Watch para cargar opciones cuando cambia el tipo de apoyo
|
|
watch(() => form.value.supportType, async (newType) => {
|
|
if (newType && newType !== '') {
|
|
await loadSupportOptions(newType);
|
|
} else {
|
|
typeOptions.value = [];
|
|
supportOptionsData.value = [];
|
|
form.value.typeId = '';
|
|
form.value.supportOptions = [];
|
|
}
|
|
});
|
|
|
|
// Watch para cargar opciones específicas cuando cambia el typeId
|
|
watch(() => form.value.typeId, async (newTypeId) => {
|
|
if (newTypeId && newTypeId !== '') {
|
|
await loadSpecificOptions(newTypeId);
|
|
} else {
|
|
supportOptionsData.value = [];
|
|
form.value.supportOptions = [];
|
|
}
|
|
});
|
|
|
|
const loadSupportOptions = async (supportType) => {
|
|
loadingOptions.value = true;
|
|
try {
|
|
const selectedType = supportTypes.find(type => type.id === supportType);
|
|
if (selectedType && selectedType.api_types) {
|
|
// Cargar tipos disponibles para este tipo de apoyo
|
|
const options = selectedType.api_types.map(id => ({
|
|
id: id,
|
|
name: getTypeName(id)
|
|
}));
|
|
|
|
typeOptions.value = [
|
|
{ id: '', name: 'Seleccionar categoría específica' },
|
|
...options
|
|
];
|
|
}
|
|
} catch (error) {
|
|
console.error('Error cargando opciones de tipo:', error);
|
|
} finally {
|
|
loadingOptions.value = false;
|
|
}
|
|
};
|
|
|
|
const loadSpecificOptions = async (typeId) => {
|
|
loadingOptions.value = true;
|
|
try {
|
|
const response = await axios.get('/api/support-options', {
|
|
params: { type_id: typeId },
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
});
|
|
|
|
if (response.data && response.data.options) {
|
|
supportOptionsData.value = response.data.options;
|
|
// Pre-seleccionar "Todos los enlistados" si existe
|
|
const allOption = response.data.options.find(opt => opt.id === 'all');
|
|
if (allOption) {
|
|
form.value.supportOptions = ['all'];
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error cargando opciones específicas:', error);
|
|
supportOptionsData.value = [];
|
|
|
|
// Mostrar mensaje de error más específico
|
|
if (error.response?.data?.error) {
|
|
alert(`Error: ${error.response.data.error}`);
|
|
} else {
|
|
alert('Error al cargar las opciones de apoyo');
|
|
}
|
|
} finally {
|
|
loadingOptions.value = false;
|
|
}
|
|
};
|
|
|
|
const getTypeName = (id) => {
|
|
const names = {
|
|
1: "Medicamentos",
|
|
2: "Material de Construcción",
|
|
3: "Aparatos Ortopédicos",
|
|
4: "Despensas",
|
|
5: "Útiles Escolares",
|
|
6: "Pañales",
|
|
7: "Análisis Clínicos",
|
|
8: "Traslados",
|
|
9: "Monederos",
|
|
10: "Apoyo social DIF",
|
|
11: "Alimentos",
|
|
12: "Ataúdes",
|
|
13: "Ventiladores",
|
|
14: "Apoyo a enfermedades de la vista"
|
|
};
|
|
return names[id] || `Tipo ${id}`;
|
|
};
|
|
|
|
const exportToExcel = async () => {
|
|
if (!form.value.supportType || !form.value.typeId) {
|
|
alert('Por favor selecciona el tipo de apoyo y la categoría específica');
|
|
return;
|
|
}
|
|
|
|
loading.value = true;
|
|
try {
|
|
// CAMBIO: Usar los parámetros correctos que espera la API
|
|
const params = {
|
|
start_date: form.value.startDate,
|
|
end_date: form.value.endDate,
|
|
type_id: form.value.typeId,
|
|
support_id: form.value.supportId || 'all' // Si no hay supportId, usar 'all'
|
|
};
|
|
|
|
console.log('Enviando parámetros:', params); // Debug
|
|
|
|
// CAMBIO: Configuración más específica para descarga de archivos
|
|
const response = await axios({
|
|
method: 'GET',
|
|
url: '/api/export-excel',
|
|
params: params,
|
|
responseType: 'blob', // Importante para archivos binarios
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'Accept': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
},
|
|
timeout: 60000 // 60 segundos timeout
|
|
});
|
|
|
|
// VERIFICAR que la respuesta sea un blob válido
|
|
if (!response.data || response.data.size === 0) {
|
|
throw new Error('El archivo descargado está vacío');
|
|
}
|
|
|
|
// VERIFICAR el tipo de contenido
|
|
const contentType = response.headers['content-type'] || '';
|
|
if (!contentType.includes('spreadsheet') && !contentType.includes('excel') && !contentType.includes('octet-stream')) {
|
|
// Si no es Excel, probablemente sea un JSON con error
|
|
const text = await response.data.text();
|
|
console.error('Respuesta inesperada:', text);
|
|
throw new Error('El servidor no devolvió un archivo Excel válido');
|
|
}
|
|
|
|
// Crear y descargar el archivo
|
|
const blob = new Blob([response.data], {
|
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
});
|
|
|
|
// Generar nombre de archivo único
|
|
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
|
|
const filename = `beneficiarios_${form.value.startDate}_${form.value.endDate}_${timestamp}.xlsx`;
|
|
|
|
// MEJORAR la descarga
|
|
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
|
|
// Para Internet Explorer
|
|
window.navigator.msSaveOrOpenBlob(blob, filename);
|
|
} else {
|
|
// Para navegadores modernos
|
|
const url = window.URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = filename;
|
|
link.style.display = 'none';
|
|
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
|
|
// Cleanup
|
|
document.body.removeChild(link);
|
|
window.URL.revokeObjectURL(url);
|
|
}
|
|
|
|
// Cerrar modal y mostrar éxito
|
|
closeModal();
|
|
alert('Archivo Excel descargado exitosamente');
|
|
|
|
} catch (error) {
|
|
console.error('Error exportando Excel:', error);
|
|
|
|
let errorMessage = 'Error al exportar el archivo Excel';
|
|
|
|
if (error.response?.status === 400 && error.response?.data?.error) {
|
|
errorMessage = error.response.data.error;
|
|
} else if (error.response?.status === 500) {
|
|
errorMessage = 'Error interno del servidor. Intente nuevamente.';
|
|
} else if (error.message) {
|
|
errorMessage = error.message;
|
|
} else if (error.code === 'ECONNABORTED') {
|
|
errorMessage = 'La descarga tardó demasiado. Intente con un rango de fechas menor.';
|
|
}
|
|
|
|
alert(errorMessage);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const closeModal = () => {
|
|
// Resetear formulario
|
|
form.value = {
|
|
startDate: new Date().toISOString().slice(0, 10),
|
|
endDate: new Date().toISOString().slice(0, 10),
|
|
supportType: '',
|
|
typeId: '',
|
|
supportOptions: []
|
|
};
|
|
|
|
dateRange.value = {
|
|
start: new Date().toISOString().slice(0, 10),
|
|
end: new Date().toISOString().slice(0, 10)
|
|
};
|
|
|
|
typeOptions.value = [];
|
|
supportOptionsData.value = [];
|
|
|
|
emit('close');
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<!-- Modal Backdrop -->
|
|
<div
|
|
v-if="show"
|
|
class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4"
|
|
@click.self="closeModal"
|
|
>
|
|
<div class="bg-white rounded-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
|
<!-- Header -->
|
|
<div class="p-6 border-b border-gray-200">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-lg font-semibold text-gray-900">
|
|
Exportar a Excel
|
|
</h3>
|
|
<button
|
|
@click="closeModal"
|
|
class="text-gray-400 hover:text-gray-600 transition-colors"
|
|
>
|
|
<svg class="w-6 h-6" 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>
|
|
<p class="text-sm text-gray-600 mt-2">
|
|
Configure los filtros para exportar los datos de beneficiarios
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Body -->
|
|
<div class="p-6 space-y-6">
|
|
<!-- Rango de Fechas -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
Rango de Fechas
|
|
</label>
|
|
<DateRange
|
|
v-model="dateRange"
|
|
:presets="true"
|
|
title-start="Fecha inicio"
|
|
title-end="Fecha fin"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Tipo de Apoyo -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
Tipo de Apoyo
|
|
</label>
|
|
<select
|
|
v-model="form.supportType"
|
|
class="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
|
>
|
|
<option v-for="type in supportTypes" :key="type.id" :value="type.id">
|
|
{{ type.name }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Categoría Específica -->
|
|
<div v-if="form.supportType && typeOptions.length > 0">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
Categoría Específica
|
|
</label>
|
|
<select
|
|
v-model="form.typeId"
|
|
class="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
|
:disabled="loadingOptions"
|
|
>
|
|
<option v-for="option in typeOptions" :key="option.id" :value="option.id">
|
|
{{ option.name }}
|
|
</option>
|
|
</select>
|
|
|
|
<div v-if="loadingOptions" class="mt-2 text-sm text-blue-600">
|
|
Cargando opciones...
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Opciones Específicas de Apoyo -->
|
|
<div v-if="supportOptionsData.length > 0">
|
|
<label class="block text-sm font-medium text-gray-700 mb-3">
|
|
Seleccionar Apoyos Específicos
|
|
</label>
|
|
<div class="max-h-64 overflow-y-auto border border-gray-200 rounded-md p-3">
|
|
<div class="space-y-2">
|
|
<label
|
|
v-for="option in supportOptionsData"
|
|
:key="option.id"
|
|
class="flex items-center space-x-2 text-sm"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
:value="option.id"
|
|
v-model="form.supportOptions"
|
|
class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
/>
|
|
<span class="flex-1">{{ option.name }}</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<p class="text-xs text-gray-500 mt-2">
|
|
Seleccionados: {{ form.supportOptions.length }} de {{ supportOptionsData.length }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="p-6 border-t border-gray-200 flex justify-end space-x-3">
|
|
<button
|
|
@click="closeModal"
|
|
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
@click="exportToExcel"
|
|
:disabled="loading || !form.supportType || !form.typeId"
|
|
class="px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-md hover:bg-green-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors flex items-center"
|
|
>
|
|
<svg v-if="loading" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
|
|
</svg>
|
|
{{ loading ? 'Exportando...' : 'Exportar Excel' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|