|
|
|
|
@@ -1,315 +1,518 @@
|
|
|
|
|
import React, { useState } from "react";
|
|
|
|
|
import { fetchWithAuth } from '../fetchWithAuth';
|
|
|
|
|
|
|
|
|
|
const API_URL = import.meta.env.VITE_EFC_API_URL;
|
|
|
|
|
|
|
|
|
|
// Animación fade-in/slide-up para bloques
|
|
|
|
|
const fadeInSlideUp = `@keyframes fadein-slideup {
|
|
|
|
|
0% { opacity: 0; transform: translateY(40px); }
|
|
|
|
|
100% { opacity: 1; transform: translateY(0); }
|
|
|
|
|
}`;
|
|
|
|
|
if (typeof document !== 'undefined' && !document.getElementById('fadein-slideup-reportes')) {
|
|
|
|
|
if (typeof document !== 'undefined' && !document.getElementById('fadein-slideup-reports')) {
|
|
|
|
|
const style = document.createElement('style');
|
|
|
|
|
style.id = 'fadein-slideup-reportes';
|
|
|
|
|
style.id = 'fadein-slideup-reports';
|
|
|
|
|
style.innerHTML = fadeInSlideUp;
|
|
|
|
|
document.head.appendChild(style);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function Reportes() {
|
|
|
|
|
const [tipoReporte, setTipoReporte] = useState('');
|
|
|
|
|
const [fechaInicio, setFechaInicio] = useState('');
|
|
|
|
|
const [fechaFin, setFechaFin] = useState('');
|
|
|
|
|
export default function Reports() {
|
|
|
|
|
const [isExporting, setIsExporting] = useState(false);
|
|
|
|
|
|
|
|
|
|
// Datos de ejemplo
|
|
|
|
|
const reportes = [
|
|
|
|
|
{ id: 1, nombre: 'Reporte de usuarios activos', tipo: 'Usuarios', fecha: '2025-08-07', estado: 'Completado' },
|
|
|
|
|
{ id: 2, nombre: 'Análisis de documentos procesados', tipo: 'Documentos', fecha: '2025-08-06', estado: 'Procesando' },
|
|
|
|
|
{ id: 3, nombre: 'Resumen de procesos aduaneros', tipo: 'Procesos', fecha: '2025-08-05', estado: 'Completado' },
|
|
|
|
|
{ id: 4, nombre: 'Estadísticas generales del sistema', tipo: 'General', fecha: '2025-08-04', estado: 'Completado' },
|
|
|
|
|
{ id: 5, nombre: 'Reporte de expedientes', tipo: 'Documentos', fecha: '2025-08-03', estado: 'Error' },
|
|
|
|
|
// Estado para pestañas
|
|
|
|
|
const [activeTab, setActiveTab] = useState('pedimentos');
|
|
|
|
|
|
|
|
|
|
// JSON de modelos para Datastage
|
|
|
|
|
const datastageModels = [
|
|
|
|
|
{ model: "Registro500", name: "Validación y reconocimiento", fields: ["id", "patente", "pedimento", "seccion_aduanera", "consecutivo_remesa", "numero_seleccion", "fecha_inicio_reconocimiento", "hora_inicio_reconocimiento", "fecha_fin_reconocimiento", "hora_fin_reconocimiento", "fraccion", "secuencia_fraccion", "clave_documento", "tipo_operacion", "grado_incidencia", "fecha_seleccion", "organizacion", "consulta", "datastage", "created_at", "updated_at"], filters: {patente: "", pedimento: "", seccion_aduanera: "", fecha_seleccion: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro501", name: "Datos generales", fields: ["id", "patente", "pedimento", "seccion_aduanera", "tipo_operacion", "clave_documento", "seccion_aduanera_entrada", "curp_contribuyente", "rfc", "curp_agente_a", "tipo_cambio", "total_fletes", "total_seguros", "total_embalajes", "total_incrementables", "total_deducibles", "peso_bruto_mercancia", "medio_transporte_salida", "medio_transporte_arribo", "medio_transporte_entrada_salida", "destino_mercancia", "nombre_contribuyente", "calle_contribuyente", "num_interior_contribuyente", "num_exterior_contribuyente", "cp_contribuyente", "municipio_contribuyente", "entidad_fed_contribuyente", "pais_contribuyente", "tipo_pedimento", "fecha_recepcion_pedimento", "fecha_pago_real", "organizacion", "consulta", "datastage"], filters: {patente: "", pedimento: "", seccion_aduanera: "", tipo_operacion: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro502", name: "Transporte de las mercancías", fields: ["id", "pedimento", "seccion_aduanera", "rfc_transportista", "curp_transportista", "nombre_transportista", "pais_transporte", "identificador_transporte", "fecha_pago_real", "organizacion", "created_by", "consulta", "datastage", "patente"], filters: {patente: "", pedimento: "", fecha_pago_real: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro503", name: "Guías", fields: ["id", "patente", "pedimento", "seccion_aduanera", "numero_guia", "tipo_guia", "fecha_pago_real", "organizacion", "created_by", "consulta", "datastage"], filters: {patente: "", pedimento: "", fecha_pago_real: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro504", name: "Contenedores", fields: ["id", "patente", "pedimento", "seccion_aduanera", "num_contenedor", "tipo_contenedor", "fecha_pago_real", "organizacion", "created_by", "consulta", "datastage"], filters: {patente: "", pedimento: "", num_contenedor: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro505", name: "Facturas", fields: ["id", "pedimento", "seccion_aduanera", "fecha_facturacion", "numero_factura", "termino_facturacion", "moneda_facturacion", "valor_dolares", "valor_moneda_extranjera", "pais_facturacion", "entidad_fed_facturacion", "indent_fiscal_proveedor", "proveedor_mercancia", "calle_proveedor", "num_interior_proveedor", "num_exterior_proveedor", "cp_proveedor", "municipio_proveedor", "fecha_pago_real", "organizacion", "created_by", "consulta", "datastage", "patente"], filters: {pedimento: "", numero_factura: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro506", name: "Fechas del pedimento", fields: ["id", "patente", "pedimento", "seccion_aduanera", "tipo_fecha", "fecha_operacion", "fecha_validacion_pago_r", "organizacion", "created_by", "consulta", "datastage"], filters: {patente: "", fecha_operacion: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro507", name: "Casos del pedimento", fields: ["id", "patente", "pedimento", "seccion_aduanera", "clave_caso", "identificador_caso", "tipo_pedimento", "complemento_caso", "fecha_validacion_pago_r", "organizacion", "created_by", "consulta", "datastage"], filters: {patente: "", clave_caso: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro508", name: "Cuentas aduaneras de garantía del pedimento", fields: ["id", "patente", "pedimento", "seccion_aduanera", "institucion_emisora", "numero_cuenta", "folio_constancia", "fecha_constancia", "tipo_cuenta", "clave_garantia", "valor_unitario_titulo", "total_garantia", "cantidad_unidades", "titulos_asignados", "fecha_pago_real", "organizacion", "created_by", "consulta", "datastage"], filters: {patente: "", institucion_emisora: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro509", name: "Tasas del pedimento", fields: ["id", "patente", "pedimento", "seccion_aduanera", "clave_contribucion", "tasa_contribucion", "tipo_tasa", "tipo_pedimento", "fecha_pago_real", "organizacion", "created_by", "consulta", "datastage"], filters: {patente: "", clave_contribucion: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro510", name: "Contribuciones del pedimento", fields: ["id", "patente", "pedimento", "seccion_aduanera", "clave_contribucion", "tasa_contribucion", "tipo_tasa", "tipo_pedimento", "fecha_pago_real", "organizacion", "created_by", "datastage", "consulta", "forma_pago", "importe_pago"], filters: {patente: "", clave_contribucion: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro511", name: "Observaciones del pedimento", fields: ["id", "patente", "pedimento", "seccion_aduanera", "secuencia_observacion", "observaciones", "tipo_pedimento", "fecha_validacion_pago_r", "organizacion", "created_by", "consulta", "datastage"], filters: {patente: "", secuencia_observacion: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro512", name: "Descargos de mercancías", fields: ["id", "patente", "pedimento", "seccion_aduanera", "patente_aduanal_orig", "pedimento_original", "seccion_aduanera_desp_orig", "documento_original", "fecha_operacion_orig", "fraccion_original", "unidad_medida", "mercancia_descargada", "tipo_pedimento", "fecha_pago_real", "organizacion", "created_by", "consulta", "datastage"], filters: {patente: "", patente_aduanal_orig: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro520", name: "Destinatarios de la mercancía", fields: ["id", "patente", "pedimento", "seccion_aduanera", "indent_fiscal_destinatario", "nombre_destinatario_mercancia", "calle_destinatario", "num_interior_destinatario", "num_exterior_destinatario", "cp_destinatario", "municpio_destinatario", "pais_destinatario", "fecha_pago_real", "organizacion", "created_at", "consulta", "datastage"], filters: {patente: "", indent_fiscal_destinatario: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro551", name: "Partidas", fields: ["id", "patente", "pedimento", "seccion_aduanera", "fraccion", "secuencia_fraccion", "subdivision_fraccion", "descripcion_mercancia", "precio_unitario", "valor_aduana", "valor_comercial", "valor_dolares", "cantidad_um_comercial", "unidad_medida_comercial", "cantidad_um_tarifa", "unidad_medida_tarifa", "valor_agregado", "clave_vinculacion", "metodo_valorizacion", "codigo_mercancia_producto", "marca_mercancia_producto", "modelo_mercancia_producto", "pais_origen_destino", "pais_comprador_vendedor", "entidad_fed_origen", "entidad_fed_comprador", "entidad_fed_vendedor", "tipo_operacion", "clave_documento", "fecha_pago_real", "organizacion", "created_by", "consulta", "datastage", "entidad_fed_destino"], filters: {patente: "", fraccion: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro552", name: "Mercancías", fields: ["id", "patente", "pedimento", "seccion_aduanera", "fraccion", "secuencia_fraccion", "vin_numero_serie", "kilometraje_vehiculo", "fecha_pago_real", "organizacion", "created_by", "consulta", "datastage"], filters: {patente: "", fraccion: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro553", name: "Permiso de la partida", fields: ["id", "patente", "pedimento", "seccion_aduanera", "fraccion", "secuencia_fraccion", "clave_permiso", "firma_descargo", "numero_permiso", "valor_comercial_dolares", "cantidad_mum_tarifa", "fecha_pago_real", "organizacion", "created_by", "datastage", "consulta"], filters: {patente: "", fraccion: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro554", name: "Casos de la partida", fields: ["id", "patente", "pedimento", "seccion_aduanera", "fraccion", "secuencia_fraccion", "clave_caso", "identificador_caso", "complemento_caso", "fecha_pago_real", "organizacion", "created_by", "consulta", "datastage"], filters: {patente: "", fraccion: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro555", name: "Cuentas aduaneras de garantía de la partida", fields: ["id", "patente", "pedimento", "seccion_aduanera", "fraccion", "secuencia_fraccion", "institucion_emisora", "numero_cuenta", "folio_constancia", "fecha_constancia", "clave_garantia", "valor_unitario_titulo", "total_garantia", "cantidad_unidades_medida", "titulos_asignados", "fecha_pago_real", "organizacion", "datastage", "created_by", "consulta"], filters: {patente: "", fraccion: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro556", name: "Tasas de las contribuciones de la partida", fields: ["id", "patente", "pedimento", "seccion_aduanera", "clave_contribucion", "tasa_contribucion", "tipo_tasa", "fecha_pago_real", "organizacion", "created_by", "consulta", "datastage", "fraccion", "secuencia_fraccion"], filters: {patente: "", clave_contribucion: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro557", name: "Contribuciones de la partida", fields: ["id", "patente", "pedimento", "seccion_aduanera", "fraccion", "secuencia_fraccion", "clave_contribucion", "forma_pago", "importe_pago", "fecha_pago_real", "organizacion", "created_by", "datastage", "consulta"], filters: {patente: "", fraccion: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro558", name: "Observaciones de la partida", fields: ["id", "patente", "pedimento", "seccion_aduanera", "fraccion", "secuencia_fraccion", "secuencia_observacion", "observaciones", "fecha_pago_real", "organizacion", "created_by", "datastage", "consulta"], filters: {patente: "", fraccion: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "RegistroSel", fields: ["id", "patente", "pedimento", "seccion_aduanera", "consecutivo_remesa", "numero_seleccion", "fecha_seleccion", "hora_seleccion", "semaforo_fiscal", "clave_documento", "tipo_operacion", "organizacion", "created_by", "consulta", "datastage"], filters: {patente: "", consecutivo_remesa: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro701", name: "Rectificaciones", fields: ["id", "patente", "pedimento", "seccion_aduanera", "clave_documento", "fecha_pago", "pedimento_anterior", "patente_anterior", "seccion_aduanera_anterior", "documento_anterior", "fecha_operacion_anterior", "pedimento_original", "patente_aduanal_orig", "seccion_aduanera_desp_orig", "fecha_pago_real", "organizacion", "created_by", "consulta", "datastage"], filters: {patente: "", pedimento_anterior: "", organizacion: "", datastage: ""}},
|
|
|
|
|
{ model: "Registro702", name: "Diferencias de contribuciones del pedimento", fields: ["id", "patente", "pedimento", "seccion_aduanera", "clave_contribucion", "forma_pago", "importe_pago", "tipo_pedimento", "fecha_pago_real", "organizacion", "created_by", "consulta", "datastage"], filters: {patente: "", clave_contribucion: "", organizacion: "", datastage: ""}},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const getEstadoBadge = (estado) => {
|
|
|
|
|
const styles = {
|
|
|
|
|
'Completado': 'bg-emerald-100 text-emerald-800 border-emerald-200',
|
|
|
|
|
'Procesando': 'bg-yellow-100 text-yellow-800 border-yellow-200',
|
|
|
|
|
'Error': 'bg-red-100 text-red-800 border-red-200'
|
|
|
|
|
};
|
|
|
|
|
return styles[estado] || 'bg-gray-100 text-gray-800 border-gray-200';
|
|
|
|
|
|
|
|
|
|
// Estado para modelo seleccionado en Datastage
|
|
|
|
|
const [selectedModel, setSelectedModel] = useState(datastageModels[0].model);
|
|
|
|
|
|
|
|
|
|
// Estado para campos seleccionados
|
|
|
|
|
const [selectedFields, setSelectedFields] = useState(datastageModels[0].fields);
|
|
|
|
|
|
|
|
|
|
// Estado para campo seleccionado en lista disponible
|
|
|
|
|
const [availableSelected, setAvailableSelected] = useState(null);
|
|
|
|
|
|
|
|
|
|
// Estado para los filtros
|
|
|
|
|
const [filters, setFilters] = useState(datastageModels[0].filters);
|
|
|
|
|
|
|
|
|
|
// Actualizar campos seleccionados al cambiar de modelo
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
const modelObj = datastageModels.find(m => m.model === selectedModel);
|
|
|
|
|
setSelectedFields(modelObj.fields);
|
|
|
|
|
setAvailableSelected(null);
|
|
|
|
|
setFilters(modelObj.filters);
|
|
|
|
|
}, [selectedModel]);
|
|
|
|
|
|
|
|
|
|
// Encontrar el modelo actual
|
|
|
|
|
const currentModel = datastageModels.find(m => m.model === selectedModel);
|
|
|
|
|
|
|
|
|
|
// Campos disponibles (no seleccionados)
|
|
|
|
|
const availableFields = currentModel.fields.filter(f => !selectedFields.includes(f));
|
|
|
|
|
|
|
|
|
|
// Mover campo de disponible a seleccionado
|
|
|
|
|
const addField = (field) => {
|
|
|
|
|
setSelectedFields([...selectedFields, field]);
|
|
|
|
|
setAvailableSelected(null);
|
|
|
|
|
};
|
|
|
|
|
// Mover campo de seleccionado a disponible
|
|
|
|
|
const removeField = (field) => {
|
|
|
|
|
setSelectedFields(selectedFields.filter(f => f !== field));
|
|
|
|
|
};
|
|
|
|
|
// Incluir todos los campos
|
|
|
|
|
const includeAllFields = () => {
|
|
|
|
|
setSelectedFields([...currentModel.fields]);
|
|
|
|
|
setAvailableSelected(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const limpiarFiltros = () => {
|
|
|
|
|
setTipoReporte('');
|
|
|
|
|
setFechaInicio('');
|
|
|
|
|
setFechaFin('');
|
|
|
|
|
// Renderizar selector dual de campos
|
|
|
|
|
// Estado para campo seleccionado en lista de incluidos
|
|
|
|
|
const [includedSelected, setIncludedSelected] = useState(null);
|
|
|
|
|
|
|
|
|
|
// Quitar todos los campos
|
|
|
|
|
const removeAllFields = () => {
|
|
|
|
|
setSelectedFields([]);
|
|
|
|
|
setIncludedSelected(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Quitar campo seleccionado en incluidos
|
|
|
|
|
const removeSelectedField = () => {
|
|
|
|
|
if (includedSelected) {
|
|
|
|
|
removeField(includedSelected);
|
|
|
|
|
setIncludedSelected(null);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Función para manejar la exportación del modelo
|
|
|
|
|
const handleExportModel = async () => {
|
|
|
|
|
setIsExporting(true);
|
|
|
|
|
try {
|
|
|
|
|
// Construir el objeto de filtros con los valores no vacíos
|
|
|
|
|
const nonEmptyFilters = {};
|
|
|
|
|
for (const [key, value] of Object.entries(filters)) {
|
|
|
|
|
if (value && value.trim() !== '') {
|
|
|
|
|
nonEmptyFilters[key] = value.trim();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Construir el payload
|
|
|
|
|
const payload = {
|
|
|
|
|
model: currentModel.model,
|
|
|
|
|
fields: selectedFields,
|
|
|
|
|
type: 'csv', // Usamos CSV como formato por defecto
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Solo agregar filtros si hay alguno con valor
|
|
|
|
|
if (Object.keys(nonEmptyFilters).length > 0) {
|
|
|
|
|
payload.filters = nonEmptyFilters;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Log para debug
|
|
|
|
|
console.log('Sending payload:', payload);
|
|
|
|
|
|
|
|
|
|
// Realizar la petición POST
|
|
|
|
|
const response = await fetchWithAuth(`${API_URL}/reports/exportmodel/`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(payload)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
// Intentar obtener el mensaje de error del backend
|
|
|
|
|
const errorData = await response.json().catch(() => ({}));
|
|
|
|
|
console.error('Error response:', {
|
|
|
|
|
status: response.status,
|
|
|
|
|
statusText: response.statusText,
|
|
|
|
|
errorData
|
|
|
|
|
});
|
|
|
|
|
throw new Error(errorData.error || errorData.message || 'Error al exportar el modelo');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Obtener el blob del archivo según el tipo
|
|
|
|
|
const blob = await response.blob();
|
|
|
|
|
const contentType = payload.type === 'excel'
|
|
|
|
|
? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
|
|
|
: 'text/csv';
|
|
|
|
|
|
|
|
|
|
// Crear URL del blob
|
|
|
|
|
const url = window.URL.createObjectURL(
|
|
|
|
|
new Blob([blob], { type: contentType })
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Crear elemento <a> temporal para la descarga
|
|
|
|
|
const link = document.createElement('a');
|
|
|
|
|
link.href = url;
|
|
|
|
|
|
|
|
|
|
// Obtener el nombre del archivo del header Content-Disposition o usar uno por defecto
|
|
|
|
|
const contentDisposition = response.headers.get('Content-Disposition');
|
|
|
|
|
const extension = payload.type === 'csv' ? 'csv' : 'xlsx';
|
|
|
|
|
const fileName = contentDisposition
|
|
|
|
|
? contentDisposition.match(/filename="(.+?)"/)?.pop() || `${currentModel.model}.${extension}`
|
|
|
|
|
: `${currentModel.model}.${extension}`;
|
|
|
|
|
|
|
|
|
|
link.setAttribute('download', fileName);
|
|
|
|
|
document.body.appendChild(link);
|
|
|
|
|
link.click();
|
|
|
|
|
|
|
|
|
|
// Limpiar recursos
|
|
|
|
|
document.body.removeChild(link);
|
|
|
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error al exportar:', error);
|
|
|
|
|
alert(error.message || 'Error al exportar el modelo. Por favor intente nuevamente.');
|
|
|
|
|
} finally {
|
|
|
|
|
setIsExporting(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderFields = () => (
|
|
|
|
|
<div className="mb-4 flex flex-col lg:flex-row gap-4">
|
|
|
|
|
{/* Lista de campos disponibles */}
|
|
|
|
|
<div className="flex-1 bg-white/95 backdrop-blur-sm rounded-xl border border-blue-100 shadow-lg p-4">
|
|
|
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-3">
|
|
|
|
|
<span className="font-bold text-blue-800 text-sm">Campos disponibles</span>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<button
|
|
|
|
|
className="px-3 py-1.5 text-xs bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 font-semibold transition-all duration-200 hover:shadow-md"
|
|
|
|
|
onClick={includeAllFields}
|
|
|
|
|
>
|
|
|
|
|
Incluir todos
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
className="inline-flex items-center px-3 py-1.5 bg-blue-500 text-white rounded-lg shadow-md hover:bg-blue-600 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed text-xs font-semibold hover:shadow-lg"
|
|
|
|
|
onClick={() => availableSelected && addField(availableSelected)}
|
|
|
|
|
disabled={!availableSelected}
|
|
|
|
|
title="Incluir campo seleccionado"
|
|
|
|
|
>
|
|
|
|
|
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" /></svg>
|
|
|
|
|
Incluir
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<ul className="max-h-56 overflow-y-auto divide-y divide-blue-50">
|
|
|
|
|
{availableFields.length === 0 && (
|
|
|
|
|
<li className="text-gray-400 text-xs py-2">Todos los campos incluidos</li>
|
|
|
|
|
)}
|
|
|
|
|
{availableFields.map(field => (
|
|
|
|
|
<li
|
|
|
|
|
key={field}
|
|
|
|
|
className={`px-2 py-1 cursor-pointer rounded transition-all duration-100 select-none ${availableSelected === field ? 'bg-blue-100 text-blue-700' : 'hover:bg-blue-50'}`}
|
|
|
|
|
onClick={() => setAvailableSelected(field)}
|
|
|
|
|
onDoubleClick={() => addField(field)}
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
>
|
|
|
|
|
{field}
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
{/* Lista de campos seleccionados */}
|
|
|
|
|
<div className="flex-1 bg-white/95 backdrop-blur-sm rounded-xl border border-green-100 shadow-lg p-4">
|
|
|
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-3">
|
|
|
|
|
<span className="font-bold text-green-800 text-sm">Campos incluidos</span>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<button
|
|
|
|
|
className="px-3 py-1.5 text-xs bg-green-100 text-green-700 rounded-lg hover:bg-green-200 font-semibold transition-all duration-200 hover:shadow-md disabled:opacity-40 disabled:cursor-not-allowed"
|
|
|
|
|
onClick={removeAllFields}
|
|
|
|
|
disabled={selectedFields.length === 0}
|
|
|
|
|
>
|
|
|
|
|
Quitar todos
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
className="px-3 py-1.5 text-xs bg-green-100 text-green-700 rounded-lg hover:bg-green-200 font-semibold transition-all duration-200 hover:shadow-md disabled:opacity-40 disabled:cursor-not-allowed"
|
|
|
|
|
onClick={removeSelectedField}
|
|
|
|
|
disabled={!includedSelected}
|
|
|
|
|
>
|
|
|
|
|
Quitar
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<ul className="max-h-56 overflow-y-auto divide-y divide-green-50">
|
|
|
|
|
{selectedFields.length === 0 && (
|
|
|
|
|
<li className="text-gray-400 text-xs py-2">No hay campos seleccionados</li>
|
|
|
|
|
)}
|
|
|
|
|
{selectedFields.map(field => (
|
|
|
|
|
<li
|
|
|
|
|
key={field}
|
|
|
|
|
className={`px-2 py-1 cursor-pointer rounded transition-all duration-100 select-none ${includedSelected === field ? 'bg-green-100 text-green-700' : 'hover:bg-green-50'}`}
|
|
|
|
|
onClick={() => setIncludedSelected(field)}
|
|
|
|
|
onDoubleClick={() => removeField(field)}
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
title="Doble click para quitar"
|
|
|
|
|
>
|
|
|
|
|
{field}
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const renderFilters = () => (
|
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-4 bg-gradient-to-br from-blue-50/60 to-blue-100/30 p-4 sm:p-5 rounded-xl border border-blue-100 shadow-lg backdrop-blur-sm">
|
|
|
|
|
{Object.entries(currentModel.filters).map(([key, value]) => (
|
|
|
|
|
<div key={key} className="flex flex-col">
|
|
|
|
|
<label className="block text-xs font-semibold text-blue-800 mb-1.5">{key}</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={filters[key] || ''}
|
|
|
|
|
onChange={(e) => setFilters(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[key]: e.target.value
|
|
|
|
|
}))}
|
|
|
|
|
className="w-full border border-blue-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-blue-400 bg-white/90 shadow-md transition-all duration-200 hover:shadow-lg"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Contenido de cada pestaña
|
|
|
|
|
const tabContents = {
|
|
|
|
|
pedimentos: (
|
|
|
|
|
<div className="p-6">
|
|
|
|
|
<h2 className="text-xl font-bold mb-2 text-blue-900">Generar reporte de pedimentos cargados</h2>
|
|
|
|
|
<p className="mb-4 text-gray-700">Aquí puedes generar y descargar el reporte de pedimentos cargados.</p>
|
|
|
|
|
{/* Aquí va la lógica y UI específica para pedimentos */}
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
datastage: (
|
|
|
|
|
<div className="p-6 bg-white/95 rounded-2xl shadow-xl border border-blue-100">
|
|
|
|
|
<div className="relative mb-8">
|
|
|
|
|
<div className="absolute -left-2 -top-2 w-12 h-12 bg-gradient-to-br from-blue-600 to-blue-700 rounded-2xl shadow-lg flex items-center justify-center transform -rotate-6">
|
|
|
|
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 7v10c0 2 1.5 3 3 3h10c1.5 0 3-1 3-3V7c0-2-1.5-3-3-3H7C5.5 4 4 5 4 7zm8-1v14m-4-3h8" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="pl-12">
|
|
|
|
|
<h2 className="text-2xl font-bold text-gray-900">Generar reporte de datastage</h2>
|
|
|
|
|
<p className="text-gray-600 mt-1">Selecciona el modelo, revisa los campos y ajusta los filtros para generar el reporte.</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mb-6">
|
|
|
|
|
<label className="block text-xs font-semibold text-blue-800 mb-2">Modelo</label>
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<select
|
|
|
|
|
value={selectedModel}
|
|
|
|
|
onChange={e => setSelectedModel(e.target.value)}
|
|
|
|
|
className="w-full border border-blue-200 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-blue-400 bg-white/90 shadow-md appearance-none pr-10 transition-all duration-200 hover:shadow-lg"
|
|
|
|
|
>
|
|
|
|
|
{datastageModels.map(m => (
|
|
|
|
|
<option key={m.model} value={m.model} className="py-1">
|
|
|
|
|
{m.model} - {m.name}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
<div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
|
|
|
|
|
<svg className="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mb-6">
|
|
|
|
|
<h3 className="text-md font-bold text-blue-800 mb-3">Campos</h3>
|
|
|
|
|
{renderFields()}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mb-6">
|
|
|
|
|
<h3 className="text-md font-bold text-blue-800 mb-3">Filtros</h3>
|
|
|
|
|
{renderFilters()}
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleExportModel}
|
|
|
|
|
disabled={isExporting}
|
|
|
|
|
className={`group relative w-full py-3 text-lg font-semibold ${
|
|
|
|
|
isExporting
|
|
|
|
|
? 'bg-gray-400 cursor-not-allowed'
|
|
|
|
|
: 'bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700'
|
|
|
|
|
} text-white rounded-xl transition-all duration-300 transform hover:scale-[1.02] shadow-lg hover:shadow-xl shadow-green-500/20 hover:shadow-green-500/30 overflow-hidden`}
|
|
|
|
|
>
|
|
|
|
|
<div className={`absolute inset-0 w-full h-full ${
|
|
|
|
|
isExporting
|
|
|
|
|
? 'bg-gradient-to-r from-gray-300/0 via-gray-300/30 to-gray-300/0'
|
|
|
|
|
: 'bg-gradient-to-r from-green-400/0 via-green-400/30 to-green-400/0'
|
|
|
|
|
} skeleton-animation`}></div>
|
|
|
|
|
<span className="relative inline-flex items-center justify-center gap-2 px-4">
|
|
|
|
|
{isExporting ? (
|
|
|
|
|
<svg className="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
|
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
|
|
|
<path className="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"></path>
|
|
|
|
|
</svg>
|
|
|
|
|
) : (
|
|
|
|
|
<svg className="w-5 h-5 transform group-hover:scale-110 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
|
|
|
</svg>
|
|
|
|
|
)}
|
|
|
|
|
<span>{isExporting ? 'Generando archivo...' : 'Generar y descargar CSV'}</span>
|
|
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<style>{`
|
|
|
|
|
.skeleton-animation {
|
|
|
|
|
animation: shimmer 2s linear infinite;
|
|
|
|
|
background-size: 200% 100%;
|
|
|
|
|
}
|
|
|
|
|
@keyframes shimmer {
|
|
|
|
|
0% { transform: translateX(-100%); }
|
|
|
|
|
100% { transform: translateX(100%); }
|
|
|
|
|
}
|
|
|
|
|
`}</style>
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
minimos: (
|
|
|
|
|
<div className="p-6">
|
|
|
|
|
<h2 className="text-xl font-bold mb-2 text-blue-900">Generar reporte de Mínimos</h2>
|
|
|
|
|
<p className="mb-4 text-gray-700">Aquí puedes generar y descargar el reporte de Mínimos.</p>
|
|
|
|
|
{/* Aquí va la lógica y UI específica para mínimos */}
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
coves: (
|
|
|
|
|
<div className="p-6">
|
|
|
|
|
<h2 className="text-xl font-bold mb-2 text-blue-900">Generar reporte de COVES</h2>
|
|
|
|
|
<p className="mb-4 text-gray-700">Aquí puedes generar y descargar el reporte de COVES.</p>
|
|
|
|
|
{/* Aquí va la lógica y UI específica para COVES */}
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-blue-50 p-4 lg:p-6">
|
|
|
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 p-4 sm:p-6 lg:p-8">
|
|
|
|
|
<div className="max-w-7xl mx-auto">
|
|
|
|
|
{/* Header principal */}
|
|
|
|
|
<div className="mb-6 animate-fadein-slideup opacity-0" style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards' }}>
|
|
|
|
|
<div className="bg-white/80 backdrop-blur-xl rounded-2xl shadow-xl border border-blue-100/50 p-6 lg:p-8">
|
|
|
|
|
<div className="flex items-start space-x-4">
|
|
|
|
|
<div className="p-3 bg-gradient-to-br from-blue-500 to-blue-700 rounded-xl shadow-lg flex-shrink-0">
|
|
|
|
|
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900 mb-1">
|
|
|
|
|
Centro de Reportes
|
|
|
|
|
</h1>
|
|
|
|
|
<p className="text-gray-600">
|
|
|
|
|
Consulta, genera y descarga reportes relacionados con el sistema aduanero
|
|
|
|
|
</p>
|
|
|
|
|
<div className="mt-3">
|
|
|
|
|
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
|
|
|
|
|
📊 {reportes.length} reportes disponibles
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/* Header mejorado y decorativo */}
|
|
|
|
|
<div className="mb-6 sm:mb-8 relative overflow-hidden rounded-3xl shadow-2xl bg-gradient-to-r from-blue-600 via-blue-700 to-blue-800 p-6 sm:p-8 flex items-center gap-4 sm:gap-6 animate-fadein-slideup opacity-0"
|
|
|
|
|
style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards' }}>
|
|
|
|
|
<div className="flex-shrink-0 bg-white/20 backdrop-blur-sm rounded-full p-3 sm:p-4 shadow-lg animate-bounce-slow">
|
|
|
|
|
<svg className="h-8 w-8 sm:h-10 sm:w-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-extrabold text-white tracking-tight mb-1">Centro de Reportes</h1>
|
|
|
|
|
<p className="text-sm sm:text-lg text-blue-100 font-medium leading-relaxed">
|
|
|
|
|
Consulta, genera y descarga reportes relacionados con el sistema aduanero
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
{/* Efectos decorativos de fondo */}
|
|
|
|
|
<div className="absolute -top-10 -right-10 opacity-20 pointer-events-none select-none">
|
|
|
|
|
<div className="w-32 h-32 bg-white/10 rounded-full blur-xl"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="absolute -bottom-6 -left-6 opacity-15 pointer-events-none select-none">
|
|
|
|
|
<div className="w-24 h-24 bg-white/10 rounded-full blur-lg"></div>
|
|
|
|
|
</div>
|
|
|
|
|
{/* Partículas flotantes */}
|
|
|
|
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
|
|
|
|
<div className="absolute top-1/4 left-1/4 w-2 h-2 bg-white/30 rounded-full animate-ping"></div>
|
|
|
|
|
<div className="absolute top-3/4 right-1/3 w-1 h-1 bg-white/40 rounded-full animate-pulse"></div>
|
|
|
|
|
<div className="absolute top-1/2 right-1/4 w-3 h-3 bg-white/20 rounded-full animate-bounce"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<style>{`
|
|
|
|
|
@keyframes bounce-slow {
|
|
|
|
|
0%, 100% { transform: translateY(0) scale(1); }
|
|
|
|
|
50% { transform: translateY(-8px) scale(1.05); }
|
|
|
|
|
}
|
|
|
|
|
.animate-bounce-slow {
|
|
|
|
|
animation: bounce-slow 3s infinite;
|
|
|
|
|
}
|
|
|
|
|
`}</style>
|
|
|
|
|
|
|
|
|
|
{/* Panel de filtros mejorado */}
|
|
|
|
|
<div className="mb-6 animate-fadein-slideup opacity-0" style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.15s forwards' }}>
|
|
|
|
|
<div className="bg-white/80 backdrop-blur-xl rounded-2xl shadow-xl border border-blue-100/50">
|
|
|
|
|
<div className="px-6 py-4 border-b border-gray-200/50">
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<div className="p-2 bg-gradient-to-br from-blue-500 to-blue-700 rounded-xl">
|
|
|
|
|
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
|
|
|
|
{/* Pestañas */}
|
|
|
|
|
<div className="mb-4 sm:mb-6 animate-fadein-slideup opacity-0" style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.15s forwards' }}>
|
|
|
|
|
<div className="bg-white/95 backdrop-blur-xl rounded-2xl shadow-xl border border-blue-100">
|
|
|
|
|
<div className="flex flex-col sm:flex-row border-b border-blue-200/50 p-1 gap-1">
|
|
|
|
|
<button
|
|
|
|
|
className={`flex-1 py-3 px-4 text-sm font-semibold rounded-xl focus:outline-none transition-all duration-200 ${
|
|
|
|
|
activeTab === 'pedimentos'
|
|
|
|
|
? 'bg-gradient-to-br from-blue-600 to-blue-700 text-white shadow-lg shadow-blue-500/20 scale-[1.02]'
|
|
|
|
|
: 'text-gray-700 hover:bg-blue-50/80'
|
|
|
|
|
}`}
|
|
|
|
|
onClick={() => setActiveTab('pedimentos')}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center justify-center gap-2">
|
|
|
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span>Pedimentos cargados</span>
|
|
|
|
|
</div>
|
|
|
|
|
<h2 className="text-xl font-bold text-gray-900">Filtros de búsqueda</h2>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="p-6">
|
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
|
|
|
|
{/* Tipo de reporte */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-xs font-semibold text-gray-700 mb-1">Tipo de reporte</label>
|
|
|
|
|
<select
|
|
|
|
|
value={tipoReporte}
|
|
|
|
|
onChange={(e) => setTipoReporte(e.target.value)}
|
|
|
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white"
|
|
|
|
|
>
|
|
|
|
|
<option value="">Todos los tipos</option>
|
|
|
|
|
<option value="General">General</option>
|
|
|
|
|
<option value="Usuarios">Usuarios</option>
|
|
|
|
|
<option value="Documentos">Documentos</option>
|
|
|
|
|
<option value="Procesos">Procesos</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Fecha inicio */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-xs font-semibold text-gray-700 mb-1">Fecha inicio</label>
|
|
|
|
|
<input
|
|
|
|
|
type="date"
|
|
|
|
|
value={fechaInicio}
|
|
|
|
|
onChange={(e) => setFechaInicio(e.target.value)}
|
|
|
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Fecha fin */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-xs font-semibold text-gray-700 mb-1">Fecha fin</label>
|
|
|
|
|
<input
|
|
|
|
|
type="date"
|
|
|
|
|
value={fechaFin}
|
|
|
|
|
onChange={(e) => setFechaFin(e.target.value)}
|
|
|
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Botón filtrar */}
|
|
|
|
|
<div className="flex items-end">
|
|
|
|
|
<button className="w-full inline-flex items-center justify-center px-4 py-2 bg-gradient-to-r from-blue-500 to-blue-700 hover:from-blue-600 hover:to-blue-800 text-white text-sm font-medium rounded-lg transition-all duration-200 transform hover:scale-105 shadow-lg">
|
|
|
|
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
|
|
|
</svg>
|
|
|
|
|
Filtrar
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Botón limpiar */}
|
|
|
|
|
<div className="flex items-end">
|
|
|
|
|
<button
|
|
|
|
|
onClick={limpiarFiltros}
|
|
|
|
|
className="w-full px-4 py-2 text-sm font-medium text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Limpiar filtros
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Sección de resultados */}
|
|
|
|
|
<div className="bg-white/80 backdrop-blur-xl shadow-xl rounded-2xl border border-blue-100/50 animate-fadein-slideup opacity-0"
|
|
|
|
|
style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.25s forwards' }}>
|
|
|
|
|
|
|
|
|
|
{/* Header de la sección */}
|
|
|
|
|
<div className="px-6 py-4 border-b border-gray-200/50">
|
|
|
|
|
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex items-center gap-3 mb-2">
|
|
|
|
|
<div className="p-2 bg-gradient-to-br from-blue-500 to-blue-700 rounded-xl">
|
|
|
|
|
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<h2 className="text-xl font-bold text-gray-900">Reportes disponibles</h2>
|
|
|
|
|
</div>
|
|
|
|
|
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
|
|
|
|
|
📋 {reportes.length} reportes encontrados
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Botón descargar masivo */}
|
|
|
|
|
<div className="flex flex-col sm:flex-row gap-2">
|
|
|
|
|
<button className="inline-flex items-center justify-center px-4 py-2 bg-gradient-to-r from-green-500 to-green-700 hover:from-green-600 hover:to-green-800 text-white text-sm font-medium rounded-xl transition-all duration-200 transform hover:scale-105 shadow-lg">
|
|
|
|
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
className={`flex-1 py-3 px-4 text-sm font-semibold rounded-xl focus:outline-none transition-all duration-200 ${
|
|
|
|
|
activeTab === 'datastage'
|
|
|
|
|
? 'bg-gradient-to-br from-blue-600 to-blue-700 text-white shadow-lg shadow-blue-500/20 scale-[1.02]'
|
|
|
|
|
: 'text-gray-700 hover:bg-blue-50/80'
|
|
|
|
|
}`}
|
|
|
|
|
onClick={() => setActiveTab('datastage')}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center justify-center gap-2">
|
|
|
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 7v10c0 2 1.5 3 3 3h10c1.5 0 3-1 3-3V7c0-2-1.5-3-3-3H7C5.5 4 4 5 4 7zm8-1v14m-4-3h8" />
|
|
|
|
|
</svg>
|
|
|
|
|
Descargar Excel
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<button className="inline-flex items-center justify-center px-4 py-2 bg-gradient-to-r from-purple-500 to-purple-700 hover:from-purple-600 hover:to-purple-800 text-white text-sm font-medium rounded-xl transition-all duration-200 transform hover:scale-105 shadow-lg">
|
|
|
|
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" />
|
|
|
|
|
</svg>
|
|
|
|
|
Nuevo Reporte
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Contenido de reportes */}
|
|
|
|
|
<div className="p-6">
|
|
|
|
|
{/* Vista Desktop - Tabla */}
|
|
|
|
|
<div className="hidden lg:block">
|
|
|
|
|
<div className="overflow-x-auto">
|
|
|
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
|
|
|
<thead className="bg-gradient-to-r from-blue-500 to-blue-700">
|
|
|
|
|
<tr>
|
|
|
|
|
<th className="px-4 py-3 text-left text-xs font-bold text-white uppercase tracking-wider">ID</th>
|
|
|
|
|
<th className="px-4 py-3 text-left text-xs font-bold text-white uppercase tracking-wider">Nombre del Reporte</th>
|
|
|
|
|
<th className="px-4 py-3 text-left text-xs font-bold text-white uppercase tracking-wider">Tipo</th>
|
|
|
|
|
<th className="px-4 py-3 text-left text-xs font-bold text-white uppercase tracking-wider">Fecha</th>
|
|
|
|
|
<th className="px-4 py-3 text-left text-xs font-bold text-white uppercase tracking-wider">Estado</th>
|
|
|
|
|
<th className="px-4 py-3 text-center text-xs font-bold text-white uppercase tracking-wider">Acciones</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody className="bg-white/50 divide-y divide-gray-100">
|
|
|
|
|
{reportes.map((reporte, index) => (
|
|
|
|
|
<tr key={reporte.id} className="hover:bg-blue-50 transition-all duration-200">
|
|
|
|
|
<td className="px-4 py-3 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
|
|
|
#{reporte.id.toString().padStart(3, '0')}
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-4 py-3 whitespace-nowrap">
|
|
|
|
|
<div className="text-sm font-medium text-gray-900">{reporte.nombre}</div>
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-4 py-3 whitespace-nowrap">
|
|
|
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
|
|
|
|
{reporte.tipo}
|
|
|
|
|
</span>
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-700">
|
|
|
|
|
{new Date(reporte.fecha).toLocaleDateString('es-ES', {
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
month: 'short',
|
|
|
|
|
day: 'numeric'
|
|
|
|
|
})}
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-4 py-3 whitespace-nowrap">
|
|
|
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${getEstadoBadge(reporte.estado)}`}>
|
|
|
|
|
{reporte.estado}
|
|
|
|
|
</span>
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-4 py-3 whitespace-nowrap text-center">
|
|
|
|
|
<div className="flex justify-center space-x-2">
|
|
|
|
|
<button className="inline-flex items-center p-2 border border-gray-300 shadow-sm text-sm font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200"
|
|
|
|
|
title="Ver reporte">
|
|
|
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
<button className="inline-flex items-center p-2 border border-blue-300 shadow-sm text-sm font-medium rounded-lg text-blue-700 bg-white hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200"
|
|
|
|
|
title="Descargar">
|
|
|
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
))}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Vista Mobile - Cards */}
|
|
|
|
|
<div className="lg:hidden space-y-4">
|
|
|
|
|
{reportes.map((reporte, index) => (
|
|
|
|
|
<div
|
|
|
|
|
key={reporte.id}
|
|
|
|
|
className="bg-white/80 backdrop-blur-xl rounded-xl shadow-lg border border-gray-200/50 p-4 transition-all duration-200 hover:shadow-xl transform hover:scale-[1.02]"
|
|
|
|
|
style={{ animationDelay: `${index * 100}ms` }}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-start justify-between mb-3">
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<div className="flex items-center gap-2 mb-2">
|
|
|
|
|
<span className="text-xs font-medium text-gray-500">#{reporte.id.toString().padStart(3, '0')}</span>
|
|
|
|
|
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium border ${getEstadoBadge(reporte.estado)}`}>
|
|
|
|
|
{reporte.estado}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<h3 className="text-sm font-semibold text-gray-900 mb-1">
|
|
|
|
|
{reporte.nombre}
|
|
|
|
|
</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex space-x-1 ml-2">
|
|
|
|
|
<button className="p-2 text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
|
|
|
|
title="Ver reporte">
|
|
|
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
<button className="p-2 text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-lg transition-colors"
|
|
|
|
|
title="Descargar">
|
|
|
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<div className="flex items-center justify-between text-xs text-gray-600">
|
|
|
|
|
<span className="inline-flex items-center px-2 py-1 rounded-full bg-gray-100 text-gray-800 font-medium">
|
|
|
|
|
{reporte.tipo}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="flex items-center">
|
|
|
|
|
<svg className="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
|
|
</svg>
|
|
|
|
|
{new Date(reporte.fecha).toLocaleDateString('es-ES', {
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
month: 'short',
|
|
|
|
|
day: 'numeric'
|
|
|
|
|
})}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span>Datastage cargados</span>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
className={`flex-1 py-3 px-4 text-sm font-semibold rounded-xl focus:outline-none transition-all duration-200 ${
|
|
|
|
|
activeTab === 'minimos'
|
|
|
|
|
? 'bg-gradient-to-br from-blue-600 to-blue-700 text-white shadow-lg shadow-blue-500/20 scale-[1.02]'
|
|
|
|
|
: 'text-gray-700 hover:bg-blue-50/80'
|
|
|
|
|
}`}
|
|
|
|
|
onClick={() => setActiveTab('minimos')}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center justify-center gap-2">
|
|
|
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span>Mínimos</span>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
className={`flex-1 py-3 px-4 text-sm font-semibold rounded-xl focus:outline-none transition-all duration-200 ${
|
|
|
|
|
activeTab === 'coves'
|
|
|
|
|
? 'bg-gradient-to-br from-blue-600 to-blue-700 text-white shadow-lg shadow-blue-500/20 scale-[1.02]'
|
|
|
|
|
: 'text-gray-700 hover:bg-blue-50/80'
|
|
|
|
|
}`}
|
|
|
|
|
onClick={() => setActiveTab('coves')}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center justify-center gap-2">
|
|
|
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span>COVES</span>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
{/* Contenido de la pestaña activa */}
|
|
|
|
|
<div>{tabContents[activeTab]}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|