Se agregaron fecha de pago en PedimentoDetail y expedientes
This commit is contained in:
4
.env
4
.env
@@ -1,4 +1,4 @@
|
||||
VITE_DEBUG_MODE=true
|
||||
|
||||
VITE_EFC_API_URL=https://api.efc-aduanasoft.com/api/v1
|
||||
VITE_EFC_MICROSERVICE_URL=https://api.efc-aduanasoft.com/microservice/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
|
||||
|
||||
0
.env.example
Normal file
0
.env.example
Normal file
0
NOTIFICATIONS_README.md
Normal file
0
NOTIFICATIONS_README.md
Normal file
0
src/hooks/useRealTimeNotifications.js
Normal file
0
src/hooks/useRealTimeNotifications.js
Normal file
0
src/hooks/useRealTimeNotificationsSSE.js
Normal file
0
src/hooks/useRealTimeNotificationsSSE.js
Normal file
0
src/hooks/useServerSentEvents.js
Normal file
0
src/hooks/useServerSentEvents.js
Normal file
@@ -454,7 +454,7 @@ export default function Documents() {
|
||||
{ped.pedimento}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-700">{ped.fechapago}</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-700">{ped.fecha_pago}</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-700 max-w-xs truncate" title={ped.contribuyente}>{ped.contribuyente}</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-700">{ped.curp_apoderado}</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900 font-semibold">${ped.importe_total}</td>
|
||||
|
||||
@@ -327,190 +327,218 @@ const [docsPrev, setDocsPrev] = useState(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-gray-50 h-full flex flex-col">
|
||||
{/* Modal de vista previa resizable */}
|
||||
<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">
|
||||
{/* Modal de vista previa mejorado */}
|
||||
{previewOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
||||
<div
|
||||
className="bg-white rounded-xl shadow-2xl resize overflow-auto relative flex flex-col border border-blue-200"
|
||||
style={{ minWidth: '350px', minHeight: '300px', maxWidth: '600px', maxHeight: '90vh', width: '500px', height: '80vh', display: 'flex', flexDirection: 'column' }}
|
||||
>
|
||||
{/* Header mejorado del modal */}
|
||||
<div className="flex items-center justify-between px-6 py-4 bg-gradient-to-r from-blue-50 to-blue-100 border-b border-blue-200 rounded-t-xl sticky top-0">
|
||||
<div className="flex items-center gap-3 ">
|
||||
<div className="bg-blue-200 rounded-full p-2 flex items-center justify-center">
|
||||
<svg className="h-6 w-6 text-blue-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm p-4">
|
||||
<div className="bg-white/95 backdrop-blur-xl rounded-2xl shadow-2xl border border-blue-200/50 overflow-hidden relative flex flex-col w-full max-w-4xl h-full max-h-[90vh]">
|
||||
{/* Header del modal con gradiente */}
|
||||
<div className="flex items-center justify-between px-6 py-4 bg-gradient-to-r from-blue-500 to-blue-700 text-white">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-white/20 rounded-xl p-2">
|
||||
<svg className="h-6 w-6" 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>
|
||||
</div>
|
||||
<h3 className="text-xl font-extrabold text-blue-900 tracking-tight">Vista previa de documento</h3>
|
||||
<h3 className="text-xl font-bold">Vista previa de documento</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleClosePreview}
|
||||
className="ml-2 text-blue-600 hover:text-blue-800 bg-blue-100 hover:bg-blue-200 rounded-full p-2 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||
title="Cerrar"
|
||||
className="p-2 bg-white/20 hover:bg-white/30 rounded-xl transition-colors duration-200"
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Contenido del modal */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
{previewLoading ? (
|
||||
<div className="text-center py-8 text-gray-500 flex-1 flex items-center justify-center">Cargando documento...</div>
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center space-x-3 text-blue-600">
|
||||
<svg className="animate-spin h-8 w-8" 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>
|
||||
<span className="text-lg font-medium">Cargando documento...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : previewError ? (
|
||||
<div className="text-center py-8 text-danger-600 flex-1 flex items-center justify-center">{previewError}</div>
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center text-red-600">
|
||||
<svg className="w-12 h-12 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<p className="text-lg font-medium">{previewError}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : previewType === 'pdf' ? (
|
||||
<iframe src={previewUrl} title="PDF Preview" className="border rounded flex-1" style={{ width: '100%', height: '100%' }} />
|
||||
<iframe src={previewUrl} title="PDF Preview" className="flex-1 w-full border-0" />
|
||||
) : previewType === 'img' ? (
|
||||
<img src={previewUrl} alt="Vista previa" className="max-w-full max-h-full mx-auto flex-1" style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
|
||||
<div className="flex-1 flex items-center justify-center p-4">
|
||||
<img src={previewUrl} alt="Vista previa" className="max-w-full max-h-full object-contain" />
|
||||
</div>
|
||||
) : previewType === 'xml' ? (
|
||||
<div className="bg-white border rounded p-0 overflow-auto flex-1" style={{ fontFamily: 'Fira Mono, monospace', fontSize: '13px', width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<div className="bg-gray-100 px-3 py-2 text-xs text-gray-800 border-b border-gray-200 flex items-center justify-between" style={{ flexShrink: 0 }}>
|
||||
<span>Vista XML</span>
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="bg-gray-50 px-4 py-2 border-b border-gray-200 flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-gray-700">Vista XML</span>
|
||||
<button
|
||||
className="text-xs text-blue-600 hover:text-blue-800 px-2 py-1 rounded border border-blue-300 bg-blue-100"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(previewXml);
|
||||
}}
|
||||
>Copiar</button>
|
||||
className="text-sm text-blue-600 hover:text-blue-800 px-3 py-1 rounded-lg border border-blue-300 bg-blue-50 hover:bg-blue-100 transition-colors"
|
||||
onClick={() => navigator.clipboard.writeText(previewXml)}
|
||||
>
|
||||
Copiar
|
||||
</button>
|
||||
</div>
|
||||
<pre
|
||||
className="hljs language-xml p-4 text-xs text-gray-900 flex-1"
|
||||
style={{
|
||||
background: 'white',
|
||||
margin: 0,
|
||||
overflow: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}}
|
||||
className="flex-1 p-4 overflow-auto text-sm bg-white"
|
||||
style={{ fontFamily: 'Fira Code, Monaco, monospace' }}
|
||||
dangerouslySetInnerHTML={{ __html: previewXmlHtml }}
|
||||
/>
|
||||
</div>
|
||||
) : previewUrl ? (
|
||||
<a href={previewUrl} target="_blank" rel="noopener noreferrer" className="text-blue-600 underline">Descargar archivo</a>
|
||||
<div className="flex-1 flex items-center justify-center p-8">
|
||||
<a
|
||||
href={previewUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center space-x-2 bg-blue-500 hover:bg-blue-600 text-white px-6 py-3 rounded-xl font-medium transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5" 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>Descargar archivo</span>
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Header mejorado */}
|
||||
<div className="mb-8 animate-fadein-slideup opacity-0" style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards' }}>
|
||||
<div className="max-w-7xl mx-auto relative overflow-hidden rounded-2xl shadow bg-gradient-to-r from-blue-50 via-white to-indigo-50 border border-blue-100 p-8 flex items-center gap-6">
|
||||
<div className="flex-shrink-0 bg-blue-100 rounded-full p-4 shadow-md animate-bounce-slow">
|
||||
<svg className="h-10 w-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
||||
{/* Header principal 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.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 flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
||||
<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 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>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div>
|
||||
<Link
|
||||
to="/expedientes"
|
||||
className="inline-flex items-center text-blue-600 hover:text-blue-800 transition-colors duration-200 mb-4"
|
||||
className="inline-flex items-center text-blue-600 hover:text-blue-800 transition-colors duration-200 mb-2"
|
||||
>
|
||||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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="M15 19l-7-7 7-7"></path>
|
||||
</svg>
|
||||
<span className="font-semibold text-base">Volver a la lista</span>
|
||||
<span className="font-medium">Volver a la lista</span>
|
||||
</Link>
|
||||
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
|
||||
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900 mb-1">
|
||||
Detalle de Pedimento
|
||||
{docsCount !== undefined && (
|
||||
<span className="inline-block bg-blue-200 text-blue-800 text-xs font-semibold px-2 py-0.5 rounded-full ml-2 animate-fade-in">
|
||||
{docsCount} documentos
|
||||
</span>
|
||||
)}
|
||||
</h1>
|
||||
<p className="text-lg text-blue-700/80 font-medium">Información completa del pedimento y documentos asociados</p>
|
||||
<p className="text-gray-600">
|
||||
Información completa del pedimento y documentos asociados
|
||||
</p>
|
||||
{docsCount !== undefined && (
|
||||
<div className="mt-2">
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
|
||||
📄 {docsCount} documentos
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Efecto decorativo de fondo */}
|
||||
<div className="absolute -top-10 -right-10 opacity-30 pointer-events-none select-none">
|
||||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none">
|
||||
<circle cx="60" cy="60" r="50" fill="url(#grad1)" />
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0" y1="0" x2="120" y2="120" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#3b82f6" stopOpacity="0.15" />
|
||||
<stop offset="1" stopColor="#6366f1" stopOpacity="0.10" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
{/* Animación personalizada para el icono y contador */}
|
||||
<style>{`
|
||||
@keyframes bounce-slow {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
.animate-bounce-slow {
|
||||
animation: bounce-slow 2.2s infinite;
|
||||
}
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; transform: scale(0.9); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.7s ease;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contenido scrolleable */}
|
||||
<div className="flex-1 ">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
|
||||
{/* Información del Pedimento */}
|
||||
<div className="bg-white shadow-lg rounded-xl border border-gray-200 mb-8 animate-fadein-slideup opacity-0"
|
||||
<div className="bg-white/80 backdrop-blur-xl shadow-xl rounded-2xl border border-blue-100/50 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="px-8 py-6 border-b border-gray-200 flex items-center gap-4">
|
||||
<h2 className="text-2xl font-extrabold text-blue-800 tracking-tight">Información General</h2>
|
||||
<div className="h-1 w-10 bg-blue-400 rounded"></div>
|
||||
<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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="p-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="bg-gray-50 p-6 rounded-xl border border-gray-200 shadow-sm transition-all duration-400 hover:scale-105 hover:shadow-lg">
|
||||
<dt className="text-sm font-semibold text-gray-700 mb-2">Pedimento</dt>
|
||||
<dd className="text-2xl font-bold text-gray-900">{pedimento.pedimento}</dd>
|
||||
<h2 className="text-xl font-bold text-gray-900">Información General</h2>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-6 rounded-xl border border-gray-200 shadow-sm transition-all duration-400 hover:scale-105 hover:shadow-lg">
|
||||
<dt className="text-sm font-semibold text-gray-700 mb-2">Contribuyente</dt>
|
||||
<dd className="text-2xl font-bold text-gray-900">{pedimento.contribuyente}</dd>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-6 rounded-xl border border-gray-200 shadow-sm transition-all duration-400 hover:scale-105 hover:shadow-lg">
|
||||
<dt className="text-sm font-semibold text-gray-700 mb-2">Fecha de Pago</dt>
|
||||
<dd className="text-2xl font-bold text-gray-900">{pedimento.fechapago}</dd>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-6 rounded-xl border border-gray-200 shadow-sm transition-all duration-400 hover:scale-105 hover:shadow-lg">
|
||||
<dt className="text-sm font-semibold text-gray-700 mb-2">Importe Total</dt>
|
||||
<dd className="text-2xl font-bold text-gray-900">${pedimento.importe_total || 'N/A'}</dd>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-6 rounded-xl border border-gray-200 shadow-sm transition-all duration-400 hover:scale-105 hover:shadow-lg">
|
||||
<dt className="text-sm font-semibold text-gray-700 mb-2">Saldo Disponible</dt>
|
||||
<dd className="text-2xl font-bold text-gray-900">${pedimento.saldo_disponible || 'N/A'}</dd>
|
||||
</div>
|
||||
|
||||
<div className={`p-6 rounded-xl border shadow-sm transition-all duration-400 hover:scale-105 hover:shadow-lg ${pedimento.existe_expediente
|
||||
? 'bg-green-50 border-green-200'
|
||||
: 'bg-red-50 border-red-200'
|
||||
}`}>
|
||||
<dt className={`text-sm font-semibold mb-2 ${pedimento.existe_expediente ? 'text-green-700' : 'text-red-700'}`}>
|
||||
Expediente
|
||||
<div className="p-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
<div className="bg-gradient-to-br from-blue-50 to-blue-100/50 p-4 rounded-xl border border-blue-200/50 shadow-sm hover:shadow-md transition-all duration-300 hover:scale-105">
|
||||
<dt className="text-sm font-semibold text-blue-700 mb-2 flex items-center">
|
||||
<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="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
||||
</svg>
|
||||
Pedimento
|
||||
</dt>
|
||||
<dd className={`text-2xl font-bold flex items-center ${pedimento.existe_expediente ? 'text-green-900' : 'text-red-900'}`}>
|
||||
<svg className={`w-6 h-6 mr-2 ${pedimento.existe_expediente ? 'text-green-600' : 'text-red-600'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<dd className="text-xl font-bold text-gray-900">{pedimento.pedimento}</dd>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-green-50 to-green-100/50 p-4 rounded-xl border border-green-200/50 shadow-sm hover:shadow-md transition-all duration-300 hover:scale-105">
|
||||
<dt className="text-sm font-semibold text-green-700 mb-2 flex items-center">
|
||||
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
Contribuyente
|
||||
</dt>
|
||||
<dd className="text-xl font-bold text-gray-900">{pedimento.contribuyente}</dd>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-purple-50 to-purple-100/50 p-4 rounded-xl border border-purple-200/50 shadow-sm hover:shadow-md transition-all duration-300 hover:scale-105">
|
||||
<dt className="text-sm font-semibold text-purple-700 mb-2 flex items-center">
|
||||
<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="M8 7V3a4 4 0 118 0v4m-4 6v6m-4-6h8" />
|
||||
</svg>
|
||||
Fecha de Pago
|
||||
</dt>
|
||||
<dd className="text-xl font-bold text-gray-900">{pedimento.fecha_pago}</dd>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-yellow-50 to-yellow-100/50 p-4 rounded-xl border border-yellow-200/50 shadow-sm hover:shadow-md transition-all duration-300 hover:scale-105">
|
||||
<dt className="text-sm font-semibold text-yellow-700 mb-2 flex items-center">
|
||||
<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 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1" />
|
||||
</svg>
|
||||
Importe Total
|
||||
</dt>
|
||||
<dd className="text-xl font-bold text-gray-900">${pedimento.importe_total || 'N/A'}</dd>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-indigo-50 to-indigo-100/50 p-4 rounded-xl border border-indigo-200/50 shadow-sm hover:shadow-md transition-all duration-300 hover:scale-105">
|
||||
<dt className="text-sm font-semibold text-indigo-700 mb-2 flex items-center">
|
||||
<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="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
|
||||
</svg>
|
||||
Saldo Disponible
|
||||
</dt>
|
||||
<dd className="text-xl font-bold text-gray-900">${pedimento.saldo_disponible || 'N/A'}</dd>
|
||||
</div>
|
||||
|
||||
<div className={`p-4 rounded-xl border shadow-sm hover:shadow-md transition-all duration-300 hover:scale-105 ${pedimento.existe_expediente
|
||||
? 'bg-gradient-to-br from-emerald-50 to-emerald-100/50 border-emerald-200/50'
|
||||
: 'bg-gradient-to-br from-red-50 to-red-100/50 border-red-200/50'
|
||||
}`}>
|
||||
<dt className={`text-sm font-semibold mb-2 flex items-center ${pedimento.existe_expediente ? 'text-emerald-700' : 'text-red-700'}`}>
|
||||
<svg className={`w-4 h-4 mr-2 ${pedimento.existe_expediente ? 'text-emerald-600' : 'text-red-600'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{pedimento.existe_expediente ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
)}
|
||||
</svg>
|
||||
Expediente
|
||||
</dt>
|
||||
<dd className={`text-xl font-bold flex items-center ${pedimento.existe_expediente ? 'text-emerald-900' : 'text-red-900'}`}>
|
||||
{pedimento.existe_expediente ? 'Disponible' : 'No disponible'}
|
||||
</dd>
|
||||
</div>
|
||||
@@ -520,78 +548,33 @@ const [docsPrev, setDocsPrev] = useState(null);
|
||||
|
||||
{/* Sección de Documentos */}
|
||||
<div ref={focusKeeperRef} tabIndex={-1} style={{position:'absolute',width:0,height:0,overflow:'hidden',outline:'none'}} aria-hidden="true"></div>
|
||||
<div className="bg-white shadow-lg rounded-xl border border-gray-200 animate-fadein-slideup opacity-0"
|
||||
<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' }}>
|
||||
<div className="px-8 py-6 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
{/* 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>
|
||||
<h2 className="text-2xl font-extrabold text-blue-800 tracking-tight mb-1">
|
||||
Documentos Relacionados
|
||||
</h2>
|
||||
<div className="h-1 w-10 bg-blue-400 rounded mb-2"></div>
|
||||
<div className="flex items-center mt-1">
|
||||
<span className="text-sm text-gray-600 bg-blue-50 px-3 py-1 rounded-full font-semibold">
|
||||
<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">Documentos Relacionados</h2>
|
||||
</div>
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
|
||||
📄 {docsCount} documentos
|
||||
</span>
|
||||
</div>
|
||||
{/* Filtro de tipo de documento */}
|
||||
<div className="mt-4">
|
||||
{/* Filtros avanzados */}
|
||||
<div className="mb-4 flex flex-wrap gap-4 items-end">
|
||||
{/* Archivo */}
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Archivo</label>
|
||||
<input
|
||||
type="text"
|
||||
value={fileNameFilter}
|
||||
onChange={e => setFileNameFilter(e.target.value)}
|
||||
placeholder="Buscar archivo..."
|
||||
className="w-44 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-gray-50"
|
||||
/>
|
||||
</div>
|
||||
{/* Extensión */}
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Extensión</label>
|
||||
<select
|
||||
value={extensionFilter}
|
||||
onChange={e => setExtensionFilter(e.target.value)}
|
||||
className="w-32 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-gray-50"
|
||||
>
|
||||
<option value="">Todas</option>
|
||||
{[...new Set(documents.map(d => d.extension).filter(Boolean))].map(ext => (
|
||||
<option key={ext} value={ext}>{ext}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{/* Fecha */}
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Fecha</label>
|
||||
<input
|
||||
type="date"
|
||||
value={dateFilter}
|
||||
onChange={e => setDateFilter(e.target.value)}
|
||||
className="w-36 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-gray-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1">Tipo de documento</label>
|
||||
<select
|
||||
value={documentTypeFilter}
|
||||
onChange={e => setDocumentTypeFilter(e.target.value)}
|
||||
className="w-64 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-gray-50"
|
||||
>
|
||||
{documentTypeOptions.map(opt => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Botones de acción */}
|
||||
{allDocIds.length > 0 && (
|
||||
<div className="flex space-x-3">
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<button
|
||||
onClick={() => handleBulkDownload(allDocIds)}
|
||||
disabled={downloading}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:scale-105 shadow-lg"
|
||||
className="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-xl transition-all duration-200 transform hover:scale-105 shadow-lg disabled:opacity-50"
|
||||
>
|
||||
<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" />
|
||||
@@ -602,7 +585,7 @@ const [docsPrev, setDocsPrev] = useState(null);
|
||||
<button
|
||||
onClick={() => handleBulkDownload(selected)}
|
||||
disabled={selected.length === 0 || downloading}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200 transform hover:scale-105 shadow-lg"
|
||||
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 disabled:opacity-50"
|
||||
>
|
||||
<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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
@@ -613,121 +596,199 @@ const [docsPrev, setDocsPrev] = useState(null);
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* ...existing code... */}
|
||||
<div className="overflow-hidden">
|
||||
<div className="overflow-x-auto" id="tabla-documentos">
|
||||
<div style={{ minHeight: 'calc(8 * 56px)', maxHeight: 'calc(8 * 56px)', overflowY: documents.length > 8 ? 'auto' : 'hidden', position: 'relative' }}>
|
||||
<table className="min-w-full divide-y divide-gray-200 rounded-lg overflow-hidden text-xs font-normal">
|
||||
<thead className="bg-gray-50 sticky top-0 z-20">
|
||||
|
||||
{/* Filtros mejorados */}
|
||||
<div className="px-6 py-4 bg-gray-50/50 border-b border-gray-200/50">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
||||
{/* Tipo de documento */}
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1">Tipo</label>
|
||||
<select
|
||||
value={documentTypeFilter}
|
||||
onChange={e => setDocumentTypeFilter(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"
|
||||
>
|
||||
{documentTypeOptions.map(opt => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Archivo */}
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1">Archivo</label>
|
||||
<input
|
||||
type="text"
|
||||
value={fileNameFilter}
|
||||
onChange={e => setFileNameFilter(e.target.value)}
|
||||
placeholder="Buscar archivo..."
|
||||
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>
|
||||
|
||||
{/* Extensión */}
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1">Extensión</label>
|
||||
<select
|
||||
value={extensionFilter}
|
||||
onChange={e => setExtensionFilter(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="">Todas</option>
|
||||
{[...new Set(documents.map(d => d.extension).filter(Boolean))].map(ext => (
|
||||
<option key={ext} value={ext}>{ext}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Fecha */}
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1">Fecha</label>
|
||||
<input
|
||||
type="date"
|
||||
value={dateFilter}
|
||||
onChange={e => setDateFilter(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 limpiar filtros */}
|
||||
<div className="flex items-end">
|
||||
<button
|
||||
onClick={() => {
|
||||
setFileNameFilter('');
|
||||
setExtensionFilter('');
|
||||
setDateFilter('');
|
||||
setDocumentTypeFilter('');
|
||||
}}
|
||||
className="w-full px-3 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>
|
||||
{/* Contenido de documentos */}
|
||||
<div className="p-6">
|
||||
{docsLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center space-x-3 text-blue-600">
|
||||
<svg className="animate-spin h-8 w-8" 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>
|
||||
<span className="text-lg font-medium">Cargando documentos...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : docsError ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="text-center text-red-600">
|
||||
<svg className="w-12 h-12 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<p className="text-lg font-medium">Error: {docsError}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : documents.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<div className="mx-auto w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
|
||||
<svg className="w-8 h-8 text-gray-400" 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>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Sin documentos</h3>
|
||||
<p className="text-gray-500">No hay documentos relacionados con este pedimento.</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 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-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap align-middle" style={{ minWidth: '36px', width: '36px', maxWidth: '36px' }}>
|
||||
<th className="px-4 py-3 text-left text-xs font-bold text-white uppercase tracking-wider">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allSelected}
|
||||
onChange={handleSelectAll}
|
||||
className="h-3.5 w-3.5 text-blue-600 focus:ring-blue-500 border-gray-300 rounded align-middle"
|
||||
style={{ minWidth: '0.85rem', minHeight: '0.85rem' }}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left text-[11px] font-bold text-gray-600 uppercase tracking-wider border-b border-gray-200 whitespace-nowrap cursor-pointer select-none align-middle max-w-[180px]" style={{ minWidth: '120px' }} onClick={() => {
|
||||
<th className="px-4 py-3 text-left text-xs font-bold text-white uppercase tracking-wider cursor-pointer" onClick={() => {
|
||||
setOrderBy('archivo');
|
||||
setOrderDir(orderBy === 'archivo' && orderDir === 'asc' ? 'desc' : 'asc');
|
||||
}}>
|
||||
Archivo {orderBy === 'archivo' && (<span className="ml-1">{orderDir === 'asc' ? '▲' : '▼'}</span>)}
|
||||
</th>
|
||||
<th className="px-2 py-2 text-left text-[11px] font-bold text-gray-600 uppercase tracking-wider border-b border-gray-200 whitespace-nowrap cursor-pointer select-none align-middle" style={{ minWidth: '90px' }} onClick={() => {
|
||||
<th className="px-4 py-3 text-left text-xs font-bold text-white uppercase tracking-wider cursor-pointer" onClick={() => {
|
||||
setOrderBy('document_type');
|
||||
setOrderDir(orderBy === 'document_type' && orderDir === 'asc' ? 'desc' : 'asc');
|
||||
}}>
|
||||
Tipo {orderBy === 'document_type' && (<span className="ml-1">{orderDir === 'asc' ? '▲' : '▼'}</span>)}
|
||||
</th>
|
||||
<th className="px-2 py-2 text-left text-[11px] font-bold text-gray-600 uppercase tracking-wider border-b border-gray-200 whitespace-nowrap cursor-pointer select-none align-middle" style={{ minWidth: '70px' }} onClick={() => {
|
||||
<th className="px-4 py-3 text-left text-xs font-bold text-white uppercase tracking-wider cursor-pointer" onClick={() => {
|
||||
setOrderBy('extension');
|
||||
setOrderDir(orderBy === 'extension' && orderDir === 'asc' ? 'desc' : 'asc');
|
||||
}}>
|
||||
Extensión {orderBy === 'extension' && (<span className="ml-1">{orderDir === 'asc' ? '▲' : '▼'}</span>)}
|
||||
</th>
|
||||
<th className="px-2 py-2 text-left text-[11px] font-bold text-gray-600 uppercase tracking-wider border-b border-gray-200 whitespace-nowrap cursor-pointer select-none align-middle" style={{ minWidth: '70px' }} onClick={() => {
|
||||
<th className="px-4 py-3 text-left text-xs font-bold text-white uppercase tracking-wider cursor-pointer" onClick={() => {
|
||||
setOrderBy('size');
|
||||
setOrderDir(orderBy === 'size' && orderDir === 'asc' ? 'desc' : 'asc');
|
||||
}}>
|
||||
Tamaño {orderBy === 'size' && (<span className="ml-1">{orderDir === 'asc' ? '▲' : '▼'}</span>)}
|
||||
</th>
|
||||
<th className="px-2 py-2 text-left text-[11px] font-bold text-gray-600 uppercase tracking-wider border-b border-gray-200 whitespace-nowrap cursor-pointer select-none align-middle" style={{ minWidth: '90px' }} onClick={() => {
|
||||
<th className="px-4 py-3 text-left text-xs font-bold text-white uppercase tracking-wider cursor-pointer" onClick={() => {
|
||||
setOrderBy('created_at');
|
||||
setOrderDir(orderBy === 'created_at' && orderDir === 'asc' ? 'desc' : 'asc');
|
||||
}}>
|
||||
Fecha {orderBy === 'created_at' && (<span className="ml-1">{orderDir === 'asc' ? '▲' : '▼'}</span>)}
|
||||
</th>
|
||||
<th className="px-2 py-2 text-center text-[11px] font-bold text-gray-600 uppercase tracking-wider border-b border-gray-200 whitespace-nowrap align-middle" style={{ minWidth: '80px' }}>
|
||||
Acción
|
||||
<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 divide-y divide-gray-200 text-[13px]" style={{ position: 'relative', minHeight: 'calc(8 * 40px)' }}>
|
||||
{docsLoading ? (
|
||||
<tr>
|
||||
<td colSpan={7} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
|
||||
<div className="flex items-center justify-center h-full w-full absolute left-0 top-0" style={{ minHeight: 'calc(8 * 56px)', background: 'rgba(255,255,255,0.7)', zIndex: 10 }}>
|
||||
<span className="text-gray-500 text-lg">Cargando documentos...</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : docsError ? (
|
||||
<tr>
|
||||
<td colSpan={7} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
|
||||
<div className="flex items-center justify-center h-full w-full absolute left-0 top-0" style={{ minHeight: 'calc(8 * 56px)', background: 'rgba(255,255,255,0.7)', zIndex: 10 }}>
|
||||
<span className="text-danger-600 text-lg">Error: {docsError}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : documents.length > 0 ? (
|
||||
<>
|
||||
<tbody className="bg-white/50 divide-y divide-gray-100">
|
||||
{documents
|
||||
// Filtro por tipo de documento
|
||||
.filter(doc => {
|
||||
if (!documentTypeFilter) return true;
|
||||
return String(doc.document_type) === String(documentTypeFilter);
|
||||
})
|
||||
// Filtro por nombre de archivo
|
||||
.filter(doc => {
|
||||
if (!fileNameFilter) return true;
|
||||
const fileName = doc.archivo ? doc.archivo.split('/').pop().toLowerCase() : '';
|
||||
return fileName.includes(fileNameFilter.toLowerCase());
|
||||
})
|
||||
// Filtro por extensión
|
||||
.filter(doc => {
|
||||
if (!extensionFilter) return true;
|
||||
return doc.extension === extensionFilter;
|
||||
})
|
||||
// Filtro por fecha
|
||||
.filter(doc => {
|
||||
if (!dateFilter) return true;
|
||||
if (!doc.created_at) return false;
|
||||
const docDate = new Date(doc.created_at).toISOString().slice(0, 10);
|
||||
return docDate === dateFilter;
|
||||
})
|
||||
// Ordenamiento
|
||||
.sort((a, b) => {
|
||||
if (!orderBy) return 0;
|
||||
let aVal = a[orderBy];
|
||||
let bVal = b[orderBy];
|
||||
// Para archivo, usar solo el nombre
|
||||
if (orderBy === 'archivo') {
|
||||
aVal = a.archivo ? a.archivo.split('/').pop().toLowerCase() : '';
|
||||
bVal = b.archivo ? b.archivo.split('/').pop().toLowerCase() : '';
|
||||
}
|
||||
// Para fecha, convertir a Date
|
||||
if (orderBy === 'created_at') {
|
||||
aVal = a.created_at ? new Date(a.created_at) : new Date(0);
|
||||
bVal = b.created_at ? new Date(b.created_at) : new Date(0);
|
||||
}
|
||||
// Para tamaño, convertir a número
|
||||
if (orderBy === 'size') {
|
||||
aVal = Number(a.size) || 0;
|
||||
bVal = Number(b.size) || 0;
|
||||
}
|
||||
// Para document_type, convertir a número
|
||||
if (orderBy === 'document_type') {
|
||||
aVal = Number(a.document_type) || 0;
|
||||
bVal = Number(b.document_type) || 0;
|
||||
@@ -738,45 +799,44 @@ const [docsPrev, setDocsPrev] = useState(null);
|
||||
})
|
||||
.map((doc, index) => (
|
||||
<tr key={doc.id} className="hover:bg-blue-50 transition-all duration-200">
|
||||
<td className="px-2 py-2 whitespace-nowrap align-middle text-center" style={{ minWidth: '36px', width: '36px', maxWidth: '36px' }}>
|
||||
<td className="px-4 py-3 whitespace-nowrap">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.includes(doc.id)}
|
||||
onChange={() => handleSelect(doc.id)}
|
||||
className="h-3.5 w-3.5 text-blue-600 focus:ring-blue-500 border-gray-300 rounded align-middle"
|
||||
style={{ minWidth: '0.85rem', minHeight: '0.85rem' }}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap text-[13px] text-gray-900 max-w-[180px] truncate align-middle" style={{ minWidth: '120px' }}>
|
||||
<span className="truncate font-medium" title={doc.archivo || 'Sin nombre'}>
|
||||
<td className="px-4 py-3 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900 max-w-xs truncate" title={doc.archivo || 'Sin nombre'}>
|
||||
{doc.archivo ? doc.archivo.split('/').pop() : 'Sin nombre'}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-2 whitespace-nowrap align-middle">
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] font-medium bg-gray-100 text-gray-800">
|
||||
<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">
|
||||
{getDocumentTypeName(doc.document_type)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-2 py-2 whitespace-nowrap align-middle">
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] font-medium bg-blue-100 text-blue-800">
|
||||
<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-blue-100 text-blue-800">
|
||||
{doc.extension || 'N/A'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-2 py-2 whitespace-nowrap text-[13px] text-gray-700 align-middle">
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-700">
|
||||
{doc.size || 'N/A'}
|
||||
</td>
|
||||
<td className="px-2 py-2 whitespace-nowrap text-[13px] text-gray-700 align-middle">
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-700">
|
||||
{doc.created_at ? new Date(doc.created_at).toLocaleDateString('es-ES', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
}) : 'N/A'}
|
||||
</td>
|
||||
<td className="px-2 py-2 whitespace-nowrap text-center align-middle">
|
||||
<td className="px-4 py-3 whitespace-nowrap text-center">
|
||||
<div className="flex justify-center space-x-2">
|
||||
<button
|
||||
onClick={() => handlePreview(doc)}
|
||||
className="inline-flex items-center px-2 py-1 border border-gray-300 shadow-sm text-[11px] font-medium rounded-md 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"
|
||||
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="Vista previa"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -786,7 +846,7 @@ const [docsPrev, setDocsPrev] = useState(null);
|
||||
</button>
|
||||
<button
|
||||
onClick={() => downloadFile(doc.id, doc.archivo ? doc.archivo.split('/').pop() : `documento_${doc.id}`, showMessage)}
|
||||
className="inline-flex items-center px-2 py-1 border border-blue-300 shadow-sm text-[11px] font-medium rounded-md 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"
|
||||
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">
|
||||
@@ -797,109 +857,110 @@ const [docsPrev, setDocsPrev] = useState(null);
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{/* Rellenar con filas vacías si hay menos de 8 */}
|
||||
{documents.length < 8 && !docsLoading && !docsError && Array.from({length: 8 - documents.length}).map((_, idx) => (
|
||||
<tr key={`empty-${idx}`}>
|
||||
<td className="px-6 py-4 whitespace-nowrap" colSpan={7}> </td>
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={7} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
|
||||
<div className="flex flex-col items-center justify-center h-full w-full absolute left-0 top-0" style={{ minHeight: 'calc(8 * 56px)', background: 'rgba(255,255,255,0.7)', zIndex: 10 }}>
|
||||
<div className="mx-auto h-16 w-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
|
||||
<svg className="h-8 w-8 text-gray-400" 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>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Sin documentos</h3>
|
||||
<p className="text-gray-500">No hay documentos relacionados con este pedimento.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* Pagination block below the table, always visible at the bottom */}
|
||||
<div className="px-6 py-4 flex flex-col sm:flex-row items-center justify-between border-t border-gray-200 bg-gray-50 rounded-b-xl">
|
||||
{/* Selector de número de registros y paginación numerada */}
|
||||
{(() => {
|
||||
const totalPages = Math.max(1, Math.ceil(docsCount / pageSize));
|
||||
const maxPagesToShow = 5;
|
||||
let startPage = Math.max(1, page - Math.floor(maxPagesToShow / 2));
|
||||
let endPage = startPage + maxPagesToShow - 1;
|
||||
if (endPage > totalPages) {
|
||||
endPage = totalPages;
|
||||
startPage = Math.max(1, endPage - maxPagesToShow + 1);
|
||||
}
|
||||
const pageNumbers = [];
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
pageNumbers.push(i);
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row sm:items-center w-full gap-2 sm:gap-4 mt-2 sm:mt-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor="pageSize" className="text-xs text-gray-600 font-medium">Registros por página:</label>
|
||||
<select
|
||||
id="pageSize"
|
||||
value={pageSize}
|
||||
onChange={e => { setPageSize(Number(e.target.value)); setPage(1); }}
|
||||
className="border border-gray-300 rounded px-2 py-1 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white"
|
||||
</div>
|
||||
|
||||
{/* Vista Mobile - Cards */}
|
||||
<div className="lg:hidden space-y-4">
|
||||
{documents
|
||||
.filter(doc => {
|
||||
if (!documentTypeFilter) return true;
|
||||
return String(doc.document_type) === String(documentTypeFilter);
|
||||
})
|
||||
.filter(doc => {
|
||||
if (!fileNameFilter) return true;
|
||||
const fileName = doc.archivo ? doc.archivo.split('/').pop().toLowerCase() : '';
|
||||
return fileName.includes(fileNameFilter.toLowerCase());
|
||||
})
|
||||
.filter(doc => {
|
||||
if (!extensionFilter) return true;
|
||||
return doc.extension === extensionFilter;
|
||||
})
|
||||
.filter(doc => {
|
||||
if (!dateFilter) return true;
|
||||
if (!doc.created_at) return false;
|
||||
const docDate = new Date(doc.created_at).toISOString().slice(0, 10);
|
||||
return docDate === dateFilter;
|
||||
})
|
||||
.map((doc, index) => (
|
||||
<div
|
||||
key={doc.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` }}
|
||||
>
|
||||
{[5, 10, 20, 50, 100, 200, 400,600, 1200, 2400, 10000].map(size => (
|
||||
<option key={size} value={size}>{size}</option>
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="flex-shrink-0 pt-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.includes(doc.id)}
|
||||
onChange={() => handleSelect(doc.id)}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h3 className="text-sm font-semibold text-gray-900 truncate pr-2" title={doc.archivo || 'Sin nombre'}>
|
||||
{doc.archivo ? doc.archivo.split('/').pop() : 'Sin nombre'}
|
||||
</h3>
|
||||
<div className="flex space-x-1">
|
||||
<button
|
||||
onClick={() => handlePreview(doc)}
|
||||
className="p-2 text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||||
title="Vista previa"
|
||||
>
|
||||
<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
|
||||
onClick={() => downloadFile(doc.id, doc.archivo ? doc.archivo.split('/').pop() : `documento_${doc.id}`, showMessage)}
|
||||
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 space-x-4 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">
|
||||
{getDocumentTypeName(doc.document_type)}
|
||||
</span>
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full bg-blue-100 text-blue-800 font-medium">
|
||||
{doc.extension || 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs text-gray-500">
|
||||
<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="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4" />
|
||||
</svg>
|
||||
{doc.size || 'N/A'}
|
||||
</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>
|
||||
{doc.created_at ? new Date(doc.created_at).toLocaleDateString('es-ES', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
}) : 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
<button
|
||||
onClick={e => handlePageChange(1, e)}
|
||||
disabled={page === 1}
|
||||
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${page === 1 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
|
||||
>
|
||||
«
|
||||
</button>
|
||||
<button
|
||||
onClick={e => handlePageChange(page - 1, e)}
|
||||
disabled={page === 1}
|
||||
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${page === 1 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
{pageNumbers.map(num => (
|
||||
<button
|
||||
key={num}
|
||||
onClick={e => handlePageChange(num, e)}
|
||||
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${num === page ? 'bg-blue-600 text-white border-blue-700 cursor-default' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
|
||||
disabled={num === page}
|
||||
>
|
||||
{num}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
onClick={e => handlePageChange(page + 1, e)}
|
||||
disabled={page >= totalPages}
|
||||
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(page >= totalPages) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
|
||||
>
|
||||
›
|
||||
</button>
|
||||
<button
|
||||
onClick={e => handlePageChange(totalPages, e)}
|
||||
disabled={page >= totalPages}
|
||||
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(page >= totalPages) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
|
||||
>
|
||||
»
|
||||
</button>
|
||||
<span className="ml-3 text-xs text-gray-500">Página <span className="font-bold">{page}</span> de <span className="font-bold">{totalPages}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,79 +1,317 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
// 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')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'fadein-slideup-reportes';
|
||||
style.innerHTML = fadeInSlideUp;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
export default function Reportes() {
|
||||
const [tipoReporte, setTipoReporte] = useState('');
|
||||
const [fechaInicio, setFechaInicio] = useState('');
|
||||
const [fechaFin, setFechaFin] = useState('');
|
||||
|
||||
// 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' },
|
||||
];
|
||||
|
||||
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';
|
||||
};
|
||||
|
||||
const limpiarFiltros = () => {
|
||||
setTipoReporte('');
|
||||
setFechaInicio('');
|
||||
setFechaFin('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-gradient-to-br from-blue-50 to-blue-100 min-h-screen">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<h1 className="text-3xl font-extrabold mb-2 text-blue-900 tracking-tight">Reportes</h1>
|
||||
<p className="mb-6 text-gray-600">Consulta y descarga reportes relacionados con el sistema.</p>
|
||||
{/* Filtros de ejemplo */}
|
||||
<div className="flex flex-wrap gap-4 mb-6 bg-white p-4 rounded-lg shadow-sm">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-500 mb-1">Tipo de reporte</label>
|
||||
<select className="border rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-300">
|
||||
<option>General</option>
|
||||
<option>Usuarios</option>
|
||||
<option>Documentos</option>
|
||||
<option>Procesos</option>
|
||||
</select>
|
||||
<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">
|
||||
{/* 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>
|
||||
<label className="block text-xs font-semibold text-gray-500 mb-1">Fecha inicio</label>
|
||||
<input type="date" className="border rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-300" />
|
||||
<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>
|
||||
<label className="block text-xs font-semibold text-gray-500 mb-1">Fecha fin</label>
|
||||
<input type="date" className="border rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-300" />
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<button className="bg-blue-600 text-white px-4 py-2 rounded shadow hover:bg-blue-700 transition">Filtrar</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabla de reportes de ejemplo */}
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-lg font-bold text-blue-800">Resultados</h2>
|
||||
<button className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 transition flex items-center gap-2">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" /></svg>
|
||||
Descargar Excel
|
||||
{/* 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" />
|
||||
</svg>
|
||||
</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" />
|
||||
</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 text-sm text-left">
|
||||
<thead>
|
||||
<tr className="bg-blue-100 text-blue-900">
|
||||
<th className="py-2 px-4 font-semibold">ID</th>
|
||||
<th className="py-2 px-4 font-semibold">Nombre</th>
|
||||
<th className="py-2 px-4 font-semibold">Tipo</th>
|
||||
<th className="py-2 px-4 font-semibold">Fecha</th>
|
||||
<th className="py-2 px-4 font-semibold">Acciones</th>
|
||||
<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>
|
||||
<tr className="border-b hover:bg-blue-50">
|
||||
<td className="py-2 px-4">1</td>
|
||||
<td className="py-2 px-4">Reporte de usuarios</td>
|
||||
<td className="py-2 px-4">Usuarios</td>
|
||||
<td className="py-2 px-4">2025-07-22</td>
|
||||
<td className="py-2 px-4">
|
||||
<button className="text-blue-600 hover:underline">Ver</button>
|
||||
<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>
|
||||
<tr className="border-b hover:bg-blue-50">
|
||||
<td className="py-2 px-4">2</td>
|
||||
<td className="py-2 px-4">Reporte de documentos</td>
|
||||
<td className="py-2 px-4">Documentos</td>
|
||||
<td className="py-2 px-4">2025-07-21</td>
|
||||
<td className="py-2 px-4">
|
||||
<button className="text-blue-600 hover:underline">Ver</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/* Más filas de ejemplo aquí */}
|
||||
))}
|
||||
</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>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,315 +1,317 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
export default function Reports() {
|
||||
const tabs = [
|
||||
{ key: 'pedimentos', label: 'Generar Reporte de Pedimentos' },
|
||||
{ key: 'datastage', label: 'Generar Reporte de Datastage' },
|
||||
{ key: 'minimos', label: 'Generar Reporte de Mínimos' },
|
||||
{ key: 'coves', label: 'Generar Reporte de COVES' },
|
||||
];
|
||||
import React, { useState } from "react";
|
||||
|
||||
// Columnas por tipo de reporte y tipo de registro para datastage
|
||||
const columnasPorTab = {
|
||||
pedimentos: [
|
||||
{ key: 'id', label: 'ID' },
|
||||
{ key: 'nombre', label: 'Nombre' },
|
||||
{ key: 'pedimento', label: 'Pedimento' },
|
||||
{ key: 'fecha', label: 'Fecha' },
|
||||
{ key: 'estado', label: 'Estado' },
|
||||
],
|
||||
datastage: {
|
||||
entrada: [
|
||||
{ key: 'id', label: 'ID' },
|
||||
{ key: 'nombre', label: 'Nombre' },
|
||||
{ key: 'fecha', label: 'Fecha' },
|
||||
{ key: 'usuario', label: 'Usuario' },
|
||||
{ key: 'entrada', label: 'Entrada' },
|
||||
],
|
||||
salida: [
|
||||
{ key: 'id', label: 'ID' },
|
||||
{ key: 'nombre', label: 'Nombre' },
|
||||
{ key: 'fecha', label: 'Fecha' },
|
||||
{ key: 'usuario', label: 'Usuario' },
|
||||
{ key: 'salida', label: 'Salida' },
|
||||
],
|
||||
proceso: [
|
||||
{ key: 'id', label: 'ID' },
|
||||
{ key: 'nombre', label: 'Nombre' },
|
||||
{ key: 'fecha', label: 'Fecha' },
|
||||
{ key: 'usuario', label: 'Usuario' },
|
||||
{ key: 'proceso', label: 'Proceso' },
|
||||
],
|
||||
default: [
|
||||
{ key: 'id', label: 'ID' },
|
||||
{ key: 'nombre', label: 'Nombre' },
|
||||
{ key: 'fecha', label: 'Fecha' },
|
||||
{ key: 'usuario', label: 'Usuario' },
|
||||
],
|
||||
},
|
||||
minimos: [
|
||||
{ key: 'id', label: 'ID' },
|
||||
{ key: 'nombre', label: 'Nombre' },
|
||||
{ key: 'minimo', label: 'Mínimo' },
|
||||
{ key: 'fecha', label: 'Fecha' },
|
||||
],
|
||||
coves: [
|
||||
{ key: 'id', label: 'ID' },
|
||||
{ key: 'nombre', label: 'Nombre' },
|
||||
{ key: 'cove', label: 'COVE' },
|
||||
{ key: 'fecha', label: 'Fecha' },
|
||||
],
|
||||
};
|
||||
// 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')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'fadein-slideup-reportes';
|
||||
style.innerHTML = fadeInSlideUp;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
const [activeTab, setActiveTab] = useState('pedimentos');
|
||||
const [nombreReporte, setNombreReporte] = useState('');
|
||||
const [columnas, setColumnas] = useState(['id', 'nombre']);
|
||||
export default function Reportes() {
|
||||
const [tipoReporte, setTipoReporte] = useState('');
|
||||
const [fechaInicio, setFechaInicio] = useState('');
|
||||
const [fechaFin, setFechaFin] = useState('');
|
||||
const [pedimento, setPedimento] = useState('');
|
||||
const [tipoRegistro, setTipoRegistro] = useState('');
|
||||
|
||||
const handleColumnaChange = (col) => {
|
||||
setColumnas((prev) =>
|
||||
prev.includes(col)
|
||||
? prev.filter((c) => c !== col)
|
||||
: [...prev, col]
|
||||
);
|
||||
// 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' },
|
||||
];
|
||||
|
||||
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';
|
||||
};
|
||||
|
||||
const handleGenerarReporte = (e) => {
|
||||
e.preventDefault();
|
||||
alert(`Generando reporte: ${nombreReporte}\nTipo: ${activeTab}\nColumnas: ${columnas.join(', ')}\nPedimento: ${pedimento}\nFecha: ${fechaInicio} a ${fechaFin}`);
|
||||
const limpiarFiltros = () => {
|
||||
setTipoReporte('');
|
||||
setFechaInicio('');
|
||||
setFechaFin('');
|
||||
};
|
||||
|
||||
// Reset columnas al cambiar de tab o tipo de registro en datastage
|
||||
useEffect(() => {
|
||||
if (activeTab === 'datastage') {
|
||||
if (tipoRegistro && columnasPorTab.datastage[tipoRegistro]) {
|
||||
setColumnas(['id', 'nombre']);
|
||||
} else {
|
||||
setColumnas(['id', 'nombre']);
|
||||
}
|
||||
} else {
|
||||
setColumnas(['id', 'nombre']);
|
||||
}
|
||||
}, [activeTab, tipoRegistro]);
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-gray-50 min-h-screen">
|
||||
<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">
|
||||
{/* Header decorativo */}
|
||||
<div className="mb-8 relative overflow-hidden rounded-2xl shadow bg-gradient-to-r from-blue-50 via-white to-indigo-50 border border-blue-100 p-8 flex items-center gap-6">
|
||||
<div className="flex-shrink-0 bg-blue-100 rounded-full p-4 shadow-md animate-bounce-slow">
|
||||
<svg className="h-10 w-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{/* 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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" />
|
||||
</svg>
|
||||
</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>
|
||||
<div>
|
||||
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
|
||||
Reportes
|
||||
</h1>
|
||||
<p className="text-lg text-blue-700/80 font-medium">Consulta y descarga reportes relacionados con el sistema.</p>
|
||||
<h2 className="text-xl font-bold text-gray-900">Reportes disponibles</h2>
|
||||
</div>
|
||||
{/* Efecto decorativo de fondo */}
|
||||
<div className="absolute -top-10 -right-10 opacity-30 pointer-events-none select-none">
|
||||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none">
|
||||
<circle cx="60" cy="60" r="50" fill="url(#grad1)" />
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0" y1="0" x2="120" y2="120" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#3b82f6" stopOpacity="0.15" />
|
||||
<stop offset="1" stopColor="#6366f1" stopOpacity="0.10" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
{/* Animación personalizada para el icono */}
|
||||
<style>{`
|
||||
@keyframes bounce-slow {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
.animate-bounce-slow {
|
||||
animation: bounce-slow 2.2s infinite;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{/* Tabs y formulario en tarjeta */}
|
||||
<div className="mb-8 bg-white shadow-lg rounded-xl border border-gray-200">
|
||||
<div className="flex gap-2 mb-4 px-6 pt-6">
|
||||
{tabs.map(tab => (
|
||||
<button
|
||||
key={tab.key}
|
||||
className={`px-4 py-2 rounded-t font-semibold border-b-2 transition-all ${activeTab === tab.key ? 'bg-white border-blue-700 text-blue-800' : 'bg-blue-100 border-transparent text-blue-500 hover:bg-blue-200'}`}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
type="button"
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="px-6 pb-6 min-h-[340px] flex flex-col justify-between">
|
||||
<h2 className="text-xl font-bold text-blue-800 mb-2">{tabs.find(t => t.key === activeTab)?.label}</h2>
|
||||
<p className="text-gray-600 mb-4">Selecciona los campos y filtros que deseas incluir en tu reporte personalizado.</p>
|
||||
<form className="grid grid-cols-1 md:grid-cols-2 gap-4" onSubmit={handleGenerarReporte}>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-500 mb-1">Nombre del reporte</label>
|
||||
<input type="text" value={nombreReporte} onChange={e => setNombreReporte(e.target.value)} placeholder="Ej: Reporte personalizado" className="border rounded px-3 py-2 w-full text-sm focus:outline-none focus:ring-2 focus:ring-blue-300" />
|
||||
</div>
|
||||
{/* Pedimento y fechas para cada tab según requerimiento */}
|
||||
{activeTab === 'pedimentos' && (
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-500 mb-1">Pedimento específico</label>
|
||||
<input type="text" value={pedimento} onChange={e => setPedimento(e.target.value)} placeholder="Ej: 12345678" className="border rounded px-3 py-2 w-full text-sm focus:outline-none focus:ring-2 focus:ring-blue-300" />
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'datastage' && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-500 mb-1">Pedimento</label>
|
||||
<input type="text" value={pedimento} onChange={e => setPedimento(e.target.value)} placeholder="Ej: 12345678" className="border rounded px-3 py-2 w-full text-sm focus:outline-none focus:ring-2 focus:ring-blue-300" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-500 mb-1">Fecha inicial</label>
|
||||
<input type="date" value={fechaInicio} onChange={e => setFechaInicio(e.target.value)} className="border rounded px-3 py-2 w-full text-sm focus:outline-none focus:ring-2 focus:ring-blue-300" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-500 mb-1">Fecha final</label>
|
||||
<input type="date" value={fechaFin} onChange={e => setFechaFin(e.target.value)} className="border rounded px-3 py-2 w-full text-sm focus:outline-none focus:ring-2 focus:ring-blue-300" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-500 mb-1">Tipo de registro</label>
|
||||
<select
|
||||
className="border rounded px-3 py-2 w-full text-sm focus:outline-none focus:ring-2 focus:ring-blue-300"
|
||||
value={tipoRegistro}
|
||||
onChange={e => setTipoRegistro(e.target.value)}
|
||||
>
|
||||
<option value="">Selecciona...</option>
|
||||
<option value="entrada">Entrada</option>
|
||||
<option value="salida">Salida</option>
|
||||
<option value="proceso">Proceso</option>
|
||||
{/* Agrega más opciones según los registros disponibles */}
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{activeTab === 'coves' && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-500 mb-1">Pedimento</label>
|
||||
<input type="text" value={pedimento} onChange={e => setPedimento(e.target.value)} placeholder="Ej: 12345678" className="border rounded px-3 py-2 w-full text-sm focus:outline-none focus:ring-2 focus:ring-blue-300" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-500 mb-1">Fecha inicial</label>
|
||||
<input type="date" value={fechaInicio} onChange={e => setFechaInicio(e.target.value)} className="border rounded px-3 py-2 w-full text-sm focus:outline-none focus:ring-2 focus:ring-blue-300" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-500 mb-1">Fecha final</label>
|
||||
<input type="date" value={fechaFin} onChange={e => setFechaFin(e.target.value)} className="border rounded px-3 py-2 w-full text-sm focus:outline-none focus:ring-2 focus:ring-blue-300" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{activeTab === 'minimos' && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-500 mb-1">Pedimento</label>
|
||||
<input type="text" value={pedimento} onChange={e => setPedimento(e.target.value)} placeholder="Ej: 12345678" className="border rounded px-3 py-2 w-full text-sm focus:outline-none focus:ring-2 focus:ring-blue-300" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-500 mb-1">Fecha inicial</label>
|
||||
<input type="date" value={fechaInicio} onChange={e => setFechaInicio(e.target.value)} className="border rounded px-3 py-2 w-full text-sm focus:outline-none focus:ring-2 focus:ring-blue-300" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-500 mb-1">Fecha final</label>
|
||||
<input type="date" value={fechaFin} onChange={e => setFechaFin(e.target.value)} className="border rounded px-3 py-2 w-full text-sm focus:outline-none focus:ring-2 focus:ring-blue-300" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{/* Columnas a incluir solo si no es minimos */}
|
||||
{activeTab !== 'minimos' && (
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-xs font-semibold text-gray-500 mb-1">Columnas a incluir</label>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{activeTab === 'datastage'
|
||||
? (columnasPorTab.datastage[tipoRegistro] || columnasPorTab.datastage.default).map(col => (
|
||||
<label key={col.key} className="inline-flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-2"
|
||||
checked={columnas.includes(col.key)}
|
||||
onChange={() => handleColumnaChange(col.key)}
|
||||
/>
|
||||
{col.label}
|
||||
</label>
|
||||
))
|
||||
: columnasPorTab[activeTab].map(col => (
|
||||
<label key={col.key} className="inline-flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-2"
|
||||
checked={columnas.includes(col.key)}
|
||||
onChange={() => handleColumnaChange(col.key)}
|
||||
/>
|
||||
{col.label}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Fechas para los demás tabs - ya incluidas arriba */}
|
||||
<div className="md:col-span-2 flex justify-end mt-2">
|
||||
<button type="submit" className="bg-blue-700 text-white px-6 py-2 rounded shadow hover:bg-blue-800 transition">Generar reporte</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabla de reportes de ejemplo */}
|
||||
<div className="bg-white shadow-lg rounded-xl border border-gray-200 mt-8">
|
||||
<div className="flex justify-between items-center mb-4 px-6 pt-6">
|
||||
<h2 className="text-lg font-bold text-blue-800">Resultados</h2>
|
||||
<button className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 transition flex items-center gap-2">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" /></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 className="overflow-x-auto px-6 pb-6">
|
||||
<table className="min-w-full text-sm text-left">
|
||||
<thead>
|
||||
<tr className="bg-blue-100 text-blue-900">
|
||||
<th className="py-2 px-4 font-semibold">ID</th>
|
||||
<th className="py-2 px-4 font-semibold">Nombre</th>
|
||||
<th className="py-2 px-4 font-semibold">Tipo</th>
|
||||
<th className="py-2 px-4 font-semibold">Pedimento</th>
|
||||
<th className="py-2 px-4 font-semibold">Fecha</th>
|
||||
<th className="py-2 px-4 font-semibold">Acciones</th>
|
||||
</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>
|
||||
<tr className="border-b hover:bg-blue-50">
|
||||
<td className="py-2 px-4">1</td>
|
||||
<td className="py-2 px-4">Reporte de usuarios</td>
|
||||
<td className="py-2 px-4">Usuarios</td>
|
||||
<td className="py-2 px-4">12345678</td>
|
||||
<td className="py-2 px-4">2025-07-22</td>
|
||||
<td className="py-2 px-4">
|
||||
<button className="text-blue-600 hover:underline">Ver</button>
|
||||
<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>
|
||||
<tr className="border-b hover:bg-blue-50">
|
||||
<td className="py-2 px-4">2</td>
|
||||
<td className="py-2 px-4">Reporte de documentos</td>
|
||||
<td className="py-2 px-4">Documentos</td>
|
||||
<td className="py-2 px-4">87654321</td>
|
||||
<td className="py-2 px-4">2025-07-21</td>
|
||||
<td className="py-2 px-4">
|
||||
<button className="text-blue-600 hover:underline">Ver</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/* Más filas de ejemplo aquí */}
|
||||
))}
|
||||
</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>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user