Se agrego modulo de reportes, en especifico el submodulo de reportes de Datastage

This commit is contained in:
2025-09-10 16:39:22 -06:00
parent 4ee5c0d19d
commit 6c020d4799
8 changed files with 3070 additions and 2453 deletions

2
.env
View File

@@ -1,4 +1,4 @@
VITE_DEBUG_MODE=false VITE_DEBUG_MODE=true
VITE_EFC_API_URL=http://192.168.1.195:8000/api/v1 VITE_EFC_API_URL=http://192.168.1.195:8000/api/v1
VITE_EFC_MICROSERVICE_URL=http://192.168.1.195:8001/api/v1 VITE_EFC_MICROSERVICE_URL=http://192.168.1.195:8001/api/v1

View File

@@ -5,10 +5,9 @@ WORKDIR /app
COPY package*.json ./ COPY package*.json ./
COPY vite.config.* ./ COPY vite.config.* ./
RUN npm install
COPY . . COPY . .
EXPOSE 5173 EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host"] CMD ["npm", "run", "dev"]

View File

@@ -9,7 +9,6 @@ services:
- "5173:5173" - "5173:5173"
volumes: volumes:
- .:/app - .:/app
- /app/node_modules
environment: environment:
- NODE_ENV=development - NODE_ENV=development
command: ["npm", "run", "dev", "--", "--host"] command: ["npm", "run", "dev", "--", "--host"]

4625
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -35,6 +35,6 @@
"globals": "^16.0.0", "globals": "^16.0.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"vite": "^6.3.5" "vite": "^6.3.6"
} }
} }

View File

@@ -13,12 +13,12 @@ export async function login(username, password) {
return response.json(); // { access, refresh } return response.json(); // { access, refresh }
} }
export async function refreshToken(refresh) { // export async function refreshToken(refresh) {
const res = await fetch(`${API_URL}/token/refresh/`, { // const res = await fetch(`${API_URL}/token/refresh/`, {
method: 'POST', // method: 'POST',
headers: { 'Content-Type': 'application/json' }, // headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh }), // body: JSON.stringify({ refresh }),
}); // });
if (!res.ok) throw new Error('SESSION_EXPIRED'); // if (!res.ok) throw new Error('SESSION_EXPIRED');
return res.json(); // { access: '...' } // return res.json(); // { access: '...' }
} // }

View File

@@ -1,3 +1,42 @@
// --- DATSTAGE CONFIG ---
const datastageModels = [
{"model":"Registro500","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":"1234","pedimento":"5678901","seccion_aduanera":"001","fecha_seleccion":"2023-01-03","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro501","fields":["id","patente","pedimento","seccion_aduanera","tipo_operacion","clave_documento","seccion_aduanera_entrada","curp_contribuyente","rfc","curp_agente_a"],"filters":{"patente":"1234","pedimento":"5678901","seccion_aduanera":"001","tipo_operacion":"IMPORT","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro502","fields":["id","patente","pedimento","seccion_aduanera","tipo_operacion","clave_documento","fecha_pago_real","organizacion","created_by","consulta","datastage"],"filters":{"patente":"1234","pedimento":"5678901","fecha_pago_real":"2023-01-01","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro503","fields":["id","patente","pedimento","seccion_aduanera","tipo_operacion","clave_documento","fecha_pago_real","organizacion","created_by","consulta","datastage"],"filters":{"patente":"1234","pedimento":"5678901","fecha_pago_real":"2023-01-01","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro504","fields":["id","patente","pedimento","seccion_aduanera","num_contenedor","tipo_contenedor","fecha_pago_real","organizacion","created_by","consulta","datastage"],"filters":{"patente":"1234","pedimento":"5678901","num_contenedor":"CONT001","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro505","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":"5678901","numero_factura":"FAC001","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro506","fields":["id","patente","pedimento","seccion_aduanera","tipo_fecha","fecha_operacion","fecha_validacion_pago_r","organizacion","created_by","consulta","datastage"],"filters":{"patente":"1234","fecha_operacion":"2023-01-01","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro507","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":"1234","clave_caso":"CASO001","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro508","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":"1234","institucion_emisora":"IE01","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro509","fields":["id","patente","pedimento","seccion_aduanera","clave_contribucion","tasa_contribucion","tipo_tasa","tipo_pedimento","fecha_pago_real","organizacion","created_by","consulta","datastage"],"filters":{"patente":"1234","clave_contribucion":"CC01","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro510","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":"1234","clave_contribucion":"CC01","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro511","fields":["id","patente","pedimento","seccion_aduanera","secuencia_observacion","observaciones","tipo_pedimento","fecha_validacion_pago_r","organizacion","created_by","consulta","datastage"],"filters":{"patente":"1234","secuencia_observacion":"OBS01","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro512","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":"1234","patente_aduanal_orig":"PAO01","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro520","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":"1234","indent_fiscal_destinatario":"IFD01","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro551","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":"1234","fraccion":"FRAC001","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro552","fields":["id","patente","pedimento","seccion_aduanera","fraccion","secuencia_fraccion","vin_numero_serie","kilometraje_vehiculo","fecha_pago_real","organizacion","created_by","consulta","datastage"],"filters":{"patente":"1234","fraccion":"FRAC001","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro553","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":"1234","fraccion":"FRAC001","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro554","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":"1234","fraccion":"FRAC001","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro555","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":"1234","fraccion":"FRAC001","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro556","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":"1234","clave_contribucion":"CC01","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro557","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":"1234","fraccion":"FRAC001","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro558","fields":["id","patente","pedimento","seccion_aduanera","fraccion","secuencia_fraccion","secuencia_observacion","observaciones","fecha_pago_real","organizacion","created_by","datastage","consulta"],"filters":{"patente":"1234","fraccion":"FRAC001","organizacion":1,"datastage":1},"type":"excel"},
{"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":"1234","consecutivo_remesa":"REM001","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro701","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":"1234","pedimento_anterior":"PEDANT01","organizacion":1,"datastage":1},"type":"excel"},
{"model":"Registro702","fields":["id","patente","pedimento","seccion_aduanera","clave_contribucion","forma_pago","importe_pago","tipo_pedimento","fecha_pago_real","organizacion","created_by","consulta","datastage"],"filters":{"patente":"1234","clave_contribucion":"CC01","organizacion":1,"datastage":1},"type":"excel"}
];
const [selectedModel, setSelectedModel] = useState('');
const [filterValues, setFilterValues] = useState({});
// Obtiene el modelo seleccionado
const currentModel = datastageModels.find(m => m.model === selectedModel);
// Actualiza los filtros dinámicamente
const handleFilterChange = (key, value) => {
setFilterValues(prev => ({ ...prev, [key]: value }));
};
import React, { useState } from "react"; import React, { useState } from "react";
// Animación fade-in/slide-up para bloques // Animación fade-in/slide-up para bloques
@@ -44,6 +83,62 @@ export default function Reportes() {
return ( 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-blue-50 via-white to-blue-50 p-4 lg:p-6">
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
{/* Sección Datastage */}
<div className="mb-8 animate-fadein-slideup opacity-0" style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.01s forwards' }}>
<div className="bg-white/80 backdrop-blur-xl rounded-2xl shadow-xl border border-blue-100/50 p-6">
<h2 className="text-xl font-bold text-blue-900 mb-4">Reportes Datastage</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Selector de modelo */}
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1">Tabla / Modelo</label>
<select
value={selectedModel}
onChange={e => {
setSelectedModel(e.target.value);
setFilterValues({});
}}
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="">Selecciona una tabla</option>
{datastageModels.map(m => (
<option key={m.model} value={m.model}>{m.model}</option>
))}
</select>
</div>
{/* Campos del modelo */}
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1">Campos disponibles</label>
<div className="flex flex-wrap gap-2">
{currentModel ? currentModel.fields.map(f => (
<span key={f} className="inline-block bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs font-mono border border-blue-200">{f}</span>
)) : <span className="text-gray-400 italic">Selecciona un modelo</span>}
</div>
</div>
{/* Filtros dinámicos */}
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1">Filtros</label>
{currentModel ? (
<form className="space-y-2">
{Object.keys(currentModel.filters).map(key => (
<div key={key}>
<label className="block text-xs text-gray-600 mb-0.5">{key}</label>
<input
type="text"
value={filterValues[key] ?? ''}
onChange={e => handleFilterChange(key, e.target.value)}
className="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={String(currentModel.filters[key])}
/>
</div>
))}
</form>
) : <span className="text-gray-400 italic">Selecciona un modelo para ver filtros</span>}
</div>
</div>
</div>
</div>
{/* Header principal */} {/* 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="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="bg-white/80 backdrop-blur-xl rounded-2xl shadow-xl border border-blue-100/50 p-6 lg:p-8">

View File

@@ -1,315 +1,518 @@
import React, { useState } from "react"; 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 // Animación fade-in/slide-up para bloques
const fadeInSlideUp = `@keyframes fadein-slideup { const fadeInSlideUp = `@keyframes fadein-slideup {
0% { opacity: 0; transform: translateY(40px); } 0% { opacity: 0; transform: translateY(40px); }
100% { opacity: 1; transform: translateY(0); } 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'); const style = document.createElement('style');
style.id = 'fadein-slideup-reportes'; style.id = 'fadein-slideup-reports';
style.innerHTML = fadeInSlideUp; style.innerHTML = fadeInSlideUp;
document.head.appendChild(style); document.head.appendChild(style);
} }
export default function Reportes() { export default function Reports() {
const [tipoReporte, setTipoReporte] = useState(''); const [isExporting, setIsExporting] = useState(false);
const [fechaInicio, setFechaInicio] = useState('');
const [fechaFin, setFechaFin] = useState('');
// Datos de ejemplo // Estado para pestañas
const reportes = [ const [activeTab, setActiveTab] = useState('pedimentos');
{ 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' }, // JSON de modelos para Datastage
{ id: 3, nombre: 'Resumen de procesos aduaneros', tipo: 'Procesos', fecha: '2025-08-05', estado: 'Completado' }, const datastageModels = [
{ id: 4, nombre: 'Estadísticas generales del sistema', tipo: 'General', fecha: '2025-08-04', estado: 'Completado' }, { 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: ""}},
{ id: 5, nombre: 'Reporte de expedientes', tipo: 'Documentos', fecha: '2025-08-03', estado: 'Error' }, { 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 = { // Estado para modelo seleccionado en Datastage
'Completado': 'bg-emerald-100 text-emerald-800 border-emerald-200', const [selectedModel, setSelectedModel] = useState(datastageModels[0].model);
'Procesando': 'bg-yellow-100 text-yellow-800 border-yellow-200',
'Error': 'bg-red-100 text-red-800 border-red-200' // 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);
}; };
return styles[estado] || 'bg-gray-100 text-gray-800 border-gray-200'; // 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 = () => { // Renderizar selector dual de campos
setTipoReporte(''); // Estado para campo seleccionado en lista de incluidos
setFechaInicio(''); const [includedSelected, setIncludedSelected] = useState(null);
setFechaFin('');
// 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 ( 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"> <div className="max-w-7xl mx-auto">
{/* Header principal */} {/* Header mejorado y decorativo */}
<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="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"
<div className="bg-white/80 backdrop-blur-xl rounded-2xl shadow-xl border border-blue-100/50 p-6 lg:p-8"> style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards' }}>
<div className="flex items-start space-x-4"> <div className="flex-shrink-0 bg-white/20 backdrop-blur-sm rounded-full p-3 sm:p-4 shadow-lg animate-bounce-slow">
<div className="p-3 bg-gradient-to-br from-blue-500 to-blue-700 rounded-xl shadow-lg flex-shrink-0"> <svg className="h-8 w-8 sm:h-10 sm:w-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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 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" />
<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> </svg>
</div> </div>
<div> <div className="flex-1 min-w-0">
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900 mb-1"> <h1 className="text-2xl sm:text-3xl lg:text-4xl font-extrabold text-white tracking-tight mb-1">Centro de Reportes</h1>
Centro de Reportes <p className="text-sm sm:text-lg text-blue-100 font-medium leading-relaxed">
</h1>
<p className="text-gray-600">
Consulta, genera y descarga reportes relacionados con el sistema aduanero Consulta, genera y descarga reportes relacionados con el sistema aduanero
</p> </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>
{/* 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>
<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> </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>
</div> </div>
{/* Panel de filtros mejorado */} <style>{`
<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' }}> @keyframes bounce-slow {
<div className="bg-white/80 backdrop-blur-xl rounded-2xl shadow-xl border border-blue-100/50"> 0%, 100% { transform: translateY(0) scale(1); }
<div className="px-6 py-4 border-b border-gray-200/50"> 50% { transform: translateY(-8px) scale(1.05); }
<div className="flex items-center gap-3"> }
<div className="p-2 bg-gradient-to-br from-blue-500 to-blue-700 rounded-xl"> .animate-bounce-slow {
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> animation: bounce-slow 3s infinite;
<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" /> }
</svg> `}</style>
</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 */} {/* Pestañas */}
<div> <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' }}>
<label className="block text-xs font-semibold text-gray-700 mb-1">Fecha inicio</label> <div className="bg-white/95 backdrop-blur-xl rounded-2xl shadow-xl border border-blue-100">
<input <div className="flex flex-col sm:flex-row border-b border-blue-200/50 p-1 gap-1">
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 <button
onClick={limpiarFiltros} className={`flex-1 py-3 px-4 text-sm font-semibold rounded-xl focus:outline-none transition-all duration-200 ${
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" 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')}
> >
Limpiar filtros <div className="flex items-center justify-center gap-2">
</button> <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
</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" /> <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> </svg>
<span>Pedimentos cargados</span>
</div> </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" />
</svg>
Descargar Excel
</button> </button>
<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"> className={`flex-1 py-3 px-4 text-sm font-semibold rounded-xl focus:outline-none transition-all duration-200 ${
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> activeTab === 'datastage'
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" /> ? 'bg-gradient-to-br from-blue-600 to-blue-700 text-white shadow-lg shadow-blue-500/20 scale-[1.02]'
</svg> : 'text-gray-700 hover:bg-blue-50/80'
Nuevo Reporte }`}
</button> onClick={() => setActiveTab('datastage')}
</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 items-center justify-center gap-2">
<div className="flex-1"> <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div className="flex items-center gap-2 mb-2"> <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" />
<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> </svg>
<span>Datastage cargados</span>
</div>
</button> </button>
<button className="p-2 text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-lg transition-colors" <button
title="Descargar"> className={`flex-1 py-3 px-4 text-sm font-semibold rounded-xl focus:outline-none transition-all duration-200 ${
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> activeTab === 'minimos'
<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" /> ? '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> </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> </button>
</div> </div>
</div> {/* Contenido de la pestaña activa */}
<div>{tabContents[activeTab]}</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>
</div>
))}
</div>
</div> </div>
</div> </div>
</div> </div>