4 Commits

2 changed files with 224 additions and 67 deletions

View File

@@ -312,72 +312,6 @@ export default function Admin() {
</div> </div>
</div> </div>
</div> </div>
{/* Análisis de actividad de usuario */}
{!(typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true') && !isGroup35 && (
<div className="bg-white rounded-3xl shadow-2xl border border-gray-100 p-4 sm:p-6 mb-6 sm:mb-8 animate-fadein-slideup opacity-0 relative overflow-hidden"
style={{
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.65s forwards',
}}
>
<div className="absolute inset-0 bg-gradient-to-br from-indigo-500/3 to-purple-500/3"></div>
<div className="relative z-10">
<div className="flex items-center gap-3 mb-4 sm:mb-6">
<div className="bg-gradient-to-br from-indigo-500 to-purple-600 rounded-full p-3 shadow-lg">
<svg className="h-6 w-6 sm:h-7 sm:w-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 8v8m-4-5v5m-4-2v2m-2 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
<h3 className="text-xl sm:text-2xl font-bold text-gray-900">Actividad de Usuarios</h3>
</div>
{loading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600"></div>
<span className="ml-3 text-gray-500">Cargando...</span>
</div>
) : error ? (
<div className="text-red-600 bg-red-50 p-4 rounded-xl border border-red-200">{error}</div>
) : userActivity ? (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 sm:gap-8">
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-2xl p-4 sm:p-6 border border-blue-100">
<h4 className="font-bold text-gray-800 mb-4 flex items-center gap-2">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
Resumen de acciones
</h4>
<div className="space-y-3">
{Object.entries(userActivity.actions_count).map(([action, count]) => (
<div key={action} className="flex justify-between items-center bg-white rounded-xl p-3 shadow-sm border border-blue-100">
<span className="capitalize text-gray-700 font-medium">{action}</span>
<span className="font-mono text-blue-700 bg-blue-100 px-2 py-1 rounded-lg text-sm font-bold">{count}</span>
</div>
))}
<div className="flex justify-between items-center bg-gradient-to-r from-blue-600 to-indigo-600 text-white rounded-xl p-3 shadow-lg font-semibold">
<span>Total actividades</span>
<span className="font-mono bg-white/20 px-2 py-1 rounded-lg">{userActivity.actividades_filtradas}</span>
</div>
</div>
</div>
<div className="bg-gradient-to-br from-green-50 to-emerald-50 rounded-2xl p-4 sm:p-6 border border-green-100">
<h4 className="font-bold text-gray-800 mb-4 flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
Top usuarios
</h4>
<div className="space-y-3">
{userActivity.top_users.map((user, idx) => (
<div key={user.username} className="flex justify-between items-center bg-white rounded-xl p-3 shadow-sm border border-green-100">
<div className="flex items-center gap-3">
<span className="bg-green-100 text-green-800 rounded-full w-6 h-6 flex items-center justify-center text-xs font-bold">{idx + 1}</span>
<span className="text-gray-700 font-medium">{user.username}</span>
</div>
<span className="font-mono text-green-700 bg-green-100 px-2 py-1 rounded-lg text-sm font-bold">{user.activity_count}</span>
</div>
))}
</div>
</div>
</div>
) : null}
</div>
</div>
)}
{/* Tabla de últimos documentos */} {/* Tabla de últimos documentos */}
<div className="bg-white rounded-3xl shadow-2xl border border-gray-100 p-4 sm:p-6 mb-6 sm:mb-8 animate-fadein-slideup opacity-0 relative overflow-hidden" <div className="bg-white rounded-3xl shadow-2xl border border-gray-100 p-4 sm:p-6 mb-6 sm:mb-8 animate-fadein-slideup opacity-0 relative overflow-hidden"

View File

@@ -1524,6 +1524,65 @@ export default function PedimentoDetail() {
}; };
// Funciones para procesar peticiones // Funciones para procesar peticiones
// Descargar todos los AcuseCoves
// Ejecutar la acción de procesar AcuseCove para todos los COVEs visibles
// Ejecutar la acción de procesar AcuseCove solo para los que no están descargados
const handleDownloadAllAcuseCoves = async () => {
if (!coves || coves.length === 0) {
showMessage('No hay AcuseCoves por descargar', 'info');
return;
}
const pendientes = coves.filter(cove => !cove.acuse_cove_descargado);
if (pendientes.length === 0) {
showMessage('No hay AcuseCoves pendientes por descargar', 'info');
return;
}
let successCount = 0;
let errorCount = 0;
for (const cove of pendientes) {
try {
await handleAcuseCoveProcess(cove);
successCount++;
} catch (error) {
errorCount++;
}
}
if (successCount > 0) {
showMessage(`${successCount} AcuseCove(s) procesados exitosamente`, 'success');
}
if (errorCount > 0) {
showMessage(`${errorCount} AcuseCove(s) no se pudieron procesar`, 'error');
}
};
// Ejecutar la acción de procesar COVE solo para los que no están descargados
const handleDownloadAllCoves = async () => {
if (!coves || coves.length === 0) {
showMessage('No hay COVEs por descargar', 'info');
return;
}
const pendientes = coves.filter(cove => !cove.cove_descargado);
if (pendientes.length === 0) {
showMessage('No hay COVEs pendientes por descargar', 'info');
return;
}
let successCount = 0;
let errorCount = 0;
for (const cove of pendientes) {
try {
await handleCoveProcess(cove);
successCount++;
} catch (error) {
errorCount++;
}
}
if (successCount > 0) {
showMessage(`${successCount} COVE(s) procesados exitosamente`, 'success');
}
if (errorCount > 0) {
showMessage(`${errorCount} COVE(s) no se pudieron procesar`, 'error');
}
};
const handleCoveRequest = async (cove) => { const handleCoveRequest = async (cove) => {
console.log('Request cove:', cove); console.log('Request cove:', cove);
showMessage(`Procesando petición para COVE #${cove.numero_cove}...`, 'info'); showMessage(`Procesando petición para COVE #${cove.numero_cove}...`, 'info');
@@ -2762,7 +2821,37 @@ export default function PedimentoDetail() {
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
{/* Tabla de partidas */} {/* Botón Descargar partidas y tabla de partidas */}
<div className="flex justify-end mb-2">
<button
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
onClick={async () => {
if (!partidas || partidas.length === 0) {
showMessage('No hay partidas para procesar', 'info');
return;
}
const pendientes = partidas.filter(p => !p.descargado);
if (pendientes.length === 0) {
showMessage('No hay partidas pendientes por descargar', 'info');
return;
}
let successCount = 0;
let errorCount = 0;
for (const partida of pendientes) {
try {
await handlePartidaRequest(partida);
successCount++;
} catch (error) {
errorCount++;
}
}
if (successCount > 0) showMessage(`${successCount} partida(s) procesadas exitosamente`, 'success');
if (errorCount > 0) showMessage(`${errorCount} partida(s) no se pudieron procesar`, 'error');
}}
>
Descargar partidas
</button>
</div>
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 rounded-lg"> <div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 rounded-lg">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-300"> <table className="min-w-full divide-y divide-gray-300">
@@ -2957,6 +3046,24 @@ export default function PedimentoDetail() {
</div> </div>
</div> </div>
{/* Botones para descargar todos los AcuseCoves y COVEs */}
{coves.length > 0 && (
<div className="mb-2 flex flex-col sm:flex-row gap-2 justify-end">
<button
onClick={handleDownloadAllAcuseCoves}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
Descargar todos los AcuseCoves
</button>
<button
onClick={handleDownloadAllCoves}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Descargar todos los COVEs
</button>
</div>
)}
{/* Filtros */} {/* Filtros */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 p-3 sm:p-4 bg-gray-50 rounded-lg border"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 p-3 sm:p-4 bg-gray-50 rounded-lg border">
<div> <div>
@@ -3230,6 +3337,64 @@ export default function PedimentoDetail() {
<div className="p-6"> <div className="p-6">
{/* Header de la sección */} {/* Header de la sección */}
<div className="mb-6 space-y-4"> <div className="mb-6 space-y-4">
<div className="flex flex-col gap-2 mb-2 sm:flex-row sm:justify-end">
<button
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
onClick={async () => {
if (!edocs || edocs.length === 0) {
showMessage('No hay EDocs para procesar', 'info');
return;
}
const pendientes = edocs.filter(e => !e.edocument_descargado);
if (pendientes.length === 0) {
showMessage('No hay EDocs pendientes por procesar', 'info');
return;
}
let successCount = 0;
let errorCount = 0;
for (const edoc of pendientes) {
try {
await handleEdocProcess(edoc);
successCount++;
} catch (error) {
errorCount++;
}
}
if (successCount > 0) showMessage(`${successCount} EDoc(s) procesados exitosamente`, 'success');
if (errorCount > 0) showMessage(`${errorCount} EDoc(s) no se pudieron procesar`, 'error');
}}
>
Descargar todos los EDocs
</button>
<button
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-colors"
onClick={async () => {
if (!edocs || edocs.length === 0) {
showMessage('No hay acuses de EDocs para descargar', 'info');
return;
}
const pendientes = edocs.filter(e => !e.acuse_descargado);
if (pendientes.length === 0) {
showMessage('No hay acuses de EDocs pendientes por descargar', 'info');
return;
}
let successCount = 0;
let errorCount = 0;
for (const edoc of pendientes) {
try {
await handleAcuseEdocDownload(edoc);
successCount++;
} catch (error) {
errorCount++;
}
}
if (successCount > 0) showMessage(`${successCount} acuse(s) de EDoc descargados exitosamente`, 'success');
if (errorCount > 0) showMessage(`${errorCount} acuse(s) de EDoc no se pudieron descargar`, 'error');
}}
>
Descargar Todos los acuses
</button>
</div>
<div className="flex flex-wrap items-center justify-between gap-4"> <div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<h3 className="text-lg font-semibold text-gray-900"> <h3 className="text-lg font-semibold text-gray-900">
@@ -3686,6 +3851,64 @@ export default function PedimentoDetail() {
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
{/* Tabla de Procesos */} {/* Tabla de Procesos */}
<div className="flex flex-col gap-2 mb-2 sm:flex-row sm:justify-end">
<button
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
onClick={async () => {
if (!edocs || edocs.length === 0) {
showMessage('No hay EDocs para procesar', 'info');
return;
}
const pendientes = edocs.filter(e => !e.edocument_descargado);
if (pendientes.length === 0) {
showMessage('No hay EDocs pendientes por descargar', 'info');
return;
}
let successCount = 0;
let errorCount = 0;
for (const edoc of pendientes) {
try {
await handleEdocProcess(edoc);
successCount++;
} catch (error) {
errorCount++;
}
}
if (successCount > 0) showMessage(`${successCount} EDoc(s) procesados exitosamente`, 'success');
if (errorCount > 0) showMessage(`${errorCount} EDoc(s) no se pudieron procesar`, 'error');
}}
>
Descargar todos los EDocs
</button>
<button
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-colors"
onClick={async () => {
if (!edocs || edocs.length === 0) {
showMessage('No hay Acuses de EDocs para procesar', 'info');
return;
}
const pendientes = edocs.filter(e => !e.acuse_descargado);
if (pendientes.length === 0) {
showMessage('No hay Acuses de EDocs pendientes por descargar', 'info');
return;
}
let successCount = 0;
let errorCount = 0;
for (const edoc of pendientes) {
try {
await handleAcuseEdocProcess(edoc);
successCount++;
} catch (error) {
errorCount++;
}
}
if (successCount > 0) showMessage(`${successCount} Acuse(s) de EDoc procesados exitosamente`, 'success');
if (errorCount > 0) showMessage(`${errorCount} Acuse(s) de EDoc no se pudieron procesar`, 'error');
}}
>
Descargar todos los Acuses de EDocs
</button>
</div>
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 rounded-lg"> <div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 rounded-lg">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-300"> <table className="min-w-full divide-y divide-gray-300">